Formularios en Presentadores

Nette Forms facilita enormemente la creación y el procesamiento de formularios web. En este capítulo, aprenderá a utilizar formularios dentro de los presentadores.

Si estás interesado en utilizarlos de forma completamente autónoma sin el resto del framework, existe una guía para formularios autónomos.

Primer formulario

Intentaremos escribir un sencillo formulario de registro. Su código será el siguiente:

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Nombre:');
$form->addPassword('password', 'Contraseña:');
$form->addSubmit('send', 'Regístrate');
$form->onSuccess[] = [$this, 'formSucceeded'];

y en el navegador el resultado debería verse así:

El formulario en el presentador es un objeto de la clase Nette\Application\UI\Form, su predecesor Nette\Forms\Form está pensado para uso independiente. Le añadimos los campos nombre, contraseña y botón de envío. Por último, la línea con $form->onSuccess dice que después de la presentación y la validación exitosa, el método $this->formSucceeded() debe ser llamado.

Desde el punto de vista del presentador, el formulario es un componente común. Por lo tanto, se trata como un componente y se incorpora al presentador utilizando el método factory. Tendrá el siguiente aspecto:

use Nette;
use Nette\Application\UI\Form;

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nombre:');
		$form->addPassword('password', 'Contraseña:');
		$form->addSubmit('send', 'Regístrate');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// aquí procesaremos los datos enviados por el formulario
		// $data->name contiene nombre
		// $data->password contiene contraseña
		$this->flashMessage('You have successfully signed up.');
		$this->redirect('Home:');
	}
}

Y el renderizado en la plantilla se realiza utilizando la etiqueta {control}:

<h1>Registro</h1>

{control registrationForm}

Y eso es todo :-) Tenemos un formulario funcional y perfectamente seguro.

Ahora probablemente estés pensando que ha sido demasiado rápido, preguntándote cómo es posible que se llame al método formSucceeded() y qué parámetros obtiene. Claro, tienes razón, esto merece una explicación.

A Nette se le ocurre un mecanismo genial, que llamamos estilo Hollywood. En lugar de tener que preguntar constantemente si ha pasado algo (“¿se ha enviado el formulario?”, “¿se ha enviado válidamente?” o “¿no se ha falsificado?”), le dices al framework “cuando el formulario se complete válidamente, llama a este método” y dejas que siga trabajando en ello. Si programas en JavaScript, estás familiarizado con este estilo de programación. Escribes funciones que son llamadas cuando ocurre un determinado evento. Y el lenguaje les pasa los argumentos apropiados.

Así es como se construye el código del presentador anterior. Array $form->onSuccess representa la lista de callbacks PHP que Nette llamará cuando el formulario sea enviado y rellenado correctamente. Dentro del ciclo de vida del presentador es una llamada señal, por lo que son llamadas después del método action* y antes del método render*. Y pasa a cada callback el propio formulario en el primer parámetro y los datos enviados como objeto ArrayHash en el segundo. Puedes omitir el primer parámetro si no necesitas el objeto formulario. El segundo parámetro puede ser aún más útil, pero sobre eso más adelante.

El objeto $data contiene las propiedades name y password con los datos introducidos por el usuario. Normalmente enviamos los datos directamente para su posterior procesamiento, que puede ser, por ejemplo, su inserción en la base de datos. Sin embargo, puede producirse un error durante el procesamiento, por ejemplo, que el nombre de usuario ya esté ocupado. En este caso, pasamos el error de vuelta al formulario usando addError() y dejamos que se redibuje, con un mensaje de error:

$form->addError('Lo sentimos, el nombre de usuario ya está en uso.');

Además de onSuccess, existe también onSubmit: los callbacks son llamados siempre después del envío del formulario, incluso si no se ha rellenado correctamente. Y finalmente onError: las llamadas de retorno sólo se activan si el envío no es válido. Incluso son llamados si invalidamos el formulario en onSuccess o onSubmit usando addError().

Después de procesar el formulario, redirigiremos a la página siguiente. Esto evita que el formulario sea reenviado involuntariamente pulsando el botón refresh, back, o moviendo el historial del navegador.

Prueba a añadir más controles de formulario.

Acceso a los controles

El formulario es un componente del presentador, en nuestro caso llamado registrationForm (por el nombre del método de fábrica createComponentRegistrationForm), por lo que en cualquier parte del presentador se puede acceder al formulario utilizando:

