Formularios en presenters

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

Si le interesa cómo usarlos de forma completamente independiente sin el resto del framework, la guía para uso independiente es para usted.

Primer formulario

Intentemos escribir un formulario de registro simple. 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', 'Registrar');
$form->onSuccess[] = [$this, 'formSucceeded'];

y en el navegador se mostrará así:

El formulario en el presenter es un objeto de la clase Nette\Application\UI\Form, su predecesor Nette\Forms\Form está destinado a un uso independiente. Le hemos agregado los llamados elementos nombre, contraseña y botón de envío. Y finalmente, la línea con $form->onSuccess dice que después del envío y la validación exitosa, se debe llamar al método $this->formSucceeded().

Desde el punto de vista del presenter, el formulario es un componente común. Por lo tanto, se trata como un componente y lo incorporamos al presenter mediante un método de fábrica. Se verá 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', 'Nombre:');
		$form->addPassword('password', 'Contraseña:');
		$form->addSubmit('send', 'Registrar');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// aquí procesamos los datos enviados por el formulario
		// $data->name contiene el nombre
		// $data->password contiene la contraseña
		$this->flashMessage('Ha sido registrado exitosamente.');
		$this->redirect('Home:');
	}
}

Y en la plantilla renderizamos el formulario con la etiqueta {control}:

<h1>Registro</h1>

{control registrationForm}

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

Y ahora probablemente esté pensando que fue demasiado rápido, se pregunta cómo es posible que se llame al método formSucceeded() y cuáles son los parámetros que recibe. Ciertamente, tiene razón, esto merece una explicación.

Nette presenta un mecanismo fresco que llamamos Hollywood style. En lugar de que usted, como desarrollador, tenga que preguntar constantemente si algo sucedió (“¿se envió el formulario?”, “¿se envió válidamente?” y “¿no fue falsificado?”), le dice al framework “cuando el formulario esté válidamente completado, llama a este método” y deja el trabajo adicional en sus manos. Si programa en JavaScript, este estilo de programación le resultará familiar. Escribe funciones que se llaman cuando ocurre un cierto evento. Y el lenguaje les pasa los argumentos apropiados.

Así es exactamente como está construido el código del presenter anterior. 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 se completa correctamente (es decir, es válido). Dentro del ciclo de vida del presenter, esto es una llamada señal, por lo que se llaman después del método action* y antes del método render*. Y a cada callback le pasa como primer parámetro el propio formulario y como segundo los datos enviados en forma de objeto ArrayHash. Puede omitir el primer parámetro si no necesita el objeto del formulario. Y el segundo parámetro puede ser más inteligente, pero hablaremos de eso más adelante.

El objeto $data contiene las claves name y password con los datos que el usuario completó. Normalmente, enviamos los datos directamente para su posterior procesamiento, que puede ser, por ejemplo, la inserción en una base de datos. Sin embargo, durante el procesamiento puede ocurrir un error, por ejemplo, el nombre de usuario ya está ocupado. En tal caso, devolvemos el error al formulario usando addError() y dejamos que se renderice de nuevo, con el mensaje de error.

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

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

Después de procesar el formulario, redirigimos a la siguiente página. Esto evita el reenvío no deseado del formulario con el botón actualizar, atrás o moviéndose en el historial del navegador.

Intente agregar también otros elementos de formulario.

Acceso a los elementos

El formulario es un componente del presenter, en nuestro caso llamado registrationForm (según el nombre del método de fábrica createComponentRegistrationForm), por lo que en cualquier lugar del presenter puede acceder al formulario mediante:

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

Los elementos individuales del formulario también son componentes, por lo 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 usando unset:

unset($form['name']);

Reglas de validación

Se mencionó la palabra válido, pero el formulario aún no tiene reglas de validación. Vamos a solucionarlo.

El nombre será obligatorio, por lo que lo marcamos con el método setRequired(), cuyo argumento es el texto del mensaje de error que se mostrará si el usuario no completa el nombre. Si no se proporciona el argumento, se utilizará el mensaje de error predeterminado.

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

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

Al mismo tiempo, el sistema no se deja engañar si escribe, por ejemplo, solo espacios en el campo. De ninguna manera. Nette elimina automáticamente los espacios iniciales y finales. Pruébelo. Es algo que siempre debería hacer con cada input de una sola línea, pero a menudo se olvida. Nette lo hace automáticamente. (Puede intentar engañar al formulario y enviar una cadena multilínea como nombre. Incluso aquí, Nette no se dejará engañar y cambiará los saltos de línea por espacios.)

