Nette Documentation Preview

syntax
Formulaires dans Presenters
***************************

.[perex]
Les formulaires Nette facilitent considérablement la création et le traitement des formulaires Web. Dans ce chapitre, vous allez apprendre à utiliser les formulaires dans les Presenters.

Si vous souhaitez les utiliser de manière complètement autonome sans le reste du framework, il existe un guide pour les [formulaires autonomes |standalone].


Premier formulaire .[#toc-first-form]
=====================================

Nous allons essayer d'écrire un formulaire d'inscription simple. Son code ressemblera à ceci :

```php
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'];
```

et dans le navigateur, le résultat devrait ressembler à ceci :

[* form-en.webp *]

Le formulaire dans le présentateur est un objet de la classe `Nette\Application\UI\Form`, son prédécesseur `Nette\Forms\Form` est destiné à une utilisation autonome. Nous lui avons ajouté les champs nom, mot de passe et bouton d'envoi. Enfin, la ligne avec `$form->onSuccess` indique qu'après soumission et validation réussie, la méthode `$this->formSucceeded()` doit être appelée.

Du point de vue du présentateur, le formulaire est un composant commun. Par conséquent, il est traité comme un composant et incorporé dans le présentateur à l'aide de la [méthode factory |application:components#Factory Methods]. Cela ressemblera à ceci :

```php .{file:app/Presenters/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', 'Name:');
		$form->addPassword('password', 'Password:');
		$form->addSubmit('send', 'Sign up');
		$form->onSuccess[] = [$this, 'formSucceeded'];
		return $form;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// ici nous allons traiter les données envoyées par le formulaire
		// $data->name contient le nom
		// $data->password contient password
		$this->flashMessage('Vous vous êtes inscrit avec succès.');
		$this->redirect('Home:');
	}
}
```

Et le rendu dans le modèle est effectué à l'aide de la balise `{control}`:

```latte .{file:app/Presenters/templates/Home/default.latte}
<h1>Registration</h1>

{control registrationForm}
```

Et c'est tout :-) Nous avons un formulaire fonctionnel et parfaitement [sécurisé |#Vulnerability Protection].

Maintenant, vous pensez probablement que c'était trop rapide, vous vous demandez comment il est possible que la méthode `formSucceeded()` soit appelée et quels sont les paramètres qu'elle reçoit. Bien sûr, vous avez raison, cela mérite une explication.

Nette propose un mécanisme sympa, que nous appelons le [style Hollywood |application:components#Hollywood style]. Au lieu de devoir constamment demander si quelque chose s'est produit ("le formulaire a-t-il été soumis ?", "a-t-il été soumis de manière valide ?" ou "n'a-t-il pas été forgé ?"), vous dites au framework "lorsque le formulaire est valablement rempli, appelez cette méthode" et laissez la suite du travail. Si vous programmez en JavaScript, vous êtes familier avec ce style de programmation. Vous écrivez des fonctions qui sont appelées lorsqu'un certain [événement |nette:glossary#Events] se produit. Et le langage leur transmet les arguments appropriés.

C'est ainsi que le code du présentateur ci-dessus est construit. Le tableau `$form->onSuccess` représente la liste des callbacks PHP que Nette appellera lorsque le formulaire sera soumis et rempli correctement.
Dans le [cycle de vie du présentateur |application:presenters#Life Cycle of Presenter], il s'agit de ce que l'on appelle un signal. Ils sont donc appelés après la méthode `action*` et avant la méthode `render*`.
Il transmet à chaque callback le formulaire lui-même dans le premier paramètre et les données envoyées sous forme d'objet [ArrayHash |utils:arrays#ArrayHash] dans le second. Vous pouvez omettre le premier paramètre si vous n'avez pas besoin de l'objet formulaire. Le second paramètre peut être encore plus pratique, mais nous y reviendrons [plus tard |#Mapping to Classes].

L'objet `$data` contient les propriétés `name` et `password` avec les données saisies par l'utilisateur. Habituellement, nous envoyons les données directement pour un traitement ultérieur, qui peut être, par exemple, l'insertion dans la base de données. Toutefois, une erreur peut se produire pendant le traitement, par exemple, le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire en utilisant `addError()` et le laissons se redessiner, avec un message d'erreur :

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

En plus de `onSuccess`, il y a aussi `onSubmit`: les callbacks sont toujours appelés après l'envoi du formulaire, même s'il n'est pas rempli correctement. Et enfin `onError`: les callbacks ne sont appelés que si la soumission n'est pas valide. Ils sont même appelés si nous invalidons le formulaire dans `onSuccess` ou `onSubmit` en utilisant `addError()`.

Après avoir traité le formulaire, nous redirigeons vers la page suivante. Cela permet d'éviter que le formulaire soit involontairement resoumis en cliquant sur le bouton *refresh*, *back*, ou en déplaçant l'historique du navigateur.

Essayez d'ajouter d'autres [contrôles de formulaire |controls].


Accès aux contrôles .[#toc-access-to-controls]
==============================================

Le formulaire est un composant du présentateur, nommé dans notre cas `registrationForm` (d'après le nom de la méthode d'usine `createComponentRegistrationForm`), donc n'importe où dans le présentateur vous pouvez accéder au formulaire en utilisant :

```php
$form = $this->getComponent('registrationForm');
// syntaxe alternative: $form = $this['registrationForm'];
```

Les contrôles individuels du formulaire sont également des composants, vous pouvez donc y accéder de la même manière :

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

Les contrôles sont supprimés à l'aide de unset :

```php
unset($form['name']);
```


Règles de validation .[#toc-validation-rules]
=============================================

Le mot *valide* a été utilisé plusieurs fois, mais le formulaire n'a pas encore de règles de validation. Corrigeons cela.

Le nom sera obligatoire, nous le marquerons donc avec la méthode `setRequired()`, dont l'argument est le texte du message d'erreur qui sera affiché si l'utilisateur ne le remplit pas. Si aucun argument n'est donné, le message d'erreur par défaut est utilisé.

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

Essayez de soumettre le formulaire sans que le nom soit rempli et vous verrez qu'un message d'erreur s'affiche et que le navigateur ou le serveur le rejettera jusqu'à ce que vous le remplissiez.

En même temps, vous ne pourrez pas tromper le système en ne tapant que des espaces dans la saisie, par exemple. Pas question. Nette supprime automatiquement les espaces à gauche et à droite. Essayez-le. C'est quelque chose que vous devriez toujours faire avec chaque entrée d'une seule ligne, mais on l'oublie souvent. Nette le fait automatiquement. (Vous pouvez essayer de tromper les formulaires et envoyer une chaîne de caractères multiligne comme nom. Même dans ce cas, Nette ne sera pas dupe et les sauts de ligne se transformeront en espaces).

Le formulaire est toujours validé du côté du serveur, mais la validation JavaScript est également générée, ce qui est rapide et l'utilisateur connaît l'erreur immédiatement, sans avoir à envoyer le formulaire au serveur. Ceci est géré par le script `netteForms.js`.
Insérez-le dans le modèle de mise en page :

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

Si vous regardez dans le code source de la page avec formulaire, vous pouvez remarquer que Nette insère les champs obligatoires dans des éléments avec une classe CSS `required`. Essayez d'ajouter le style suivant au modèle, et l'étiquette "Nom" sera rouge. De manière élégante, nous marquons les champs obligatoires pour les utilisateurs :

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

Des règles de validation supplémentaires seront ajoutées par la méthode `addRule()`. Le premier paramètre est la règle, le second est à nouveau le texte du message d'erreur, et l'argument facultatif de la règle de validation peut suivre. Qu'est-ce que cela signifie ?

Le formulaire recevra une autre entrée facultative *âge* avec la condition qu'il s'agisse d'un nombre (`addInteger()`) et qu'il soit dans certaines limites (`$form::Range`). Et ici, nous utiliserons le troisième argument de `addRule()`, la plage elle-même :

```php
$form->addInteger('age', 'Age:')
	->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);
```

.[tip]
Si l'utilisateur ne remplit pas le champ, les règles de validation ne seront pas vérifiées, car le champ est facultatif.

Il y a évidemment de la place pour un petit remaniement. Dans le message d'erreur et dans le troisième paramètre, les nombres sont listés en double, ce qui n'est pas idéal. Si nous créions un [formulaire multilingue |rendering#translating] et que le message contenant les chiffres devait être traduit en plusieurs langues, il serait plus difficile de modifier les valeurs. C'est pourquoi on peut utiliser les caractères de substitution `%d`:

```php
	->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);
```

Revenons au champ *mot de passe*, rendons-le *obligatoire* et vérifions la longueur minimale du mot de passe (`$form::MinLength`), en utilisant à nouveau les caractères de substitution du message :

```php
$form->addPassword('password', 'Password:')
	->setRequired('Pick a password')
	->addRule($form::MinLength, 'Your password has to be at least %d long', 8);
```

Nous ajouterons un champ `passwordVerify` au formulaire, dans lequel l'utilisateur saisira à nouveau le mot de passe, pour vérification. En utilisant les règles de validation, nous vérifions si les deux mots de passe sont les mêmes (`$form::Equal`). Et comme argument, nous donnons une référence au premier mot de passe en utilisant des [crochets |#Access to Controls]:

```php
$form->addPassword('passwordVerify', 'Password again:')
	->setRequired('Fill your password again to check for typo')
	->addRule($form::Equal, 'Password mismatch', $form['password'])
	->setOmitted();
```

Avec `setOmitted()`, nous marquons un élément dont la valeur ne nous intéresse pas vraiment et qui n'existe que pour la validation. Sa valeur n'est pas transmise à `$data`.

Nous avons un formulaire entièrement fonctionnel avec validation en PHP et JavaScript. Les capacités de validation de Nette sont beaucoup plus larges, vous pouvez créer des conditions, afficher et masquer des parties d'une page en fonction de celles-ci, etc. Vous pouvez tout découvrir dans le chapitre sur la [validation des formulaires |validation].


Valeurs par défaut .[#toc-default-values]
=========================================

Nous définissons souvent des valeurs par défaut pour les contrôles de formulaires :

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

Il est souvent utile de définir des valeurs par défaut pour tous les contrôles à la fois. Par exemple, lorsque le formulaire est utilisé pour modifier des enregistrements. Nous lisons l'enregistrement depuis la base de données et le définissons comme valeur par défaut :

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

Appelez `setDefaults()` après avoir défini les contrôles.


Rendu du formulaire .[#toc-rendering-the-form]
==============================================

Par défaut, le formulaire est rendu sous forme de tableau. Les contrôles individuels suivent les directives de base en matière d'accessibilité du Web. Toutes les étiquettes sont générées en tant qu'éléments `<label>` et sont associées à leurs entrées. Un clic sur l'étiquette déplace le curseur sur l'entrée.

Nous pouvons définir n'importe quel attribut HTML pour chaque élément. Par exemple, ajouter un espace réservé :

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

Il y a vraiment beaucoup de façons de rendre un formulaire, c'est pourquoi ce [chapitre |rendering] est consacré [au rendu |rendering].


Mappage vers les classes .[#toc-mapping-to-classes]
===================================================

Revenons à la méthode `formSucceeded()`, qui, dans le second paramètre `$data`, reçoit les données envoyées sous la forme d'un objet `ArrayHash`. Comme il s'agit d'une classe générique, un peu comme `stdClass`, il nous manquera certaines commodités pour travailler avec elle, comme la complétion du code pour les propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait être résolu en ayant une classe spécifique pour chaque formulaire, dont les propriétés représentent les contrôles individuels. Par exemple :

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

A partir de PHP 8.0, vous pouvez utiliser cette notation élégante qui utilise un constructeur :

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

Comment dire à Nette de retourner les données sous forme d'objets de cette classe ? C'est plus facile que vous ne le pensez. Tout ce que vous avez à faire est de spécifier la classe comme type du paramètre `$data` dans le gestionnaire :

```php
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name est une instance de RegistrationFormData
	$name = $data->name;
	// ...
}
```

Vous pouvez également spécifier `array` comme type et les données seront alors transmises sous forme de tableau.

De la même manière, vous pouvez utiliser la méthode `getValues()`, que nous passons comme nom de classe ou objet à hydrater en tant que paramètre :

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

Si les formulaires consistent en une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun d'eux :

```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;
}
```

Le mappage sait alors, à partir du type de propriété `$person`, qu'il doit mapper le conteneur à la classe `PersonFormData`. Si la propriété contient un tableau de conteneurs, fournissez le type `array` et passez la classe à mapper directement au conteneur :

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

Vous pouvez générer une proposition pour la classe de données d'un formulaire à l'aide de la méthode `Nette\Forms\Blueprint::dataClass($form)`, qui l'imprimera sur la page du navigateur. Il vous suffit ensuite de cliquer pour sélectionner et copier le code dans votre projet. .{data-version:3.1.15}


Boutons d'envoi multiples .[#toc-multiple-submit-buttons]
=========================================================

Si le formulaire comporte plus d'un bouton, nous avons généralement besoin de distinguer lequel a été pressé. Nous pouvons créer notre propre fonction pour chaque bouton. Définissez-la comme gestionnaire de l'[événement |nette:glossary#Events] `onClick`:

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

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

Ces gestionnaires sont également appelés uniquement dans le cas où le formulaire est valide, comme dans le cas de l'événement `onSuccess`. La différence est que le premier paramètre peut être l'objet bouton de soumission au lieu du formulaire, selon le type que vous spécifiez :

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

Lorsqu'un formulaire est soumis avec la touche <kbd>Entrée</kbd>, il est traité comme s'il avait été soumis avec le premier bouton.


Événement onAnchor .[#toc-event-onanchor]
=========================================

Lorsque vous construisez un formulaire à l'aide d'une méthode d'usine (comme `createComponentRegistrationForm`), il ne sait pas encore s'il a été soumis ou avec quelles données il a été soumis. Mais il existe des cas où nous avons besoin de connaître les valeurs soumises, peut-être que l'aspect du formulaire en dépend, ou qu'elles sont utilisées pour des boîtes de sélection dépendantes, etc.

Par conséquent, vous pouvez faire en sorte que le code qui construit le formulaire soit appelé lorsqu'il est ancré, c'est-à-dire qu'il est déjà lié au présentateur et qu'il connaît ses données soumises. Nous placerons ce code dans le tableau `$onAnchor`:

```php
$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');

$form->onAnchor[] = function () use ($country, $city) {
	// cette fonction sera appelée lorsque le formulaire connaîtra les données avec lesquelles il a été soumis
	// pour que vous puissiez utiliser la méthode getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};
```


Protection contre les vulnérabilités .[#toc-vulnerability-protection]
=====================================================================

Nette Framework fait de gros efforts pour être sûr et comme les formulaires sont les entrées les plus courantes de l'utilisateur, les formulaires Nette sont aussi bien impénétrables. Tout est géré de manière dynamique et transparente, rien ne doit être réglé manuellement.

En plus de protéger les formulaires contre les attaques visant des vulnérabilités bien connues comme le [Cross-Site Scripting (XSS) |nette:glossary#cross-site-scripting-xss] et le [Cross-Site Request Forgery (CSRF) |nette:glossary#cross-site-request-forgery-csrf], il effectue de nombreuses petites tâches de sécurité auxquelles vous ne devez plus penser.

Par exemple, il filtre tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, de sorte que les données du formulaire sont toujours propres. Pour les cases de sélection et les listes radio, il vérifie que les éléments sélectionnés font bien partie des éléments proposés et qu'il n'y a pas eu de falsification. Nous avons déjà mentionné que pour les entrées de texte sur une seule ligne, il supprime les caractères de fin de ligne qu'un attaquant pourrait y envoyer. Pour les entrées multilignes, il normalise les caractères de fin de ligne. Et ainsi de suite.

Nette corrige pour vous les failles de sécurité dont la plupart des programmeurs ignorent l'existence.

L'attaque CSRF mentionnée consiste en ce qu'un attaquant attire la victime à visiter une page qui exécute silencieusement une requête dans le navigateur de la victime vers le serveur où la victime est actuellement connectée, et le serveur croit que la requête a été faite par la victime à volonté. Par conséquent, Nette empêche que le formulaire soit soumis via POST à partir d'un autre domaine. Si, pour une raison quelconque, vous souhaitez désactiver la protection et autoriser la soumission du formulaire à partir d'un autre domaine, utilisez :

```php
$form->allowCrossOrigin(); // ATTENTION ! Désactive la protection !
```

Cette protection utilise un cookie SameSite nommé `_nss`. La protection par cookie SameSite peut ne pas être fiable à 100 %, il est donc préférable d'activer la protection par jeton :

```php
$form->addProtection();
```

Il est fortement recommandé d'appliquer cette protection aux formulaires d'une partie administrative de votre application qui modifie des données sensibles. Le framework protège contre une attaque CSRF en générant et en validant le jeton d'authentification qui est stocké dans une session (l'argument est le message d'erreur affiché si le jeton a expiré). C'est pourquoi il est nécessaire de démarrer une session avant d'afficher le formulaire. Dans la partie administration du site, la session est généralement déjà démarrée, en raison du login de l'utilisateur.
Sinon, démarrez la session avec la méthode `Nette\Http\Session::start()`.


Utilisation d'un formulaire dans plusieurs présentateurs .[#toc-using-one-form-in-multiple-presenters]
======================================================================================================

Si vous devez utiliser un formulaire dans plusieurs présentateurs, nous vous recommandons de créer une fabrique pour ce formulaire, que vous transmettrez ensuite au présentateur. Un emplacement approprié pour une telle classe est, par exemple, le répertoire `app/Forms`.

La classe de fabrique pourrait ressembler à ceci :

```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;
	}
}
```

Nous demandons à la classe de produire le formulaire dans la méthode d'usine pour les composants du présentateur :

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

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// nous pouvons modifier le formulaire, ici par exemple nous changeons le libellé du bouton
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // et ajouter le gestionnaire
	return $form;
}
```

Le gestionnaire de traitement du formulaire peut également être fourni par la fabrique :

```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 {
			// nous traitons ici notre formulaire soumis
		};
		return $form;
	}
}
```

Ainsi, nous avons une introduction rapide aux formulaires dans Nette. Essayez de regarder dans le répertoire des [exemples |https://github.com/nette/forms/tree/master/examples] dans la distribution pour plus d'inspiration.

Formulaires dans Presenters

Les formulaires Nette facilitent considérablement la création et le traitement des formulaires Web. Dans ce chapitre, vous allez apprendre à utiliser les formulaires dans les Presenters.

Si vous souhaitez les utiliser de manière complètement autonome sans le reste du framework, il existe un guide pour les formulaires autonomes.

Premier formulaire

Nous allons essayer d'écrire un formulaire d'inscription simple. Son code ressemblera à ceci :

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'];

et dans le navigateur, le résultat devrait ressembler à ceci :

Le formulaire dans le présentateur est un objet de la classe Nette\Application\UI\Form, son prédécesseur Nette\Forms\Form est destiné à une utilisation autonome. Nous lui avons ajouté les champs nom, mot de passe et bouton d'envoi. Enfin, la ligne avec $form->onSuccess indique qu'après soumission et validation réussie, la méthode $this->formSucceeded() doit être appelée.

Du point de vue du présentateur, le formulaire est un composant commun. Par conséquent, il est traité comme un composant et incorporé dans le présentateur à l'aide de la méthode factory. Cela ressemblera à ceci :

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;
	}

	public function formSucceeded(Form $form, $data): void
	{
		// ici nous allons traiter les données envoyées par le formulaire
		// $data->name contient le nom
		// $data->password contient password
		$this->flashMessage('Vous vous êtes inscrit avec succès.');
		$this->redirect('Home:');
	}
}

Et le rendu dans le modèle est effectué à l'aide de la balise {control}:

<h1>Registration</h1>

{control registrationForm}

Et c'est tout :-) Nous avons un formulaire fonctionnel et parfaitement sécurisé.

Maintenant, vous pensez probablement que c'était trop rapide, vous vous demandez comment il est possible que la méthode formSucceeded() soit appelée et quels sont les paramètres qu'elle reçoit. Bien sûr, vous avez raison, cela mérite une explication.

Nette propose un mécanisme sympa, que nous appelons le style Hollywood. Au lieu de devoir constamment demander si quelque chose s'est produit („le formulaire a-t-il été soumis ?“, „a-t-il été soumis de manière valide ?“ ou „n'a-t-il pas été forgé ?“), vous dites au framework „lorsque le formulaire est valablement rempli, appelez cette méthode“ et laissez la suite du travail. Si vous programmez en JavaScript, vous êtes familier avec ce style de programmation. Vous écrivez des fonctions qui sont appelées lorsqu'un certain événement se produit. Et le langage leur transmet les arguments appropriés.

C'est ainsi que le code du présentateur ci-dessus est construit. Le tableau $form->onSuccess représente la liste des callbacks PHP que Nette appellera lorsque le formulaire sera soumis et rempli correctement. Dans le cycle de vie du présentateur, il s'agit de ce que l'on appelle un signal. Ils sont donc appelés après la méthode action* et avant la méthode render*. Il transmet à chaque callback le formulaire lui-même dans le premier paramètre et les données envoyées sous forme d'objet ArrayHash dans le second. Vous pouvez omettre le premier paramètre si vous n'avez pas besoin de l'objet formulaire. Le second paramètre peut être encore plus pratique, mais nous y reviendrons plus tard.

L'objet $data contient les propriétés name et password avec les données saisies par l'utilisateur. Habituellement, nous envoyons les données directement pour un traitement ultérieur, qui peut être, par exemple, l'insertion dans la base de données. Toutefois, une erreur peut se produire pendant le traitement, par exemple, le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire en utilisant addError() et le laissons se redessiner, avec un message d'erreur :

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

En plus de onSuccess, il y a aussi onSubmit: les callbacks sont toujours appelés après l'envoi du formulaire, même s'il n'est pas rempli correctement. Et enfin onError: les callbacks ne sont appelés que si la soumission n'est pas valide. Ils sont même appelés si nous invalidons le formulaire dans onSuccess ou onSubmit en utilisant addError().

Après avoir traité le formulaire, nous redirigeons vers la page suivante. Cela permet d'éviter que le formulaire soit involontairement resoumis en cliquant sur le bouton refresh, back, ou en déplaçant l'historique du navigateur.

Essayez d'ajouter d'autres contrôles de formulaire.

Accès aux contrôles

Le formulaire est un composant du présentateur, nommé dans notre cas registrationForm (d'après le nom de la méthode d'usine createComponentRegistrationForm), donc n'importe où dans le présentateur vous pouvez accéder au formulaire en utilisant :

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

Les contrôles individuels du formulaire sont également des composants, vous pouvez donc y accéder de la même manière :

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

Les contrôles sont supprimés à l'aide de unset :

unset($form['name']);

Règles de validation

Le mot valide a été utilisé plusieurs fois, mais le formulaire n'a pas encore de règles de validation. Corrigeons cela.

Le nom sera obligatoire, nous le marquerons donc avec la méthode setRequired(), dont l'argument est le texte du message d'erreur qui sera affiché si l'utilisateur ne le remplit pas. Si aucun argument n'est donné, le message d'erreur par défaut est utilisé.

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

Essayez de soumettre le formulaire sans que le nom soit rempli et vous verrez qu'un message d'erreur s'affiche et que le navigateur ou le serveur le rejettera jusqu'à ce que vous le remplissiez.

En même temps, vous ne pourrez pas tromper le système en ne tapant que des espaces dans la saisie, par exemple. Pas question. Nette supprime automatiquement les espaces à gauche et à droite. Essayez-le. C'est quelque chose que vous devriez toujours faire avec chaque entrée d'une seule ligne, mais on l'oublie souvent. Nette le fait automatiquement. (Vous pouvez essayer de tromper les formulaires et envoyer une chaîne de caractères multiligne comme nom. Même dans ce cas, Nette ne sera pas dupe et les sauts de ligne se transformeront en espaces).

Le formulaire est toujours validé du côté du serveur, mais la validation JavaScript est également générée, ce qui est rapide et l'utilisateur connaît l'erreur immédiatement, sans avoir à envoyer le formulaire au serveur. Ceci est géré par le script netteForms.js. Insérez-le dans le modèle de mise en page :

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

Si vous regardez dans le code source de la page avec formulaire, vous pouvez remarquer que Nette insère les champs obligatoires dans des éléments avec une classe CSS required. Essayez d'ajouter le style suivant au modèle, et l'étiquette „Nom“ sera rouge. De manière élégante, nous marquons les champs obligatoires pour les utilisateurs :

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

Des règles de validation supplémentaires seront ajoutées par la méthode addRule(). Le premier paramètre est la règle, le second est à nouveau le texte du message d'erreur, et l'argument facultatif de la règle de validation peut suivre. Qu'est-ce que cela signifie ?

Le formulaire recevra une autre entrée facultative âge avec la condition qu'il s'agisse d'un nombre (addInteger()) et qu'il soit dans certaines limites ($form::Range). Et ici, nous utiliserons le troisième argument de addRule(), la plage elle-même :

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

Si l'utilisateur ne remplit pas le champ, les règles de validation ne seront pas vérifiées, car le champ est facultatif.

Il y a évidemment de la place pour un petit remaniement. Dans le message d'erreur et dans le troisième paramètre, les nombres sont listés en double, ce qui n'est pas idéal. Si nous créions un formulaire multilingue et que le message contenant les chiffres devait être traduit en plusieurs langues, il serait plus difficile de modifier les valeurs. C'est pourquoi on peut utiliser les caractères de substitution %d:

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

Revenons au champ mot de passe, rendons-le obligatoire et vérifions la longueur minimale du mot de passe ($form::MinLength), en utilisant à nouveau les caractères de substitution du message :

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

Nous ajouterons un champ passwordVerify au formulaire, dans lequel l'utilisateur saisira à nouveau le mot de passe, pour vérification. En utilisant les règles de validation, nous vérifions si les deux mots de passe sont les mêmes ($form::Equal). Et comme argument, nous donnons une référence au premier mot de passe en utilisant des crochets:

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

Avec setOmitted(), nous marquons un élément dont la valeur ne nous intéresse pas vraiment et qui n'existe que pour la validation. Sa valeur n'est pas transmise à $data.

Nous avons un formulaire entièrement fonctionnel avec validation en PHP et JavaScript. Les capacités de validation de Nette sont beaucoup plus larges, vous pouvez créer des conditions, afficher et masquer des parties d'une page en fonction de celles-ci, etc. Vous pouvez tout découvrir dans le chapitre sur la validation des formulaires.

Valeurs par défaut

Nous définissons souvent des valeurs par défaut pour les contrôles de formulaires :

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

Il est souvent utile de définir des valeurs par défaut pour tous les contrôles à la fois. Par exemple, lorsque le formulaire est utilisé pour modifier des enregistrements. Nous lisons l'enregistrement depuis la base de données et le définissons comme valeur par défaut :

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

Appelez setDefaults() après avoir défini les contrôles.

Rendu du formulaire

Par défaut, le formulaire est rendu sous forme de tableau. Les contrôles individuels suivent les directives de base en matière d'accessibilité du Web. Toutes les étiquettes sont générées en tant qu'éléments <label> et sont associées à leurs entrées. Un clic sur l'étiquette déplace le curseur sur l'entrée.

Nous pouvons définir n'importe quel attribut HTML pour chaque élément. Par exemple, ajouter un espace réservé :

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

Il y a vraiment beaucoup de façons de rendre un formulaire, c'est pourquoi ce chapitre est consacré au rendu.

Mappage vers les classes

Revenons à la méthode formSucceeded(), qui, dans le second paramètre $data, reçoit les données envoyées sous la forme d'un objet ArrayHash. Comme il s'agit d'une classe générique, un peu comme stdClass, il nous manquera certaines commodités pour travailler avec elle, comme la complétion du code pour les propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait être résolu en ayant une classe spécifique pour chaque formulaire, dont les propriétés représentent les contrôles individuels. Par exemple :

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

A partir de PHP 8.0, vous pouvez utiliser cette notation élégante qui utilise un constructeur :

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

Comment dire à Nette de retourner les données sous forme d'objets de cette classe ? C'est plus facile que vous ne le pensez. Tout ce que vous avez à faire est de spécifier la classe comme type du paramètre $data dans le gestionnaire :

public function formSucceeded(Form $form, RegistrationFormData $data): void
{
	// $name est une instance de RegistrationFormData
	$name = $data->name;
	// ...
}

Vous pouvez également spécifier array comme type et les données seront alors transmises sous forme de tableau.

De la même manière, vous pouvez utiliser la méthode getValues(), que nous passons comme nom de classe ou objet à hydrater en tant que paramètre :

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

Si les formulaires consistent en une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun d'eux :

$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;
}

Le mappage sait alors, à partir du type de propriété $person, qu'il doit mapper le conteneur à la classe PersonFormData. Si la propriété contient un tableau de conteneurs, fournissez le type array et passez la classe à mapper directement au conteneur :

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

Vous pouvez générer une proposition pour la classe de données d'un formulaire à l'aide de la méthode Nette\Forms\Blueprint::dataClass($form), qui l'imprimera sur la page du navigateur. Il vous suffit ensuite de cliquer pour sélectionner et copier le code dans votre projet.

Boutons d'envoi multiples

Si le formulaire comporte plus d'un bouton, nous avons généralement besoin de distinguer lequel a été pressé. Nous pouvons créer notre propre fonction pour chaque bouton. Définissez-la comme gestionnaire de l'événement onClick:

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

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

Ces gestionnaires sont également appelés uniquement dans le cas où le formulaire est valide, comme dans le cas de l'événement onSuccess. La différence est que le premier paramètre peut être l'objet bouton de soumission au lieu du formulaire, selon le type que vous spécifiez :

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

Lorsqu'un formulaire est soumis avec la touche Entrée, il est traité comme s'il avait été soumis avec le premier bouton.

Événement onAnchor

Lorsque vous construisez un formulaire à l'aide d'une méthode d'usine (comme createComponentRegistrationForm), il ne sait pas encore s'il a été soumis ou avec quelles données il a été soumis. Mais il existe des cas où nous avons besoin de connaître les valeurs soumises, peut-être que l'aspect du formulaire en dépend, ou qu'elles sont utilisées pour des boîtes de sélection dépendantes, etc.

Par conséquent, vous pouvez faire en sorte que le code qui construit le formulaire soit appelé lorsqu'il est ancré, c'est-à-dire qu'il est déjà lié au présentateur et qu'il connaît ses données soumises. Nous placerons ce code dans le tableau $onAnchor:

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

$form->onAnchor[] = function () use ($country, $city) {
	// cette fonction sera appelée lorsque le formulaire connaîtra les données avec lesquelles il a été soumis
	// pour que vous puissiez utiliser la méthode getValue()
	$val = $country->getValue();
	$city->setItems($val ? $this->model->getCities($val) : []);
};

Protection contre les vulnérabilités

Nette Framework fait de gros efforts pour être sûr et comme les formulaires sont les entrées les plus courantes de l'utilisateur, les formulaires Nette sont aussi bien impénétrables. Tout est géré de manière dynamique et transparente, rien ne doit être réglé manuellement.

En plus de protéger les formulaires contre les attaques visant des vulnérabilités bien connues comme le Cross-Site Scripting (XSS) et le Cross-Site Request Forgery (CSRF), il effectue de nombreuses petites tâches de sécurité auxquelles vous ne devez plus penser.

Par exemple, il filtre tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, de sorte que les données du formulaire sont toujours propres. Pour les cases de sélection et les listes radio, il vérifie que les éléments sélectionnés font bien partie des éléments proposés et qu'il n'y a pas eu de falsification. Nous avons déjà mentionné que pour les entrées de texte sur une seule ligne, il supprime les caractères de fin de ligne qu'un attaquant pourrait y envoyer. Pour les entrées multilignes, il normalise les caractères de fin de ligne. Et ainsi de suite.

Nette corrige pour vous les failles de sécurité dont la plupart des programmeurs ignorent l'existence.

L'attaque CSRF mentionnée consiste en ce qu'un attaquant attire la victime à visiter une page qui exécute silencieusement une requête dans le navigateur de la victime vers le serveur où la victime est actuellement connectée, et le serveur croit que la requête a été faite par la victime à volonté. Par conséquent, Nette empêche que le formulaire soit soumis via POST à partir d'un autre domaine. Si, pour une raison quelconque, vous souhaitez désactiver la protection et autoriser la soumission du formulaire à partir d'un autre domaine, utilisez :

$form->allowCrossOrigin(); // ATTENTION ! Désactive la protection !

Cette protection utilise un cookie SameSite nommé _nss. La protection par cookie SameSite peut ne pas être fiable à 100 %, il est donc préférable d'activer la protection par jeton :

$form->addProtection();

Il est fortement recommandé d'appliquer cette protection aux formulaires d'une partie administrative de votre application qui modifie des données sensibles. Le framework protège contre une attaque CSRF en générant et en validant le jeton d'authentification qui est stocké dans une session (l'argument est le message d'erreur affiché si le jeton a expiré). C'est pourquoi il est nécessaire de démarrer une session avant d'afficher le formulaire. Dans la partie administration du site, la session est généralement déjà démarrée, en raison du login de l'utilisateur. Sinon, démarrez la session avec la méthode Nette\Http\Session::start().

Utilisation d'un formulaire dans plusieurs présentateurs

Si vous devez utiliser un formulaire dans plusieurs présentateurs, nous vous recommandons de créer une fabrique pour ce formulaire, que vous transmettrez ensuite au présentateur. Un emplacement approprié pour une telle classe est, par exemple, le répertoire app/Forms.

La classe de fabrique pourrait ressembler à ceci :

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;
	}
}

Nous demandons à la classe de produire le formulaire dans la méthode d'usine pour les composants du présentateur :

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

protected function createComponentSignInForm(): Form
{
	$form = $this->formFactory->create();
	// nous pouvons modifier le formulaire, ici par exemple nous changeons le libellé du bouton
	$form['login']->setCaption('Continue');
	$form->onSuccess[] = [$this, 'signInFormSubmitted']; // et ajouter le gestionnaire
	return $form;
}

Le gestionnaire de traitement du formulaire peut également être fourni par la fabrique :

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 {
			// nous traitons ici notre formulaire soumis
		};
		return $form;
	}
}

Ainsi, nous avons une introduction rapide aux formulaires dans Nette. Essayez de regarder dans le répertoire des exemples dans la distribution pour plus d'inspiration.