Nette Documentation Preview

syntax
Formulaires dans les presenters
*******************************

.[perex]
Nette Forms simplifie considérablement la création et le traitement des formulaires web. Dans ce chapitre, vous apprendrez à utiliser les formulaires à l'intérieur des presenters.

Si vous êtes intéressé par leur utilisation totalement autonome, sans le reste du framework, un guide est consacré à l'[utilisation autonome|standalone].


Premier formulaire
==================

Essayons d'écrire un simple formulaire d'inscription. Son code sera le suivant :

```php
use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Nom :');
$form->addPassword('password', 'Mot de passe :');
$form->addSubmit('send', 'S\'inscrire');
$form->onSuccess[] = $this->formSucceeded(...);
```

et dans le navigateur, il s'affichera ainsi :

[* form-en.webp *]

Un formulaire dans un presenter est un objet de la classe `Nette\Application\UI\Form` ; son prédécesseur `Nette\Forms\Form` est destiné à une utilisation autonome. Nous y avons ajouté des champs nommés name et password, ainsi qu'un bouton d'envoi. Enfin, la ligne `$form->onSuccess` indique qu'après la soumission et une validation réussie, la méthode `$this->formSucceeded()` doit être appelée.

Du point de vue du presenter, le formulaire est un composant ordinaire. Il est donc traité comme un composant et intégré au presenter à l'aide d'une [méthode fabrique |application:components#Méthodes Factory]. Cela ressemblera à ceci :

```php .{file:app/Presentation/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', 'Nom :');
		$form->addPassword('password', 'Mot de passe :');
		$form->addSubmit('send', 'S\'inscrire');
		$form->onSuccess[] = $this->formSucceeded(...);
		return $form;
	}

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

Et dans le template, le formulaire se rend à l'aide de la balise `{control}` :

```latte .{file:app/Presentation/Home/default.latte}
<h1>Inscription</h1>

{control registrationForm}
```

Et c'est à peu près tout :-) Nous avons un formulaire fonctionnel et parfaitement [sécurisé |#Protection contre les vulnérabilités].

Vous vous dites sans doute que cela est allé trop vite et vous vous demandez comment il se fait que la méthode `formSucceeded()` soit appelée et quels paramètres elle reçoit. Oui, vous avez raison, cela mérite une explication.

Nette introduit un mécanisme rafraîchissant appelé [style hollywoodien |application:components#Style Hollywood]. Au lieu que vous, en tant que développeur, ayez sans cesse à demander si quelque chose s'est produit ('le formulaire a-t-il été envoyé ?', 'a-t-il été envoyé valablement ?' et 'n'a-t-il pas été falsifié ?'), vous dites au framework 'quand le formulaire sera valablement rempli, appelle cette méthode' et vous lui laissez le reste du travail. Si vous programmez en JavaScript, vous connaissez intimement ce style de programmation. Vous écrivez des fonctions qui sont appelées quand un certain [événement |nette:glossary#Événements] survient. Et le langage leur passe les arguments appropriés.

C'est exactement ainsi qu'est construit le code du presenter ci-dessus. Le tableau `$form->onSuccess` représente une liste de callbacks PHP que Nette appelle au moment où le formulaire est envoyé et correctement rempli (autrement dit valide). Dans le [cycle de vie du presenter |application:presenters#Cycle de vie du presenter], il s'agit de ce qu'on appelle un signal ; ils sont donc appelés après la méthode `action*` et avant la méthode `render*`. Et à chaque callback, il passe le formulaire lui-même en premier paramètre et les données soumises en deuxième, sous forme d'objet [ArrayHash |utils:arrays#ArrayHash] (ou stdClass, ou une classe personnalisée). Vous pouvez omettre le premier paramètre si vous n'avez pas besoin de l'objet formulaire. Le deuxième paramètre peut être plus malin, mais nous y reviendrons [plus loin |#Mapping vers des 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 au traitement suivant, qui peut être par exemple leur insertion dans une base de données. Une erreur peut cependant survenir pendant ce traitement, par exemple si le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire à l'aide d'`addError()` et le laissons se rendre à nouveau, avec le message d'erreur.

```php
$form->addError('Désolé, ce nom d\'utilisateur est déjà utilisé.');
```

Outre `onSuccess`, il existe aussi `onSubmit` : les callbacks sont appelés chaque fois que le formulaire est envoyé, même s'il n'est pas rempli correctement. Et aussi `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` à l'aide d'`addError()`.

Après le traitement du formulaire, nous redirigeons vers une autre page. Cela évite le renvoi involontaire du formulaire par le bouton *actualiser*, *retour* ou en naviguant dans l'historique du navigateur.

Si le formulaire est envoyé en AJAX, vous redessinez généralement un [snippet |application:ajax] contenant le formulaire re-rendu au lieu de rediriger.

Essayez d'ajouter d'autres [champs de formulaire|controls].


Accès aux champs
================

Le formulaire est un composant du presenter, dans notre cas nommé `registrationForm` (d'après le nom de la méthode fabrique `createComponentRegistrationForm`), vous pouvez donc accéder au formulaire de n'importe où dans le presenter à l'aide de :

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

Les différents champs du formulaire sont eux aussi des composants, vous y accédez donc de la même façon :

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

Les champs se suppriment à l'aide d'`unset` :

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


Règles de validation
====================

Le mot *valide* a été prononcé, mais le formulaire n'a encore aucune règle de validation. Corrigeons cela.

Le nom sera obligatoire, nous le marquons donc avec la méthode `setRequired()`. Son argument est le texte du message d'erreur affiché si l'utilisateur ne remplit pas le nom. Si l'argument est omis, un message d'erreur par défaut est utilisé.

```php
$form->addText('name', 'Nom :')
	->setRequired('Veuillez saisir votre nom.');
```

Essayez d'envoyer le formulaire sans remplir le nom et vous verrez s'afficher un message d'erreur ; le navigateur ou le serveur le refusera tant que vous n'aurez pas rempli le champ.

En même temps, vous ne pourrez pas tricher en saisissant, par exemple, uniquement des espaces dans le champ. Impossible. Nette supprime automatiquement les espaces au début et à la fin. Essayez. C'est une chose que vous devriez toujours faire avec chaque champ sur une ligne, et que l'on oublie pourtant souvent. Nette le fait automatiquement. (Vous pouvez essayer de piéger le formulaire en envoyant comme nom une chaîne sur plusieurs lignes. Là non plus Nette ne se laisse pas avoir : les sauts de ligne seront convertis en espaces.)

Le formulaire est toujours validé côté serveur, mais une validation JavaScript est également générée ; elle s'exécute immédiatement et l'utilisateur apprend l'erreur tout de suite, sans avoir à envoyer le formulaire au serveur. C'est le script `netteForms.js` qui s'en charge. Incluez-le dans votre template de layout :

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

Si vous regardez le code source de la page contenant le formulaire, vous remarquerez peut-être que Nette enveloppe les champs obligatoires dans des éléments portant la classe CSS `required`. Essayez d'ajouter la feuille de style suivante à votre template et le label 'Nom' deviendra rouge. Cela met élégamment en évidence les champs obligatoires pour les utilisateurs :

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

Nous ajoutons d'autres règles de validation avec la méthode `addRule()`. Le premier paramètre est la règle, le deuxième là encore le texte du message d'erreur, et un argument de la règle de validation peut suivre. Qu'est-ce que cela veut dire ?

Étoffons le formulaire d'un nouveau champ facultatif 'age', qui doit être un nombre entier (`addInteger()`) et se situer dans une plage autorisée (`$form::Range`). Nous utiliserons ici le troisième paramètre de la méthode `addRule()` pour passer au validateur la plage requise sous forme de paire `[min, max]` :

```php
$form->addInteger('age', 'Âge :')
	->addRule($form::Range, 'L\'âge doit être compris entre 18 et 120 ans.', [18, 120]);
```

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

Cela laisse place à un petit refactoring. Dans le message d'erreur et dans le troisième paramètre, les nombres sont dupliqués, ce qui n'est pas idéal. Si nous créions des [formulaires multilingues |rendering#Traduction] et que le message contenant les nombres était traduit en plusieurs langues, changer les valeurs deviendrait difficile. C'est pourquoi les placeholders `%d` peuvent être utilisés, et Nette y insérera les valeurs :

```php
	->addRule($form::Range, 'L\'âge doit être compris entre %d et %d ans.', [18, 120]);
```

Revenons au champ `password`, rendons-le lui aussi obligatoire et vérifions également la longueur minimale du mot de passe (`$form::MinLength`), là encore à l'aide d'un placeholder dans le message :

```php
$form->addPassword('password', 'Mot de passe :')
	->setRequired('Choisissez un mot de passe')
	->addRule($form::MinLength, 'Votre mot de passe doit comporter au moins %d caractères.', 8);
```

Ajoutons au formulaire un autre champ `passwordVerify`, où l'utilisateur saisit le mot de passe une seconde fois pour confirmation. À l'aide des règles de validation, nous contrôlons que les deux mots de passe sont identiques (`$form::Equal`). Comme argument, nous fournissons une référence au premier mot de passe à l'aide des [crochets |#Accès aux champs] :

```php
$form->addPassword('passwordVerify', 'Mot de passe à nouveau :')
	->setRequired('Saisissez à nouveau votre mot de passe pour détecter une faute de frappe')
	->addRule($form::Equal, 'Les mots de passe ne correspondent pas.', $form['password'])
	->setOmitted();
```

Avec `setOmitted()`, nous avons marqué un champ dont la valeur ne nous intéresse pas vraiment et qui n'existe qu'à des fins de validation. Sa valeur n'est pas transmise dans `$data`.

Nous avons ainsi un formulaire pleinement fonctionnel, avec validation en PHP comme en JavaScript. Les possibilités de validation de Nette sont bien plus larges : on peut créer des conditions, afficher ou masquer des parties de la page en fonction de celles-ci, etc. Vous apprendrez tout cela dans le chapitre sur la [validation des formulaires|validation].


Valeurs par défaut
==================

Nous définissons couramment des valeurs par défaut pour les champs du formulaire :

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

Il est souvent utile de définir les valeurs par défaut de tous les champs d'un coup. Par exemple lorsque le formulaire sert à modifier des enregistrements. Nous lisons l'enregistrement dans la base de données et définissons les valeurs par défaut :

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

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

Sur un formulaire déjà soumis, `setDefaults()` n'a aucun effet - il n'écrasera pas ce que l'utilisateur a rempli, on peut donc l'appeler sans condition dans la factory du formulaire. Si vous avez besoin d'imposer les valeurs même après la soumission, utilisez plutôt `setValues()`.


Rendu du formulaire
===================

Par défaut, le formulaire est rendu sous forme de tableau. Les différents champs respectent les règles de base d'accessibilité web : tous les labels sont écrits comme éléments `<label>` et associés au champ correspondant. Un clic sur le label place automatiquement le curseur dans le champ du formulaire.

Nous pouvons définir n'importe quels attributs HTML pour chaque champ. Ajoutons par exemple un placeholder :

```php
$form->addInteger('age', 'Âge :')
	->setHtmlAttribute('placeholder', 'Veuillez indiquer votre âge');
```

Il existe vraiment beaucoup de façons de rendre un formulaire, c'est pourquoi un [chapitre distinct sur le rendu|rendering] y est consacré.


Mapping vers des classes
========================

Revenons à la méthode `formSucceeded()`, qui reçoit dans son deuxième paramètre `$data` les données soumises sous forme d'objet `ArrayHash` (ou `stdClass`). Comme il s'agit d'une classe générique, semblable à `stdClass`, il nous manque certains conforts lors du travail avec elle, comme l'autocomplétion des propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait se résoudre en ayant pour chaque formulaire une classe dédiée dont les propriétés représentent les différents champs. Par exemple :

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

Vous pouvez aussi utiliser un constructeur :

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

Les propriétés de la classe de données peuvent aussi être des enums, elles seront mappées automatiquement. .{data-version:3.2.4}

Comment dire à Nette de renvoyer les données comme objets de cette classe ? Plus simplement que vous ne le pensez. Il suffit d'indiquer la classe comme type du paramètre `$data` dans la méthode gestionnaire :

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

Vous pouvez aussi indiquer `array` comme type, et les données seront alors passées sous forme de tableau.

De la même façon, vous pouvez utiliser la méthode `getValues()` en lui passant en paramètre le nom de la classe ou un objet à hydrater :

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

Si vous avez besoin de lire les valeurs avant que le formulaire ne soit validé - typiquement dans un gestionnaire `onValidate` - utilisez plutôt la méthode `getUntrustedValues()`. Elle accepte les mêmes paramètres que `getValues()`, mais renvoie les valeurs soumises sans garantir qu'elles ont passé la validation.

Si les formulaires ont une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun :

```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 mapping déduit alors du type de la propriété `$person` qu'il doit mapper le conteneur vers la classe `PersonFormData`. Si la propriété devait contenir un tableau de conteneurs, indiquez le type `array` et passez la classe à mapper directement au conteneur :

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

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


Plusieurs boutons d'envoi
=========================

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

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

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

.{data-version:3.3.0}
Un gestionnaire peut aussi être passé directement au bouton, comme troisième argument de la méthode `addSubmit()`.

Ces gestionnaires ne sont appelés que si le formulaire est valablement rempli (sauf si la validation est désactivée pour le bouton), tout comme l'événement `onSuccess`. La différence est que le premier paramètre passé peut être l'objet du bouton d'envoi au lieu du formulaire, selon la déclaration de type que vous indiquez :

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

Lorsque le formulaire est envoyé en appuyant sur la touche <kbd>Entrée</kbd>, il est traité comme s'il avait été envoyé par le premier bouton d'envoi.


Événement onAnchor
==================

Lorsque vous construisez un formulaire dans une méthode fabrique (comme `createComponentRegistrationForm`), il ne sait pas encore s'il a été envoyé ni avec quelles données. Il y a pourtant des cas où nous avons besoin de connaître les valeurs soumises, par exemple parce que l'apparence du formulaire en dépend, ou parce qu'elles sont nécessaires à des listes déroulantes dépendantes, etc.

Vous pouvez donc faire en sorte que le code qui construit le formulaire ne soit appelé qu'au moment où celui-ci est 'ancré', c'est-à-dire déjà relié au presenter et au courant de ses données soumises. Placez un tel code dans le tableau `$onAnchor` :

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

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


Protection contre les vulnérabilités
====================================

Nette Framework accorde une grande importance à la sécurité et veille donc scrupuleusement à la sécurisation des formulaires. Il le fait de façon totalement transparente et ne demande aucun réglage manuel.

Outre la protection des formulaires contre des attaques 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 applique quantité de petites mesures de sécurité auxquelles vous n'avez plus à penser.

Il filtre par exemple tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, si bien que les données issues du formulaire sont toujours propres. Pour les listes déroulantes et les listes de boutons radio, il vérifie que les éléments choisis figuraient bien parmi ceux proposés et qu'aucune falsification n'a eu lieu. Nous avons déjà dit que, pour les champs texte sur une ligne, il remplace par des espaces les caractères de fin de ligne qu'un attaquant pourrait envoyer. Pour les champs multilignes, il normalise les fins de ligne. Et ainsi de suite.

Nette règle pour vous des risques de sécurité dont beaucoup de programmeurs ignorent jusqu'à l'existence.

L'attaque CSRF évoquée consiste, pour un attaquant, à attirer la victime sur une page qui exécute discrètement, depuis le navigateur de la victime, une requête vers le serveur sur lequel elle est connectée. Le serveur croit alors que la requête a été faite volontairement par la victime. C'est pourquoi Nette refuse les formulaires POST envoyés depuis une origine étrangère ; même un autre sous-domaine du même site compte comme étranger. Si vous avez besoin d'autoriser l'envoi depuis une autre origine, désactivez la protection avec :

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

Cela désactive cependant la protection pour toutes les origines. Pour n'autoriser que certaines origines précises, désactivez la protection et vérifiez vous-même l'en-tête `Origin` contre votre propre liste d'autorisations.

La protection repose sur l'en-tête `Sec-Fetch-Site` du navigateur (Fetch Metadata), que celui-ci envoie automatiquement et qu'il est impossible de falsifier, même avec une faille XSS. Pour les navigateurs plus anciens qui ne les prennent pas en charge, un cookie SameSite de repli s'applique, qu'une application Nette met en place automatiquement. L'article [The browser finally solves CSRF |https://blog.nette.org/en/quarter-century-of-csrf] le décrit en détail.

.[note]
L'ancienne protection par un token d'autorisation stocké en session, activée par `$form->addProtection()`, n'est plus nécessaire et est obsolète depuis la version 3.3.


Utiliser un même formulaire dans plusieurs presenters
=====================================================

Si vous avez besoin d'utiliser le même formulaire dans plusieurs presenters, nous vous recommandons de créer pour lui une factory, que vous injecterez ensuite dans les presenters. Un emplacement approprié pour une telle classe est par exemple le répertoire `app/Forms`.

La classe factory pourrait ressembler à ceci :

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nom :');
		$form->addSubmit('send', 'Se connecter');
		return $form;
	}
}
```

Nous demandons à la classe de produire le formulaire dans la méthode fabrique du composant, au sein du presenter :

```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['send']->setCaption('Continuer');
	$form->onSuccess[] = $this->signInFormSuceeded(...); // et ajoutons un gestionnaire
	return $form;
}
```

Le gestionnaire de traitement du formulaire peut aussi être fourni par la factory elle-même :

```php
use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nom :');
		$form->addSubmit('send', 'Se connecter');
		$form->onSuccess[] = function (Form $form, $data): void {
			// nous traitons ici notre formulaire envoyé
		};
		return $form;
	}
}
```

Voilà, nous avons fait un tour d'horizon rapide des formulaires dans Nette. Pour plus d'inspiration, essayez de regarder dans le répertoire des [exemples |https://github.com/nette/forms/tree/master/examples] de la distribution.

Formulaires dans les presenters

Nette Forms simplifie considérablement la création et le traitement des formulaires web. Dans ce chapitre, vous apprendrez à utiliser les formulaires à l'intérieur des presenters.

Si vous êtes intéressé par leur utilisation totalement autonome, sans le reste du framework, un guide est consacré à l'utilisation autonome.

Premier formulaire

Essayons d'écrire un simple formulaire d'inscription. Son code sera le suivant :

use Nette\Application\UI\Form;

$form = new Form;
$form->addText('name', 'Nom :');
$form->addPassword('password', 'Mot de passe :');
$form->addSubmit('send', 'S\'inscrire');
$form->onSuccess[] = $this->formSucceeded(...);

et dans le navigateur, il s'affichera ainsi :

Un formulaire dans un presenter est un objet de la classe Nette\Application\UI\Form ; son prédécesseur Nette\Forms\Form est destiné à une utilisation autonome. Nous y avons ajouté des champs nommés name et password, ainsi qu'un bouton d'envoi. Enfin, la ligne $form->onSuccess indique qu'après la soumission et une validation réussie, la méthode $this->formSucceeded() doit être appelée.

Du point de vue du presenter, le formulaire est un composant ordinaire. Il est donc traité comme un composant et intégré au presenter à l'aide d'une méthode fabrique. 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', 'Nom :');
		$form->addPassword('password', 'Mot de passe :');
		$form->addSubmit('send', 'S\'inscrire');
		$form->onSuccess[] = $this->formSucceeded(...);
		return $form;
	}

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

Et dans le template, le formulaire se rend à l'aide de la balise {control} :

<h1>Inscription</h1>

{control registrationForm}

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

Vous vous dites sans doute que cela est allé trop vite et vous vous demandez comment il se fait que la méthode formSucceeded() soit appelée et quels paramètres elle reçoit. Oui, vous avez raison, cela mérite une explication.

Nette introduit un mécanisme rafraîchissant appelé style hollywoodien. Au lieu que vous, en tant que développeur, ayez sans cesse à demander si quelque chose s'est produit (‚le formulaire a-t-il été envoyé ?‘, ‚a-t-il été envoyé valablement ?‘ et ‚n'a-t-il pas été falsifié ?‘), vous dites au framework ‚quand le formulaire sera valablement rempli, appelle cette méthode‘ et vous lui laissez le reste du travail. Si vous programmez en JavaScript, vous connaissez intimement ce style de programmation. Vous écrivez des fonctions qui sont appelées quand un certain événement survient. Et le langage leur passe les arguments appropriés.

C'est exactement ainsi qu'est construit le code du presenter ci-dessus. Le tableau $form->onSuccess représente une liste de callbacks PHP que Nette appelle au moment où le formulaire est envoyé et correctement rempli (autrement dit valide). Dans le cycle de vie du presenter, il s'agit de ce qu'on appelle un signal ; ils sont donc appelés après la méthode action* et avant la méthode render*. Et à chaque callback, il passe le formulaire lui-même en premier paramètre et les données soumises en deuxième, sous forme d'objet ArrayHash (ou stdClass, ou une classe personnalisée). Vous pouvez omettre le premier paramètre si vous n'avez pas besoin de l'objet formulaire. Le deuxième paramètre peut être plus malin, mais nous y reviendrons plus loin.

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 au traitement suivant, qui peut être par exemple leur insertion dans une base de données. Une erreur peut cependant survenir pendant ce traitement, par exemple si le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire à l'aide d'addError() et le laissons se rendre à nouveau, avec le message d'erreur.

$form->addError('Désolé, ce nom d\'utilisateur est déjà utilisé.');

Outre onSuccess, il existe aussi onSubmit : les callbacks sont appelés chaque fois que le formulaire est envoyé, même s'il n'est pas rempli correctement. Et aussi 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 à l'aide d'addError().

Après le traitement du formulaire, nous redirigeons vers une autre page. Cela évite le renvoi involontaire du formulaire par le bouton actualiser, retour ou en naviguant dans l'historique du navigateur.

Si le formulaire est envoyé en AJAX, vous redessinez généralement un snippet contenant le formulaire re-rendu au lieu de rediriger.

Essayez d'ajouter d'autres champs de formulaire.

Accès aux champs

Le formulaire est un composant du presenter, dans notre cas nommé registrationForm (d'après le nom de la méthode fabrique createComponentRegistrationForm), vous pouvez donc accéder au formulaire de n'importe où dans le presenter à l'aide de :

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

Les différents champs du formulaire sont eux aussi des composants, vous y accédez donc de la même façon :

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

Les champs se suppriment à l'aide d'unset :

unset($form['name']);

Règles de validation

Le mot valide a été prononcé, mais le formulaire n'a encore aucune règle de validation. Corrigeons cela.

Le nom sera obligatoire, nous le marquons donc avec la méthode setRequired(). Son argument est le texte du message d'erreur affiché si l'utilisateur ne remplit pas le nom. Si l'argument est omis, un message d'erreur par défaut est utilisé.

$form->addText('name', 'Nom :')
	->setRequired('Veuillez saisir votre nom.');

Essayez d'envoyer le formulaire sans remplir le nom et vous verrez s'afficher un message d'erreur ; le navigateur ou le serveur le refusera tant que vous n'aurez pas rempli le champ.

En même temps, vous ne pourrez pas tricher en saisissant, par exemple, uniquement des espaces dans le champ. Impossible. Nette supprime automatiquement les espaces au début et à la fin. Essayez. C'est une chose que vous devriez toujours faire avec chaque champ sur une ligne, et que l'on oublie pourtant souvent. Nette le fait automatiquement. (Vous pouvez essayer de piéger le formulaire en envoyant comme nom une chaîne sur plusieurs lignes. Là non plus Nette ne se laisse pas avoir : les sauts de ligne seront convertis en espaces.)

Le formulaire est toujours validé côté serveur, mais une validation JavaScript est également générée ; elle s'exécute immédiatement et l'utilisateur apprend l'erreur tout de suite, sans avoir à envoyer le formulaire au serveur. C'est le script netteForms.js qui s'en charge. Incluez-le dans votre template de layout :

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

Si vous regardez le code source de la page contenant le formulaire, vous remarquerez peut-être que Nette enveloppe les champs obligatoires dans des éléments portant la classe CSS required. Essayez d'ajouter la feuille de style suivante à votre template et le label ‚Nom‘ deviendra rouge. Cela met élégamment en évidence les champs obligatoires pour les utilisateurs :

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

Nous ajoutons d'autres règles de validation avec la méthode addRule(). Le premier paramètre est la règle, le deuxième là encore le texte du message d'erreur, et un argument de la règle de validation peut suivre. Qu'est-ce que cela veut dire ?

Étoffons le formulaire d'un nouveau champ facultatif ‚age‘, qui doit être un nombre entier (addInteger()) et se situer dans une plage autorisée ($form::Range). Nous utiliserons ici le troisième paramètre de la méthode addRule() pour passer au validateur la plage requise sous forme de paire [min, max] :

$form->addInteger('age', 'Âge :')
	->addRule($form::Range, 'L\'âge doit être compris entre 18 et 120 ans.', [18, 120]);

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

Cela laisse place à un petit refactoring. Dans le message d'erreur et dans le troisième paramètre, les nombres sont dupliqués, ce qui n'est pas idéal. Si nous créions des formulaires multilingues et que le message contenant les nombres était traduit en plusieurs langues, changer les valeurs deviendrait difficile. C'est pourquoi les placeholders %d peuvent être utilisés, et Nette y insérera les valeurs :

	->addRule($form::Range, 'L\'âge doit être compris entre %d et %d ans.', [18, 120]);

Revenons au champ password, rendons-le lui aussi obligatoire et vérifions également la longueur minimale du mot de passe ($form::MinLength), là encore à l'aide d'un placeholder dans le message :

$form->addPassword('password', 'Mot de passe :')
	->setRequired('Choisissez un mot de passe')
	->addRule($form::MinLength, 'Votre mot de passe doit comporter au moins %d caractères.', 8);

Ajoutons au formulaire un autre champ passwordVerify, où l'utilisateur saisit le mot de passe une seconde fois pour confirmation. À l'aide des règles de validation, nous contrôlons que les deux mots de passe sont identiques ($form::Equal). Comme argument, nous fournissons une référence au premier mot de passe à l'aide des crochets :

$form->addPassword('passwordVerify', 'Mot de passe à nouveau :')
	->setRequired('Saisissez à nouveau votre mot de passe pour détecter une faute de frappe')
	->addRule($form::Equal, 'Les mots de passe ne correspondent pas.', $form['password'])
	->setOmitted();

Avec setOmitted(), nous avons marqué un champ dont la valeur ne nous intéresse pas vraiment et qui n'existe qu'à des fins de validation. Sa valeur n'est pas transmise dans $data.

Nous avons ainsi un formulaire pleinement fonctionnel, avec validation en PHP comme en JavaScript. Les possibilités de validation de Nette sont bien plus larges : on peut créer des conditions, afficher ou masquer des parties de la page en fonction de celles-ci, etc. Vous apprendrez tout cela dans le chapitre sur la validation des formulaires.

Valeurs par défaut

Nous définissons couramment des valeurs par défaut pour les champs du formulaire :

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

Il est souvent utile de définir les valeurs par défaut de tous les champs d'un coup. Par exemple lorsque le formulaire sert à modifier des enregistrements. Nous lisons l'enregistrement dans la base de données et définissons les valeurs par défaut :

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

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

Sur un formulaire déjà soumis, setDefaults() n'a aucun effet – il n'écrasera pas ce que l'utilisateur a rempli, on peut donc l'appeler sans condition dans la factory du formulaire. Si vous avez besoin d'imposer les valeurs même après la soumission, utilisez plutôt setValues().

Rendu du formulaire

Par défaut, le formulaire est rendu sous forme de tableau. Les différents champs respectent les règles de base d'accessibilité web : tous les labels sont écrits comme éléments <label> et associés au champ correspondant. Un clic sur le label place automatiquement le curseur dans le champ du formulaire.

Nous pouvons définir n'importe quels attributs HTML pour chaque champ. Ajoutons par exemple un placeholder :

$form->addInteger('age', 'Âge :')
	->setHtmlAttribute('placeholder', 'Veuillez indiquer votre âge');

Il existe vraiment beaucoup de façons de rendre un formulaire, c'est pourquoi un chapitre distinct sur le rendu y est consacré.

Mapping vers des classes

Revenons à la méthode formSucceeded(), qui reçoit dans son deuxième paramètre $data les données soumises sous forme d'objet ArrayHash (ou stdClass). Comme il s'agit d'une classe générique, semblable à stdClass, il nous manque certains conforts lors du travail avec elle, comme l'autocomplétion des propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait se résoudre en ayant pour chaque formulaire une classe dédiée dont les propriétés représentent les différents champs. Par exemple :

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

Vous pouvez aussi utiliser un constructeur :

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

Les propriétés de la classe de données peuvent aussi être des enums, elles seront mappées automatiquement.

Comment dire à Nette de renvoyer les données comme objets de cette classe ? Plus simplement que vous ne le pensez. Il suffit d'indiquer la classe comme type du paramètre $data dans la méthode gestionnaire :

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

Vous pouvez aussi indiquer array comme type, et les données seront alors passées sous forme de tableau.

De la même façon, vous pouvez utiliser la méthode getValues() en lui passant en paramètre le nom de la classe ou un objet à hydrater :

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

Si vous avez besoin de lire les valeurs avant que le formulaire ne soit validé – typiquement dans un gestionnaire onValidate – utilisez plutôt la méthode getUntrustedValues(). Elle accepte les mêmes paramètres que getValues(), mais renvoie les valeurs soumises sans garantir qu'elles ont passé la validation.

Si les formulaires ont une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun :

$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 mapping déduit alors du type de la propriété $person qu'il doit mapper le conteneur vers la classe PersonFormData. Si la propriété devait contenir un tableau de conteneurs, indiquez le type array et passez la classe à mapper directement au conteneur :

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

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

Plusieurs boutons d'envoi

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

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

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

Un gestionnaire peut aussi être passé directement au bouton, comme troisième argument de la méthode addSubmit().

Ces gestionnaires ne sont appelés que si le formulaire est valablement rempli (sauf si la validation est désactivée pour le bouton), tout comme l'événement onSuccess. La différence est que le premier paramètre passé peut être l'objet du bouton d'envoi au lieu du formulaire, selon la déclaration de type que vous indiquez :

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

Lorsque le formulaire est envoyé en appuyant sur la touche Entrée, il est traité comme s'il avait été envoyé par le premier bouton d'envoi.

Événement onAnchor

Lorsque vous construisez un formulaire dans une méthode fabrique (comme createComponentRegistrationForm), il ne sait pas encore s'il a été envoyé ni avec quelles données. Il y a pourtant des cas où nous avons besoin de connaître les valeurs soumises, par exemple parce que l'apparence du formulaire en dépend, ou parce qu'elles sont nécessaires à des listes déroulantes dépendantes, etc.

Vous pouvez donc faire en sorte que le code qui construit le formulaire ne soit appelé qu'au moment où celui-ci est ‚ancré‘, c'est-à-dire déjà relié au presenter et au courant de ses données soumises. Placez un tel code dans le tableau $onAnchor :

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

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

Protection contre les vulnérabilités

Nette Framework accorde une grande importance à la sécurité et veille donc scrupuleusement à la sécurisation des formulaires. Il le fait de façon totalement transparente et ne demande aucun réglage manuel.

Outre la protection des formulaires contre des attaques comme le Cross-Site Scripting (XSS) et le Cross-Site Request Forgery (CSRF), il applique quantité de petites mesures de sécurité auxquelles vous n'avez plus à penser.

Il filtre par exemple tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, si bien que les données issues du formulaire sont toujours propres. Pour les listes déroulantes et les listes de boutons radio, il vérifie que les éléments choisis figuraient bien parmi ceux proposés et qu'aucune falsification n'a eu lieu. Nous avons déjà dit que, pour les champs texte sur une ligne, il remplace par des espaces les caractères de fin de ligne qu'un attaquant pourrait envoyer. Pour les champs multilignes, il normalise les fins de ligne. Et ainsi de suite.

Nette règle pour vous des risques de sécurité dont beaucoup de programmeurs ignorent jusqu'à l'existence.

L'attaque CSRF évoquée consiste, pour un attaquant, à attirer la victime sur une page qui exécute discrètement, depuis le navigateur de la victime, une requête vers le serveur sur lequel elle est connectée. Le serveur croit alors que la requête a été faite volontairement par la victime. C'est pourquoi Nette refuse les formulaires POST envoyés depuis une origine étrangère ; même un autre sous-domaine du même site compte comme étranger. Si vous avez besoin d'autoriser l'envoi depuis une autre origine, désactivez la protection avec :

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

Cela désactive cependant la protection pour toutes les origines. Pour n'autoriser que certaines origines précises, désactivez la protection et vérifiez vous-même l'en-tête Origin contre votre propre liste d'autorisations.

La protection repose sur l'en-tête Sec-Fetch-Site du navigateur (Fetch Metadata), que celui-ci envoie automatiquement et qu'il est impossible de falsifier, même avec une faille XSS. Pour les navigateurs plus anciens qui ne les prennent pas en charge, un cookie SameSite de repli s'applique, qu'une application Nette met en place automatiquement. L'article The browser finally solves CSRF le décrit en détail.

L'ancienne protection par un token d'autorisation stocké en session, activée par $form->addProtection(), n'est plus nécessaire et est obsolète depuis la version 3.3.

Utiliser un même formulaire dans plusieurs presenters

Si vous avez besoin d'utiliser le même formulaire dans plusieurs presenters, nous vous recommandons de créer pour lui une factory, que vous injecterez ensuite dans les presenters. Un emplacement approprié pour une telle classe est par exemple le répertoire app/Forms.

La classe factory pourrait ressembler à ceci :

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nom :');
		$form->addSubmit('send', 'Se connecter');
		return $form;
	}
}

Nous demandons à la classe de produire le formulaire dans la méthode fabrique du composant, au sein du presenter :

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['send']->setCaption('Continuer');
	$form->onSuccess[] = $this->signInFormSuceeded(...); // et ajoutons un gestionnaire
	return $form;
}

Le gestionnaire de traitement du formulaire peut aussi être fourni par la factory elle-même :

use Nette\Application\UI\Form;

class SignInFormFactory
{
	public function create(): Form
	{
		$form = new Form;
		$form->addText('name', 'Nom :');
		$form->addSubmit('send', 'Se connecter');
		$form->onSuccess[] = function (Form $form, $data): void {
			// nous traitons ici notre formulaire envoyé
		};
		return $form;
	}
}

Voilà, nous avons fait un tour d'horizon rapide des formulaires dans Nette. Pour plus d'inspiration, essayez de regarder dans le répertoire des exemples de la distribution.