El formulario siempre se valida en el lado del servidor, pero también se genera una validación JavaScript, que se ejecuta instantáneamente y el usuario se entera del error de inmediato, sin necesidad de enviar el formulario al servidor. Esto lo maneja el script netteForms.js. Insértelo en la 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, puede notar que Nette inserta los elementos obligatorios en elementos con la clase CSS required. Intente agregar la siguiente hoja de estilos a la plantilla y la etiqueta “Nombre” será roja. De esta manera, marcamos elegantemente los elementos obligatorios para los usuarios:

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

Agregamos otras reglas de validación con el método addRule(). El primer parámetro es la regla, el segundo es nuevamente el texto del mensaje de error y puede seguir un argumento de la regla de validación. ¿Qué significa esto?

Ampliaremos el formulario con un nuevo campo opcional “edad”, que debe ser un número entero (addInteger()) y además estar en un rango permitido ($form::Range). Y aquí es donde usaremos el tercer parámetro del método addRule(), con el que pasaremos al validador el rango requerido como un par [desde, hasta]:

$form->addInteger('age', 'Edad:')
	->addRule($form::Range, 'La edad debe estar entre 18 y 120', [18, 120]);

Si el usuario no completa el campo, las reglas de validación no se verificarán, ya que el elemento es opcional.

Aquí surge espacio para una pequeña refactorización. En el mensaje de error y en el tercer parámetro, los números se indican de forma duplicada, lo cual no es ideal. Si estuviéramos creando formularios multilingües y el mensaje que contiene números se tradujera a varios idiomas, dificultaría un posible cambio de valores. Por esta razón, es posible usar los placeholders %d y Nette completará los valores:

	->addRule($form::Range, 'La edad debe estar entre %d y %d años', [18, 120]);

Volvamos al elemento password, que también haremos obligatorio y además verificaremos la longitud mínima de la contraseña ($form::MinLength), nuevamente usando el placeholder:

$form->addPassword('password', 'Contraseña:')
	->setRequired('Elija una contraseña')
	->addRule($form::MinLength, 'La contraseña debe tener al menos %d caracteres', 8);

Agregaremos al formulario otro campo passwordVerify, donde el usuario ingresará la contraseña nuevamente, para verificar. Usando reglas de validación, verificaremos si ambas contraseñas son iguales ($form::Equal). Y como parámetro, daremos una referencia a la primera contraseña usando corchetes:

$form->addPassword('passwordVerify', 'Contraseña para verificar:')
	->setRequired('Por favor, introduzca la contraseña de nuevo para verificar')
	->addRule($form::Equal, 'Las contraseñas no coinciden', $form['password'])
	->setOmitted();

Con setOmitted(), hemos marcado un elemento cuyo valor en realidad no nos importa y que existe solo con fines de validación. El valor no se pasará a $data.

Con esto, tenemos un formulario completamente funcional con validación en PHP y JavaScript. Las capacidades de validación de Nette son mucho más amplias, se pueden crear condiciones, hacer que partes de la página se muestren y oculten según ellas, etc. Todo lo aprenderá en el capítulo sobre validación de formularios.

Valores predeterminados

Normalmente establecemos valores predeterminados para los elementos del formulario:

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

A menudo es útil establecer valores predeterminados para todos los elementos a la vez. Por ejemplo, cuando el formulario se usa 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.

Renderizado del formulario

Por defecto, el formulario se renderiza como una tabla. Los elementos individuales cumplen la regla básica de accesibilidad: todas las etiquetas se escriben como <label> y están vinculadas al elemento de formulario correspondiente. Al hacer clic en la etiqueta, el cursor aparece automáticamente en el campo del formulario.

Podemos establecer cualquier atributo HTML para cada elemento. Por ejemplo, agregar un placeholder:

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

Hay realmente muchas formas de renderizar un formulario, por lo que hay un capítulo separado dedicado al renderizado.

Mapeo a clases

Volvamos al método formSucceeded(), que en el segundo parámetro $data recibe los datos enviados como un objeto ArrayHash. Dado que es una clase genérica, algo así como stdClass, nos faltará cierta comodidad al trabajar con ella, como el autocompletado de propiedades en los editores o el análisis estático de código. Esto podría resolverse teniendo una clase específica para cada formulario, cuyas propiedades representen los elementos individuales. Por ejemplo:

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

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

¿Cómo decirle a Nette que nos devuelva los datos como objetos de esta clase? Más fácil de lo que piensa. Simplemente especifique la clase como el 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 se puede especificar array y luego los datos se pasarán como un array.

De manera similar, también se puede usar la función getValues(), a la que pasamos el nombre de la clase o el objeto a hidratar como parámetro:

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

Si los formularios forman una estructura multinivel compuesta por 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 entonces, a partir del tipo de la propiedad $person, sabe que debe mapear el contenedor a la clase PersonFormData. Si la propiedad contuviera un array de contenedores, especifique el tipo array y pase la clase para el mapeo directamente al contenedor:

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