$form = $this->getComponent('registrationForm');
// sintaxis alternativa: $form = $this['registrationForm'];

También los controles individuales del formulario son componentes, por lo que puede acceder a ellos de la misma manera:

$input = $form->getComponent('name'); // or $input = $form['name'];
$button = $form->getComponent('send'); // or $button = $form['send'];

Los controles se eliminan utilizando unset:

unset($form['name']);

Reglas de validación

La palabra valid se usó varias veces, pero el formulario aún no tiene reglas de validación. Vamos a arreglarlo.

El nombre será obligatorio, así que lo marcaremos con el método setRequired(), cuyo argumento es el texto del mensaje de error que se mostrará si el usuario no lo rellena. Si no se da ningún argumento, se utilizará el mensaje de error por defecto.

$form->addText('name', 'Nombre:')
	->setRequired('Por favor, introduzca su nombre.');

Intente enviar el formulario sin el nombre rellenado y verá que se muestra un mensaje de error y el navegador o servidor lo rechazará hasta que lo rellene.

Al mismo tiempo, no podrá engañar al sistema escribiendo sólo espacios en la entrada, por ejemplo. De ninguna manera. Nette recorta automáticamente los espacios en blanco a izquierda y derecha. Pruébalo. Es algo que debería hacer siempre con cada entrada de una sola línea, pero a menudo se olvida. Nette lo hace automáticamente. (Puede intentar engañar a los formularios y enviar una cadena multilínea como nombre. Incluso en este caso, Nette no se dejará engañar y los saltos de línea cambiarán a espacios).

El formulario siempre se valida en el lado del servidor, pero también se genera la validación JavaScript, que es rápida y el usuario conoce el error inmediatamente, sin tener que enviar el formulario al servidor. De esto se encarga el script netteForms.js. Insértelo en la plantilla de diseño:

<script src="https://unpkg.com/nette-forms@3"></script>

Si mira en el código fuente de la página con formulario, puede observar que Nette inserta los campos obligatorios en elementos con una clase CSS required. Pruebe a añadir el siguiente estilo a la plantilla, y la etiqueta “Nombre” será de color rojo. Elegantemente, marcamos los campos obligatorios para los usuarios:

<style>
.required label { color: maroon }
</style>

Se añadirán reglas de validación adicionales mediante el método addRule(). El primer parámetro es la regla, el segundo es de nuevo el texto del mensaje de error, y el argumento opcional regla de validación puede seguir. ¿Qué significa esto?

El formulario recibirá otra entrada opcional edad con la condición, que tiene que ser un número (addInteger()) y en ciertos límites ($form::Range). Y aquí usaremos el tercer argumento de addRule(), el propio rango:

$form->addInteger('age', 'Edad:')
	->addRule($form::Range, 'Debes ser mayor de 18 años y tener menos de 120.', [18, 120]);

Si el usuario no rellena el campo, las reglas de validación no se verificarán, porque el campo es opcional.

Obviamente hay espacio para una pequeña refactorización. En el mensaje de error y en el tercer parámetro, los números aparecen por duplicado, lo que no es lo ideal. Si estuviéramos creando un formulario multilingüe y el mensaje que contiene los números tuviera que traducirse a varios idiomas, dificultaría el cambio de valores. Por esta razón, se pueden utilizar los caracteres sustitutos %d:

	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);

Volvamos al campo contraseña, hagámoslo requerido, y verifiquemos la longitud mínima de la contraseña ($form::MinLength), de nuevo utilizando los caracteres sustitutos en el mensaje:

$form->addPassword('password', 'Contraseña:')
	->setRequired('Elige una contraseña')
	->addRule($form::MinLength, 'Su contraseña debe tener una longitud mínima de %d', 8);

Añadiremos un campo passwordVerify al formulario, donde el usuario introduzca de nuevo la contraseña, para su comprobación. Usando reglas de validación, comprobamos si ambas contraseñas son iguales ($form::Equal). Y como argumento damos una referencia a la primera contraseña usando corchetes:

$form->addPassword('passwordVerify', 'Contraseña de nuevo:')
	->setRequired('Rellene de nuevo su contraseña para comprobar si hay algún error tipográfico')
	->addRule($form::Equal, 'Contraseña incorrecta', $form['password'])
	->setOmitted();

