/
in-presenter.texy
433 lines (307 loc) · 21.4 KB
/
in-presenter.texy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
Formularios en Presentadores
****************************
.[perex]
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 |standalone].
Primer formulario .[#toc-first-form]
====================================
Intentaremos escribir un sencillo formulario de registro. Su código será el siguiente:
```php
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í:
[* form-en.webp *]
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 |application:components#Factory Methods]. Tendrá el siguiente aspecto:
```php .{file:app/UI/Home/HomePresenter.php}
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}`:
```latte .{file:app/UI/Home/default.latte}
<h1>Registro</h1>
{control registrationForm}
```
Y eso es todo :-) Tenemos un formulario funcional y perfectamente [seguro |#Vulnerability Protection].
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 |application:components#Hollywood style]. 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 |nette:glossary#Events]. 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 |application:presenters#Life Cycle of Presenter] 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 |utils:arrays#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 |#Mapping to Classes].
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:
```php
$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 |controls].
Acceso a los controles .[#toc-access-to-controls]
=================================================
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:
```php
$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:
```php
$input = $form->getComponent('name'); // or $input = $form['name'];
$button = $form->getComponent('send'); // or $button = $form['send'];
```
Los controles se eliminan utilizando unset:
```php
unset($form['name']);
```
Reglas de validación .[#toc-validation-rules]
=============================================
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.
```php
$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:
```latte
<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></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:
```latte
<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:
```php
$form->addInteger('age', 'Edad:')
->addRule($form::Range, 'Debes ser mayor de 18 años y tener menos de 120.', [18, 120]);
```
.[tip]
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 |rendering#translating] 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`:
```php
->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:
```php
$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 |#Access to Controls]:
```php
$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 |validation].
Valores por defecto .[#toc-default-values]
==========================================
A menudo establecemos valores por defecto para los controles de formulario:
```php
$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:
```php
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
```
Llama a `setDefaults()` después de definir los controles.
Representación del formulario .[#toc-rendering-the-form]
========================================================
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:
```php
$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 |rendering].
Mapeo a Clases .[#toc-mapping-to-classes]
=========================================
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:
```php
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:
```php
class RegistrationFormData
{
public function __construct(
public string $name,
public int $age,
public string $password,
) {
}
}
```
¿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:
```php
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:
```php
$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:
```php
$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:
```php
$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. .{data-version:3.1.15}
Botones de envío múltiples .[#toc-multiple-submit-buttons]
==========================================================
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 |nette:glossary#Events] `onClick`:
```php
$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:
```php
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
```
Cuando un formulario se envía con la tecla <kbd>Intro</kbd>, se trata como si se hubiera enviado con el primer botón.
Evento onAnchor .[#toc-event-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`:
```php
$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 .[#toc-vulnerability-protection]
===================================================================
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) |nette:glossary#cross-site-scripting-xss] y [Cross-Site Request Forgery (CSRF) |nette:glossary#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:
```php
$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:
```php
$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 .[#toc-using-one-form-in-multiple-presenters]
==========================================================================================
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:
```php
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:
```php
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:
```php
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 |https://github.com/nette/forms/tree/master/examples] en la distribución para más inspiración.