Puede hacer que el diseño de la clase de datos del formulario se genere usando el método Nette\Forms\Blueprint::dataClass($form), que lo imprimirá en la página del navegador. Luego, simplemente haga clic para seleccionar el código y cópielo en su proyecto.

Múltiples botones

Si el formulario tiene más de un botón, generalmente necesitamos distinguir cuál de ellos fue presionado. Podemos crear nuestra propia función de manejo para cada botón. La establecemos como handler para el evento onClick:

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

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

Estos handlers se llaman solo en caso de un formulario válidamente completado, al igual que en el caso del evento onSuccess. La diferencia es que como primer parámetro, en lugar del formulario, se puede pasar el botón de envío, dependiendo del tipo que especifique:

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

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

Evento onAnchor

Cuando construimos el formulario en el método de fábrica (como createComponentRegistrationForm), este aún no sabe si fue enviado, ni con qué datos. Pero hay casos en los que necesitamos conocer los valores enviados, por ejemplo, si la forma posterior del formulario depende de ellos, o si los necesitamos para select boxes dependientes, etc.

Por lo tanto, puede dejar que parte del código que construye el formulario se llame solo en el momento en que está, por así decirlo, anclado, es decir, ya está conectado al presenter y conoce sus datos enviados. Pasamos dicho código al array $onAnchor:

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

$form->onAnchor[] = function () use ($country, $city) {
	// esta función se llamará solo cuando el formulario sepa si fue enviado y con qué datos
	// por lo tanto, se puede usar el método getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Protección contra vulnerabilidades

Nette Framework pone gran énfasis en la seguridad y, por lo tanto, se preocupa meticulosamente por la buena seguridad de los formularios. Lo hace de forma totalmente transparente y no requiere configurar nada manualmente.

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

Por ejemplo, filtra todos los caracteres de control de las entradas y verifica la validez de la codificación UTF-8, por lo que los datos del formulario siempre estarán limpios. En los select boxes y radio lists, verifica que los elementos seleccionados fueran realmente de los ofrecidos y que no hubo falsificación. Ya mencionamos que en las entradas de texto de una sola línea, elimina los caracteres de fin de línea que un atacante podría haber enviado. En las entradas multilínea, normaliza los caracteres de fin de línea. Y así sucesivamente.

Nette resuelve por usted los riesgos de seguridad que muchos programadores ni siquiera saben que existen.

El ataque CSRF mencionado consiste en que un atacante atrae a la víctima a una página que ejecuta discretamente una petición en el navegador de la víctima al servidor en el que la víctima ha iniciado sesión, y el servidor cree que la petición fue realizada por la víctima por su propia voluntad. Por lo tanto, Nette evita el envío de formularios POST desde otro dominio. Si por alguna razón desea desactivar la protección y permitir el envío de formularios desde otro dominio, use:

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

Esta protección utiliza una cookie SameSite llamada _nss. La protección mediante cookie SameSite puede no ser 100% confiable, por lo que es recomendable activar también la protección mediante token:

$form->addProtection();

Recomendamos proteger de esta manera los formularios en la parte administrativa del sitio web que modifican datos sensibles en la aplicación. El framework se defiende contra el ataque CSRF generando y verificando un token de autorización que se almacena en la sesión. Por lo tanto, es necesario tener una sesión abierta antes de mostrar el formulario. En la parte administrativa del sitio web, la sesión generalmente ya está iniciada debido al inicio de sesión del usuario. De lo contrario, inicie la sesión con el método Nette\Http\Session::start().

Mismo formulario en múltiples presenters

Si necesita usar un formulario en múltiples presenters, recomendamos crear una fábrica para él, que luego pasará al presenter. Una ubicación adecuada para tal clase es, por ejemplo, el directorio app/Forms.

La clase de fábrica podría verse así:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nombre:');
		$form->addSubmit('send', 'Iniciar sesión');
		return $form;
	}
}

Solicitamos a la clase que fabrique el formulario en el método de fábrica de componentes en el presenter:

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

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// podemos modificar el formulario, aquí por ejemplo cambiamos la etiqueta del botón
	$form['send']->setCaption('Continuar');
	$form->onSuccess[] = [$this, 'signInFormSucceeded']; // y agregamos el handler
	return $form;
}

El handler para procesar el formulario también puede ser proporcionado desde la fábrica:

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nombre:');
		$form->addSubmit('send', 'Iniciar sesión');
		$form->onSuccess[] = function (Form $form, $data): void {
			// aquí realizamos el procesamiento del formulario
		};
		return $form;
	}
}

Bien, hemos tenido una rápida introducción a los formularios en Nette. Intente mirar también en el directorio examples en la distribución, donde encontrará más inspiración.

versión: 4.0