Usando setOmitted(), marcamos un elemento cuyo valor realmente no nos importa y que existe sólo para validación. Su valor no se pasa a $data.

Tenemos un formulario completamente funcional con validación en PHP y JavaScript. Las capacidades de validación de Nette son mucho más amplias, puedes crear condiciones, mostrar y ocultar partes de una página de acuerdo a ellas, etc. Puedes encontrarlo todo en el capítulo sobre validación de formularios.

Valores por defecto

A menudo establecemos valores por defecto para los controles de formulario:

$form->addEmail('email', 'Email')
	->setDefaultValue($lastUsedEmail);

A menudo es útil establecer valores por defecto para todos los controles a la vez. Por ejemplo, cuando el formulario se utiliza para editar registros. Leemos el registro de la base de datos y lo establecemos como valores por defecto:

//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);

Llama a setDefaults() después de definir los controles.

Representación del formulario

Por defecto, el formulario se muestra como una tabla. Los controles individuales siguen las pautas básicas de accesibilidad web. Todas las etiquetas se generan como elementos <label> y están asociadas a sus entradas; al hacer clic en la etiqueta, el cursor se desplaza a la entrada.

Podemos establecer cualquier atributo HTML para cada elemento. Por ejemplo, añadir un marcador de posición:

$form->addInteger('age', 'Edad:')
	->setHtmlAttribute('placeholder', 'Por favor, introduzca la edad');

Realmente hay muchas formas de renderizar un formulario, por lo que es un capítulo dedicado al renderizado.

Mapeo a Clases

Volvamos al método formSucceeded(), que en el segundo parámetro $data obtiene los datos enviados como un objeto ArrayHash. Al tratarse de una clase genérica, algo así como stdClass, careceremos de algunas comodidades a la hora de trabajar con ella, como la compleción de código para las propiedades en editores o el análisis estático de código. Esto podría solucionarse teniendo una clase específica para cada formulario, cuyas propiedades representen los controles individuales. Ej:

class RegistrationFormData
{
	public string $name;
	public int $age;
	public string $password;
}

Alternativamente, puede utilizar el constructor:

class RegistrationFormData
{
	public function __construct(
		public string $name,
		public int $age,
		public string $password,
	) {
	}
}

Las propiedades de la clase de datos también pueden ser enums y se asignarán automáticamente.

¿Cómo decirle a Nette que devuelva datos como objetos de esta clase? Más fácil de lo que piensa. Todo lo que tiene que hacer es especificar la clase como tipo del parámetro $data en el manejador:

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name es una instancia de RegistrationFormData
	$name = $data->name;
	// ...
}

También puedes especificar array como tipo y entonces pasará los datos como un array.

De manera similar, puede utilizar el método getValues(), que pasamos como nombre de clase u objeto a hidratar como parámetro:

$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;

Si los formularios consisten en una estructura multinivel compuesta por contenedores, cree una clase distinta para cada uno de ellos:

$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */

class PersonFormData
{
	public string $firstName;
	public string $lastName;
}

class RegistrationFormData
{
	public PersonFormData $person;
	public int $age;
	public string $password;
}

El mapeo entonces sabe por el tipo de propiedad $person que debe mapear el contenedor a la clase PersonFormData. Si la propiedad contiene una matriz de contenedores, proporcione el tipo array y pase la clase que debe asignarse directamente al contenedor:

$person->setMappedType(PersonFormData::class);

Puede generar una propuesta para la clase de datos de un formulario utilizando el método Nette\Forms\Blueprint::dataClass($form), que la imprimirá en la página del navegador. A continuación, puede simplemente hacer clic para seleccionar y copiar el código en su proyecto.

Botones de envío múltiples

Si el formulario tiene más de un botón, usualmente necesitamos distinguir cuál fue presionado. Podemos crear una función propia para cada botón. Establecerlo como un controlador para el evento onClick:

$form->addSubmit('save', 'Save')
	->onClick[] = [$this, 'saveButtonPressed'];

$form->addSubmit('delete', 'Delete')
	->onClick[] = [$this, 'deleteButtonPressed'];

Estos manejadores también son llamados sólo en el caso de que el formulario sea válido, como en el caso del evento onSuccess. La diferencia es que el primer parámetro puede ser el objeto submit button en lugar del formulario, dependiendo del tipo que especifiques:

public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
	$form = $button->getForm();
	// ...
}

