Formularios en presenters

Nette Forms simplifica notablemente la creación y el procesamiento de formularios web. En este capítulo aprenderá a usar los formularios dentro de los presenters.

Si le interesa usarlos de forma completamente independiente, sin el resto del framework, tiene una guía sobre el uso independiente.

Primer formulario

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

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = $this->formSucceeded(...);

y en el navegador se mostrará así:

Un formulario en un presenter es un objeto de la clase Nette\Application\UI\Form; su antecesora Nette\Forms\Form está pensada para el uso independiente. Le hemos añadido elementos llamados name y password y un botón de envío. Por último, la línea $form->onSuccess dice que, tras el envío y una validación correcta, se debe llamar al método $this->formSucceeded().

Desde la perspectiva del presenter, el formulario es un componente corriente. Por eso se trata como un componente y se integra en el presenter con un método fábrica. Quedará así:

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

class HomePresenter extends Nette\Application\UI\Presenter
{
	protected function createComponentRegistrationForm(): Form
	{
		$form = new Form;
		$form->addText('name', 'Name:');
		$form->addPassword('password', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = $this->formSucceeded(...);
		return $form;
	}

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

Y en la plantilla, el formulario se renderiza con la etiqueta {control}:

<h1>Registration</h1>

{control registrationForm}

Y eso es básicamente todo :-) Tenemos un formulario funcional y perfectamente protegido.

Ahora estará pensando que ha ido demasiado rápido y se preguntará cómo es posible que se llame al método formSucceeded() y qué parámetros recibe. Sí, tiene razón, esto merece una explicación.

Nette introduce un mecanismo refrescante llamado estilo Hollywood. En lugar de que usted, como desarrollador, tenga que preguntar constantemente si ha pasado algo (“¿se ha enviado el formulario?”, “¿se ha enviado de forma válida?” y “¿no ha sido falsificado?”), le dice al framework “cuando el formulario esté válidamente rellenado, llama a este método” y le deja a él el trabajo restante. Si programa en JavaScript, conoce a fondo este estilo de programación. Escribe funciones que se llaman cuando ocurre un determinado evento. Y el lenguaje les pasa los argumentos adecuados.

Justamente así está construido el código del presenter de arriba. El array $form->onSuccess representa una lista de callbacks de PHP que Nette llama en el momento en que el formulario se envía y está correctamente rellenado (es decir, es válido). Dentro del ciclo de vida del presenter se trata de una señal, así que se llaman después del método action* y antes del método render*. Y a cada callback le pasa el propio formulario como primer parámetro y los datos enviados como objeto ArrayHash (o stdClass, o una clase propia) como segundo. Puede omitir el primer parámetro si no necesita el objeto del formulario. El segundo parámetro puede ser más inteligente, pero de eso hablaremos más adelante.

El objeto $data contiene las propiedades name y password con los datos introducidos por el usuario. Normalmente enviamos los datos directamente a su procesamiento posterior, que puede ser, por ejemplo, la inserción en una base de datos. Durante el procesamiento puede producirse un error, sin embargo, por ejemplo que el nombre de usuario ya esté ocupado. En ese caso devolvemos el error al formulario con addError() y dejamos que se renderice otra vez, junto con el mensaje de error.

$form->addError('Sorry, username is already in use.');

Además de onSuccess existe también onSubmit: los callbacks se llaman siempre que se envía el formulario, aunque no esté correctamente rellenado. Y también onError: los callbacks se llaman solo si el envío no es válido. Se llaman incluso si invalidamos el formulario en onSuccess con addError().

Tras procesar el formulario redirigimos a otra página. Eso evita el reenvío no deseado del formulario mediante el botón actualizaratrás o navegando por el historial del navegador.

Si el formulario se envía por AJAX, en lugar de redirigir se suele redibujar un snippet con el formulario renderizado de nuevo.

Pruebe a añadir otros elementos de formulario.

Acceso a los elementos

El formulario es un componente del presenter, en nuestro caso llamado registrationForm (por el nombre del método fábrica createComponentRegistrationForm), así que en cualquier lugar del presenter puede acceder al formulario con:

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

Los distintos elementos del formulario también son componentes, así que puede acceder a ellos de la misma manera:

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

Los elementos se eliminan con unset:

unset($form['name']);

Reglas de validación

Hemos mencionado la palabra válido, pero el formulario todavía no tiene ninguna regla de validación. Arreglémoslo.

El nombre será obligatorio, así que lo marcamos con el método setRequired(). Su argumento es el texto del mensaje de error que se muestra si el usuario no rellena el nombre. Si se omite el argumento, se usa el mensaje de error predeterminado.

$form->addText('name', 'Name:')
	->setRequired('Please enter your name.');

Pruebe a enviar el formulario sin rellenar el nombre y verá que se muestra un mensaje de error, y el navegador o el servidor lo rechazarán hasta que rellene el campo.

Al mismo tiempo, no puede engañar al sistema escribiendo en el campo, por ejemplo, solo espacios. De ninguna manera. Nette recorta automáticamente los espacios del principio y del final. Pruébelo. Es algo que debería hacer siempre en todos los campos de una línea, pero que a menudo se olvida. Nette lo hace automáticamente. (Puede intentar engañar al formulario y enviar como nombre una cadena de varias líneas. Tampoco así se dejará engañar Nette, y los saltos de línea se convertirán en espacios.)

El formulario se valida siempre en el lado del servidor, pero además se genera la validación en JavaScript, que se ejecuta al instante y el usuario se entera del error de inmediato, sin necesidad de enviar el formulario al servidor. De eso se ocupa el script netteForms.js. Inclúyalo en su plantilla de layout:

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

Si mira el código fuente de la página con el formulario, quizá se dé cuenta de que Nette envuelve los elementos obligatorios en elementos con la clase CSS required. Pruebe a añadir la siguiente hoja de estilos a su plantilla y la etiqueta ‘Name’ se pondrá roja. Así se resaltan con elegancia los campos obligatorios para los usuarios:

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

Las demás reglas de validación las añadimos con el método addRule(). El primer parámetro es la regla, el segundo es de nuevo el texto del mensaje de error y puede seguirle un argumento de la regla de validación. ¿Qué significa eso?

Ampliemos el formulario con un nuevo campo opcional ‘age’, que debe ser un número entero (addInteger()) y además estar dentro de un rango permitido ($form::Range). Aquí usaremos el tercer parámetro del método addRule() para pasarle al validador el rango requerido como par [min, max]:

$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'Age must be between 18 and 120.', [18, 120]);

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

