Formularios independientes

Nette Forms simplifica enormemente la creación y el procesamiento de formularios web. Puede usarlos en sus aplicaciones de forma completamente independiente, sin el resto del framework, como se muestra en este capítulo.

Si usa Nette Application y presenters, en cambio, tiene una guía dedicada: formularios en presenters.

Primer formulario

Antes de empezar, instale el paquete con Composer:

composer require nette/forms

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

use Nette\Forms\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');

Y rendericémoslo de forma muy sencilla:

$form->render();

El resultado en el navegador debería tener este aspecto:

El formulario es un objeto de la clase Nette\Forms\Form (en los presenters se usa la clase Nette\Application\UI\Form). Le hemos añadido elementos llamados ‘name’ y ‘password’ y un botón de envío.

Ahora demos vida al formulario. Consultando $form->isSuccess() averiguamos si el formulario se envió y si se rellenó de forma válida. Si es así, mostraremos los datos. Después de la definición del formulario añada:

if ($form->isSuccess()) {
	echo 'Form was filled and submitted successfully';
	$data = $form->getValues();
	// $data->name contiene el nombre
	// $data->password contiene la contraseña
	var_dump($data);
}

El método getValues() devuelve los datos enviados como objeto ArrayHash. Mostraremos cómo cambiarlo más adelante. El objeto $data contiene las claves name y password con los datos introducidos por el usuario.

Normalmente enviamos los datos directamente a su procesamiento posterior, por ejemplo a insertarlos en la base de datos. Durante el procesamiento puede producirse un error, sin embargo, por ejemplo si 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, this username is already taken.');

Tras procesar el formulario redirigimos a la página siguiente. Eso evita que el formulario se reenvíe involuntariamente al pulsar los botones actualizaratrás, o al navegar por el historial del navegador.

De forma predeterminada, el formulario se envía por el método POST a la misma página. Ambas cosas se pueden cambiar:

$form->setAction('/submit.php');
$form->setMethod('GET');

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

Pruebe a añadir también otros elementos de formulario.

Acceso a los elementos

El formulario y sus distintos elementos se llaman componentes. Forman un árbol de componentes cuya raíz es el formulario. A los distintos elementos del formulario se accede así:

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

$button = $form->getComponent('send');
// sintaxis alternativa: $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 no se indica ningún argumento, se usa el mensaje de error predeterminado.

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

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

Al mismo tiempo, no puede engañar al sistema escribiendo solo espacios en el campo. 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 enviando 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. Esta se ejecuta al instante y el usuario se entera de los errores de inmediato, sin necesidad de enviar el formulario al servidor. De eso se ocupa el script netteForms.js. Insértelo en la página:

<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 inserta los elementos obligatorios en elementos con la clase CSS required. Pruebe a añadir la siguiente hoja de estilos a la plantilla y la etiqueta “Name” se pondrá roja. Así se resaltan con elegancia los elementos 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 opcional 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 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. Los números están duplicados en el mensaje de error y en el tercer parámetro, 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 rellenará 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('Choose a password')
	->addRule($form::MinLength, '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 verificarla. Con las reglas de validación comprobamos que ambas contraseñas sean iguales ($form::Equal). Como parámetro indicamos una referencia a la primera contraseña usando corchetes:

$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Please enter the password again for verification')
	->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; puede crear condiciones, mostrar y 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

A menudo 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 sus valores como 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 pautas básicas de accesibilidad: todas las etiquetas se generan como elementos <label> y se asocian con sus respectivos elementos de formulario. Al pulsar una etiqueta, el cursor se coloca 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 muchas maneras de renderizar un formulario, así que al renderizado se le dedica un capítulo aparte.

Renderizado con Latte

Si tiene a mano el sistema de plantillas Latte, puede dejar que renderice el formulario y ganar control total sobre el HTML resultante. Crea el motor, registra la extensión de formularios y pasa el formulario a la plantilla como variable:

$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);

$latte->render('form.latte', ['form' => $form]);

En la plantilla trabaja después con el formulario mediante la variable $form y etiquetas como {input}, {label} o n:name. Un ejemplo completo, incluida la plantilla, lo encontrará en el directorio de ejemplos (los archivos latte.php y latte/). Las distintas etiquetas se describen en el capítulo sobre el renderizado.

Mapeo a clases

Volvamos al procesamiento de los datos del formulario. El método getValues() devolvía los datos enviados como objeto ArrayHash. 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 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 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 como parámetro el nombre de la clase o el objeto que hay que hidratar:

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

Como parámetro también puede indicar 'array' y los datos se devolverán como array.

Si los formularios constan de 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 sabe 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 hacer que se le genere una propuesta de la clase de datos del formulario con el método Nette\Forms\Blueprint::dataClass($form), que la imprimirá 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ó. Esa información la devuelve el método isSubmittedBy() del botón:

$form->addSubmit('save', 'Save');
$form->addSubmit('delete', 'Delete');

if ($form->isSuccess()) {
	if ($form['save']->isSubmittedBy()) {
		// ...
	}

	if ($form['delete']->isSubmittedBy()) {
		// ...
	}
}

No omita la comprobación $form->isSuccess(); verifica la validez de los datos.

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

Protección frente a vulnerabilidades

Nette Framework pone un fuerte énfasis en la seguridad y por eso vela meticulosamente por la seguridad correcta de los formularios.

Además de proteger los formularios frente a vulnerabilidades conocidas 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 estarán siempre limpios. En los select boxes y las listas de radio verifica que los elementos seleccionados estaban realmente entre las opciones ofrecidas 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 que la petición la hizo la víctima voluntariamente. 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. Los navegadores antiguos que no envían estas cabeceras no pasarán la comprobación. 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.

Con esto hemos hecho 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