Formularios utilizados de forma autónoma

Los Formularios Nette facilitan enormemente la creación y el procesamiento de formularios web. Puedes utilizarlos en tus aplicaciones completamente solos sin el resto del framework, lo que demostraremos en este capítulo.

Sin embargo, si usas Nette Application y presentadores, hay una guía para ti: formularios en presentadores.

Primer formulario

Intentaremos escribir un sencillo formulario de registro. Su código tendrá este aspecto (código completo):

use Nette\Forms\Form;

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

Y vamos a renderizarlo:

$form->render();

y el resultado debería verse así:

El formulario es un objeto de la clase Nette\Forms\Form (la clase Nette\Application\UI\Form se utiliza en los presentadores). Le añadimos los controles nombre, contraseña y botón de envío.

Ahora reactivaremos el formulario. Preguntando a $form->isSuccess(), averiguaremos si el formulario fue enviado y si fue rellenado válidamente. En caso afirmativo, volcaremos los datos. Tras la definición del formulario añadiremos:

if ($form->isSuccess()) {
	echo 'El formulario se ha rellenado y enviado correctamente';
	$data = $form->getValues();
	// $data->name contains name
	// $data->password contains password
	var_dump($data);
}

El método getValues() devuelve los datos enviados en forma de objeto ArrayHash. Más adelante mostraremos cómo modificar esto. La variable $data contiene las claves 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.');

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

Por defecto, el formulario se envía usando el método POST a la misma página. Ambos pueden cambiarse:

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

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

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

Acceso a los controles

El formulario y sus controles individuales se denominan componentes. Crean un árbol de componentes, cuya raíz es el formulario. Puede acceder a los controles individuales de la siguiente manera:

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

$button = $form->getComponent('send');
// sintaxis alternativa: $button = $form['enviar'];

Los controles se eliminan utilizando unset:

unset($form['name']);

Reglas de validación

Aquí se usó la palabra valid, pero el formulario aún no tiene reglas de validación. Arreglémoslo.

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 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. Añádelo a la página:

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

Si miras en el código fuente de la página con formulario, puedes notar que Nette inserta los campos requeridos 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 usando 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 maneras de renderizar un formulario, así que es un capítulo dedicado al renderizado.

Mapeo a Clases

Volvamos al procesamiento de los datos del formulario. El método getValues() devuelve 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 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;
}

A partir de PHP 8.0, puedes usar esta elegante notación que usa un constructor:

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

¿Cómo decirle a Nette que nos devuelva datos como objetos de esta clase? Más fácil de lo que piensas. Todo lo que tienes que hacer es especificar el nombre de la clase u objeto a hidratar como parámetro:

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

También se puede especificar un 'array' como parámetro, y entonces los datos vuelven como un array.

Si los formularios consisten en una estructura de varios niveles 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, normalmente necesitamos distinguir cuál ha sido pulsado. El método isSubmittedBy() del botón nos devuelve esta información:

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

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

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

No omitas el $form->isSuccess() para verificar la validez de los datos.

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

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.

Además de proteger los formularios contra ataques de 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 SameSite llamada _nss. Por lo tanto, cree un formulario antes de enviar la primera salida para que la cookie pueda ser enviada.

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();

Es muy recomendable 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().

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

versión: 4.0