Esto abre espacio para una pequeña refactorización. En el mensaje de error y en el tercer parámetro, los números están duplicados, lo que no es ideal. Si estuviéramos creando formularios multilingües y el mensaje con los números se tradujera a varios idiomas, cambiar los valores sería complicado. Por eso se pueden usar los marcadores %d y Nette sustituirá los valores:

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

Volvamos al elemento password, hagámoslo también obligatorio y verifiquemos además la longitud mínima de la contraseña ($form::MinLength), usando de nuevo un marcador en el mensaje:

$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password must be at least %d characters long.', 8);

Añadamos al formulario otro campo passwordVerify, donde el usuario introduce la contraseña otra vez para confirmarla. Con las reglas de validación comprobamos que ambas contraseñas sean iguales ($form::Equal). Como argumento indicamos una referencia a la primera contraseña usando corchetes:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Passwords do not match.', $form['password'])
	->setOmitted();

Con setOmitted() hemos marcado un elemento cuyo valor no nos interesa realmente y que existe solo a efectos de validación. Su valor no se pasa a $data.

Con esto tenemos un formulario plenamente funcional con validación tanto en PHP como en JavaScript. Las capacidades de validación de Nette son mucho más amplias; se pueden crear condiciones, mostrar u ocultar partes de la página en función de ellas, etc. Lo aprenderá todo en el capítulo sobre la validación de formularios.

Valores predeterminados

Habitualmente establecemos valores predeterminados en los elementos del formulario:

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

A menudo resulta útil establecer los valores predeterminados de todos los elementos a la vez. Por ejemplo, cuando el formulario sirve para editar registros. Leemos el registro de la base de datos y establecemos los valores predeterminados:

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

Llame a setDefaults() después de definir los elementos.

En un formulario ya enviado, setDefaults() no tiene efecto: no sobrescribirá lo que el usuario rellenó, así que es seguro llamarlo sin condiciones en la fábrica del formulario. Si necesita forzar los valores incluso después del envío, use setValues() en su lugar.

Renderizar el formulario

De forma predeterminada, el formulario se renderiza como una tabla. Los distintos elementos respetan las reglas básicas de accesibilidad web: todas las etiquetas se escriben como elementos <label> y se asocian con sus respectivos elementos de formulario. Al pulsar la etiqueta, el cursor se sitúa automáticamente en el campo del formulario.

A cada elemento le podemos poner atributos HTML arbitrarios. Por ejemplo, añadir un placeholder:

$form->addInteger('age', 'Age:')
	->setHtmlAttribute('placeholder', 'Please fill in the age');

Hay de verdad muchas maneras de renderizar un formulario, así que se le dedica un capítulo aparte sobre el renderizado.

Mapeo a clases

Volvamos al método formSucceeded(), que recibe los datos enviados en el segundo parámetro $data como objeto ArrayHash (o stdClass). Como es una clase genérica, parecida a stdClass, al trabajar con ella nos faltan ciertas comodidades, como el autocompletado de las propiedades en los editores o el análisis estático del código. Eso se podría resolver teniendo una clase concreta para cada formulario, cuyas propiedades representen los distintos elementos. P. ej.:

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

Alternativamente puede usar un 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 mapearán automáticamente.

¿Cómo le decimos a Nette que devuelva los datos como objetos de esa clase? Es más fácil de lo que parece. Basta con indicar la clase como tipo del parámetro $data en el método manejador:

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

Como tipo también puede indicar array, y entonces los datos se pasarán como array.

De forma parecida puede usar el método getValues(), pasándole como parámetro el nombre de la clase o un objeto que hidratar:

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