Cuando un formulario se envía con la tecla Intro, se trata como si se hubiera enviado con el primer botón.

Evento onAnchor

Cuando construyes un formulario en un método de fábrica (como createComponentRegistrationForm), aún no sabe si ha sido enviado o los datos con los que fue enviado. Pero hay casos en los que necesitamos conocer los valores enviados, quizás dependa de ellos el aspecto que tendrá el formulario, o se utilicen para selectboxes dependientes, etc.

Por lo tanto, se puede hacer que el código que construye el formulario sea llamado cuando está anclado, es decir, ya está vinculado al presentador y conoce sus datos enviados. Pondremos tal código en el array $onAnchor:

$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// esta función será llamada cuando el formulario conozca los datos con los que fue enviado
	// por lo que puede utilizar el método getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Protección contra vulnerabilidades

Nette Framework pone un gran esfuerzo en ser seguro y dado que los formularios son la entrada más común del usuario, los formularios de Nette son tan buenos como impenetrables. Todo se mantiene de forma dinámica y transparente, no hay que configurar nada manualmente.

Además de proteger los formularios contra ataques dirigidos a vulnerabilidades bien conocidas como Cross-Site Scripting (XSS) y Cross-Site Request Forgery (CSRF), hace un montón de pequeñas tareas de seguridad en las que ya no tienes que pensar.

Por ejemplo, filtra todos los caracteres de control de las entradas y comprueba la validez de la codificación UTF-8, para que los datos del formulario estén siempre limpios. En el caso de las casillas de selección y las listas de radio, verifica que los elementos seleccionados sean realmente de los ofrecidos y que no haya habido ninguna falsificación. Ya hemos mencionado que para la entrada de texto de una sola línea, elimina los caracteres de final de línea que un atacante podría enviar allí. Para entradas multilínea, normaliza los caracteres de final de línea. Y así sucesivamente.

Nette soluciona para usted vulnerabilidades de seguridad que la mayoría de los programadores no tienen ni idea de que existen.

El mencionado ataque CSRF consiste en que un atacante atrae a la víctima para que visite una página que ejecuta silenciosamente una petición en el navegador de la víctima al servidor donde la víctima está conectada en ese momento, y el servidor cree que la petición ha sido realizada por la víctima a voluntad. Por lo tanto, Nette impide que el formulario sea enviado vía POST desde otro dominio. Si por alguna razón desea desactivar la protección y permitir que el formulario sea enviado desde otro dominio, utilice:

$form->allowCrossOrigin(); // ¡ATENCIÓN! ¡Desactiva la protección!

Esta protección utiliza una cookie de SameSite llamada _nss. La protección de cookies SameSite puede no ser 100% fiable, por lo que es una buena idea activar la protección de token:

$form->addProtection();

Se recomienda encarecidamente aplicar esta protección a los formularios en una parte administrativa de su aplicación que cambie datos sensibles. El framework protege contra un ataque CSRF generando y validando el token de autenticación que se almacena en una sesión (el argumento es el mensaje de error que se muestra si el token ha caducado). Por eso es necesario tener una sesión iniciada antes de mostrar el formulario. En la parte de administración del sitio web, la sesión suele estar ya iniciada, debido al login del usuario. En caso contrario, inicie la sesión con el método Nette\Http\Session::start().

Uso de un formulario en varios presentadores

Si necesitas utilizar un formulario en más de un presentador, te recomendamos que crees una fábrica para él, que luego pasarás al presentador. Una ubicación adecuada para una clase de este tipo es, por ejemplo, el directorio app/Forms.

La clase de fábrica podría tener el siguiente aspecto:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		return $form;
	}
}

Pedimos a la clase que produzca el formulario en el método de fábrica para componentes en el presentador:

public function __construct(
	private SignInFormFactory $formFactory,
) {
}

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// podemos cambiar el formulario, aquí por ejemplo cambiamos la etiqueta del botón
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // y añadimos el manejador
	return $form;
}

El manejador de procesamiento del formulario también puede ser entregado desde la fábrica:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addSubmit('send', 'Log in');
		$form->onSuccess[] = function (Form $form, $data): void {
			// procesamos aquí el formulario enviado
		};
		return $form;
	}
}

Así, tenemos una rápida introducción a los formularios en Nette. Intenta buscar en el directorio de ejemplos en la distribución para más inspiración.

versión: 4.0