Si necesita leer los valores antes de que el formulario se valide, normalmente dentro de un manejador onValidate, use en su lugar el método getUntrustedValues(). Acepta los mismos parámetros que getValues(), pero devuelve los valores enviados sin garantizar que hayan pasado la validación.

Si los formularios tienen una estructura de varios niveles compuesta de contenedores, cree una clase separada para cada uno:

$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 deduce entonces, por el tipo de la propiedad $person, que debe mapear el contenedor a la clase PersonFormData. Si la propiedad tuviera que contener un array de contenedores, indique el tipo array y pase la clase que hay que mapear directamente al contenedor:

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

Puede generar una propuesta de la clase de datos del formulario con el método Nette\Forms\Blueprint::dataClass($form), que la imprime en la página del navegador. Después basta con seleccionar y copiar el código a su proyecto.

Varios botones de envío

Si el formulario tiene más de un botón, normalmente necesitamos distinguir cuál se pulsó. Podemos crear una función manejadora separada para cada botón. Establézcala como manejador del evento onClick:

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

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

El manejador también se le puede pasar al botón directamente como tercer argumento del método addSubmit().

Estos manejadores se llaman solo si el formulario está válidamente rellenado (a no ser que la validación esté desactivada para el botón), igual que el evento onSuccess. La diferencia está en que el primer parámetro que se pasa puede ser el objeto del botón de envío en lugar del formulario, según el type hint que indique:

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

Cuando el formulario se envía pulsando la tecla Enter, se trata como si se hubiera enviado con el primer botón de envío.

Evento onAnchor

Cuando construye un formulario en un método fábrica (como createComponentRegistrationForm), este todavía no sabe si se ha enviado ni con qué datos. Hay casos, sin embargo, en los que necesitamos conocer los valores enviados: quizá el aspecto del formulario dependa de ellos, o hagan falta para select boxes dependientes, etc.

Por eso puede hacer que el código que construye el formulario se llame solo cuando este esté “anclado”, es decir, cuando ya esté conectado al presenter y conozca sus datos enviados. Coloque ese 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 se llamará cuando el formulario conozca los datos con los que se envió
	// así que puede usar el método getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Protección frente a vulnerabilidades

Nette Framework pone un gran énfasis en la seguridad y por eso vela meticulosamente por la seguridad de los formularios. Lo hace de forma completamente transparente y no requiere ninguna configuración manual.

Además de proteger los formularios frente a ataques como el Cross-Site Scripting (XSS) y el Cross-Site Request Forgery (CSRF), realiza muchas pequeñas medidas de seguridad en las que ya no tiene que pensar.

Por ejemplo, filtra de las entradas todos los caracteres de control y comprueba la validez de la codificación UTF-8, lo que garantiza que los datos del formulario estén siempre limpios. En los select boxes y las listas de radio verifica que los elementos seleccionados estaban realmente entre los ofrecidos y que no hubo ninguna falsificación. Ya hemos mencionado que en los campos de texto de una línea sustituye por espacios los caracteres de fin de línea que un atacante pudiera enviar. En los campos de varias líneas normaliza los caracteres de fin de línea. Etcétera.

Nette se ocupa por usted de riesgos de seguridad cuya existencia muchos programadores ni siquiera conocen.

El mencionado ataque CSRF consiste en que un atacante atrae a la víctima a una página que, en silencio, ejecuta en el navegador de la víctima una petición al servidor en el que esta tiene la sesión iniciada. El servidor cree entonces que la petición la hizo la víctima de buen grado. Por eso Nette rechaza los formularios POST enviados desde un origen ajeno; incluso otro subdominio del mismo sitio cuenta como ajeno. Si necesita permitir el envío desde otro origen, desactive la protección con:

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

Eso desactiva la protección para cualquier origen. Para permitir solo orígenes concretos, desactive la protección y verifique usted mismo la cabecera Origin contra su propia lista blanca.

La protección se apoya en la cabecera Sec-Fetch-Site del navegador (Fetch Metadata), que el navegador envía automáticamente y que no se puede falsificar ni siquiera con una vulnerabilidad XSS. Para los navegadores antiguos que no las soportan se aplica una cookie SameSite de reserva, que una aplicación Nette establece automáticamente. El artículo The browser finally solves CSRF lo describe en detalle.

La protección anterior, con un token de autorización guardado en la sesión y activada con $form->addProtection(), ya no hace falta y está obsoleta desde la versión 3.3.

Usar un mismo formulario en varios presenters

Si necesita usar el mismo formulario en varios presenters, recomendamos crearle una fábrica que después inyecta en los presenters. Un lugar adecuado para esa clase es, por ejemplo, el directorio app/Forms.

La clase fábrica podría tener este 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 la clase que produce el formulario en el método fábrica del componente dentro del presenter:

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

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

El manejador del procesamiento del formulario lo puede proporcionar también la propia 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 {
			// aquí procesamos nuestro formulario enviado
		};
		return $form;
	}
}

Con esto hemos cubierto una introducción rápida a los formularios en Nette. Pruebe a mirar en el directorio de ejemplos de la distribución para inspirarse más.

versión: 4.x