Formulaires utilisés de manière autonome
****************************************
.[perex]
Nette Forms simplifie radicalement la création et le traitement des formulaires web. Vous pouvez les utiliser dans vos applications de façon totalement autonome, sans le reste du framework, comme le montre ce chapitre.
Si vous utilisez Nette Application et les presenters, un guide dédié vous attend en revanche : [les formulaires dans les presenters |in-presenter].
Premier formulaire
==================
Avant de commencer, installez le paquet à l'aide de [Composer |best-practices:composer] :
```shell
composer require nette/forms
```
Essayons d'écrire un simple formulaire d'inscription. Son code sera le suivant ("code complet":https://gist.github.com/dg/370a7e3094d9ba9a9e913b8e2a2dc851) :
```php
use Nette\Forms\Form;
$form = new Form;
$form->addText('name', 'Nom :');
$form->addPassword('password', 'Mot de passe :');
$form->addSubmit('send', 'S\'inscrire');
```
Et rendons-le très simplement :
```php
$form->render();
```
Le résultat dans le navigateur devrait ressembler à ceci :
[* form-en.webp *]
Le formulaire est un objet de la classe `Nette\Forms\Form` (dans les presenters, on utilise la classe `Nette\Application\UI\Form`). Nous y avons ajouté des champs nommés 'name' et 'password', ainsi qu'un bouton d'envoi.
Donnons maintenant vie au formulaire. En interrogeant `$form->isSuccess()`, nous apprenons si le formulaire a été soumis et s'il a été rempli valablement. Si c'est le cas, nous afficherons les données. Après la définition du formulaire, ajoutez :
```php
if ($form->isSuccess()) {
echo 'Le formulaire a été rempli et envoyé avec succès';
$data = $form->getValues();
// $data->name contient le nom
// $data->password contient le mot de passe
var_dump($data);
}
```
La méthode `getValues()` renvoie les données soumises sous forme d'objet [ArrayHash |utils:arrays#ArrayHash]. Nous montrerons [plus loin |#Mapping vers des classes] comment changer cela. L'objet `$data` contient les clés `name` et `password` avec les données saisies par l'utilisateur.
Habituellement, nous envoyons les données directement au traitement suivant, 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à pris.');
```
Après le traitement du formulaire, nous redirigeons vers la page suivante. Cela évite que le formulaire soit renvoyé involontairement en cliquant sur les boutons *actualiser* ou *retour*, ou en naviguant dans l'historique du navigateur.
Par défaut, le formulaire est envoyé par la méthode POST vers la même page. Les deux peuvent être changés :
```php
$form->setAction('/submit.php');
$form->setMethod('GET');
```
Et c'est à peu près tout :-) Nous avons un formulaire fonctionnel et parfaitement [sécurisé |#Protection contre les vulnérabilités].
Essayez d'ajouter aussi d'autres [champs de formulaire |controls].
Accès aux champs
================
Le formulaire et ses différents champs sont appelés composants. Ils forment un arbre de composants dont le formulaire est la racine. Vous pouvez accéder aux différents champs du formulaire de cette façon :
```php
$input = $form->getComponent('name');
// syntaxe alternative : $input = $form['name'];
$button = $form->getComponent('send');
// syntaxe alternative : $button = $form['send'];
```
Les champs se suppriment à l'aide d'`unset` :
```php
unset($form['name']);
```
Règles de validation
====================
Le mot *valablement* 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 aucun argument n'est fourni, le message d'erreur par défaut est utilisé.
```php
$form->addText('name', 'Nom :')
->setRequired('Veuillez saisir un nom.');
```
Essayez d'envoyer le formulaire sans remplir le nom et vous verrez apparaître 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 tapant 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. Insérez-le dans la page :
```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 place les champs obligatoires dans des éléments portant la classe CSS `required`. Essayez d'ajouter la feuille de style suivante au 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 est là encore le texte du message d'erreur, et un argument facultatif de la règle de validation peut suivre. Qu'est-ce que cela veut dire ?
Étoffons le formulaire d'un nouveau champ facultatif "âge", 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 le champ est facultatif.
Cela laisse place à un petit refactoring. Les nombres sont dupliqués dans le message d'erreur et dans le troisième paramètre, 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, 'Le 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 vérification. À l'aide des règles de validation, nous contrôlons que les deux mots de passe sont identiques (`$form::Equal`). Comme paramètre, 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('Veuillez saisir à nouveau le mot de passe pour vérification')
->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 : vous pouvez créer des conditions, afficher et 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 souvent 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 un enregistrement. Nous lisons l'enregistrement dans la base de données et définissons ses valeurs comme 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é : tous les labels sont générés 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 beaucoup de façons de rendre un formulaire, c'est pourquoi un [chapitre distinct est consacré au rendu |rendering].
Rendu avec Latte
----------------
Si vous avez sous la main le moteur de templates [Latte |latte:], vous pouvez lui confier le rendu du formulaire et garder le contrôle total sur le HTML obtenu. Vous créez le moteur, enregistrez l'extension des formulaires et passez le formulaire au template comme variable :
```php
$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);
$latte->render('form.latte', ['form' => $form]);
```
Dans le template, vous travaillez ensuite avec le formulaire via la variable `$form` et des balises comme `{input}`, `{label}` ou `n:name`. Un exemple complet, template compris, se trouve dans le répertoire des [exemples |https://github.com/nette/forms/tree/master/examples] (les fichiers `latte.php` et `latte/`). Les différentes balises sont décrites dans le chapitre sur le [rendu |rendering].
Mapping vers des classes
========================
Revenons au traitement des données du formulaire. La méthode `getValues()` renvoyait les données soumises sous forme d'objet `ArrayHash`. 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 le 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 en paramètre le nom de la classe ou l'objet à hydrater :
```php
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
```
Vous pouvez aussi indiquer `'array'` comme paramètre, et les données seront renvoyées sous forme de tableau.
Si les formulaires forment 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 mapping sait alors, d'après le 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'affichera 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é. La méthode `isSubmittedBy()` du bouton nous donne cette information :
```php
$form->addSubmit('save', 'Enregistrer');
$form->addSubmit('delete', 'Supprimer');
if ($form->isSuccess()) {
if ($form['save']->isSubmittedBy()) {
// ...
}
if ($form['delete']->isSubmittedBy()) {
// ...
}
}
```
N'omettez pas la vérification `$form->isSuccess()` ; c'est elle qui contrôle la validité des données.
Lorsqu'un 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.
Protection contre les vulnérabilités
====================================
Nette Framework accorde une grande importance à la sécurité et veille donc scrupuleusement à la bonne sécurisation des formulaires.
Outre la protection des formulaires contre les 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 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 seront toujours propres. Pour les listes déroulantes et les listes de boutons radio, il vérifie que les éléments choisis figuraient bien parmi les options proposées 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 actuellement 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. Les navigateurs plus anciens, qui n'envoient pas ces en-têtes, ne passeront pas le contrôle. 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.
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 utilisés de manière autonome
Nette Forms simplifie radicalement la création et le traitement des formulaires web. Vous pouvez les utiliser
dans vos applications de façon totalement autonome, sans le reste du framework, comme le montre ce chapitre.
Avant de commencer, installez le paquet à l'aide de Composer :
composer require nette/forms
Essayons d'écrire un simple formulaire d'inscription. Son code sera le suivant (code complet) :
use Nette\Forms\Form;
$form = new Form;
$form->addText('name', 'Nom :');
$form->addPassword('password', 'Mot de passe :');
$form->addSubmit('send', 'S\'inscrire');
Et rendons-le très simplement :
$form->render();
Le résultat dans le navigateur devrait ressembler à ceci :
Le formulaire est un objet de la classe Nette\Forms\Form (dans les presenters, on utilise la classe
Nette\Application\UI\Form). Nous y avons ajouté des champs nommés ‘name’ et ‘password’, ainsi qu'un bouton
d'envoi.
Donnons maintenant vie au formulaire. En interrogeant $form->isSuccess(), nous apprenons si le formulaire a
été soumis et s'il a été rempli valablement. Si c'est le cas, nous afficherons les données. Après la définition du
formulaire, ajoutez :
if ($form->isSuccess()) {
echo 'Le formulaire a été rempli et envoyé avec succès';
$data = $form->getValues();
// $data->name contient le nom
// $data->password contient le mot de passe
var_dump($data);
}
La méthode getValues() renvoie les données soumises sous forme d'objet ArrayHash. Nous montrerons plus loin comment changer cela. L'objet $data contient les clés
name et password avec les données saisies par l'utilisateur.
Habituellement, nous envoyons les données directement au traitement suivant, 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à pris.');
Après le traitement du formulaire, nous redirigeons vers la page suivante. Cela évite que le formulaire soit renvoyé
involontairement en cliquant sur les boutons actualiser ou retour, ou en naviguant dans l'historique du
navigateur.
Par défaut, le formulaire est envoyé par la méthode POST vers la même page. Les deux peuvent être changés :
Le formulaire et ses différents champs sont appelés composants. Ils forment un arbre de composants dont le formulaire est la
racine. Vous pouvez accéder aux différents champs du formulaire de cette façon :
$input = $form->getComponent('name');
// syntaxe alternative : $input = $form['name'];
$button = $form->getComponent('send');
// syntaxe alternative : $button = $form['send'];
Les champs se suppriment à l'aide d'unset :
unset($form['name']);
Règles de validation
Le mot valablement 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 aucun argument n'est fourni, le message d'erreur par défaut
est utilisé.
$form->addText('name', 'Nom :')
->setRequired('Veuillez saisir un nom.');
Essayez d'envoyer le formulaire sans remplir le nom et vous verrez apparaître 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 tapant 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. Insérez-le dans la page :
Si vous regardez le code source de la page contenant le formulaire, vous remarquerez peut-être que Nette place les champs
obligatoires dans des éléments portant la classe CSS required. Essayez d'ajouter la feuille de style suivante au
template et le label “Nom” deviendra rouge. Cela met élégamment en évidence les champs obligatoires pour les
utilisateurs :
Nous ajoutons d'autres règles de validation avec la méthode addRule(). Le premier paramètre est la règle, le
deuxième est là encore le texte du message d'erreur, et un argument facultatif de la règle de validation peut suivre. Qu'est-ce
que cela veut dire ?
Étoffons le formulaire d'un nouveau champ facultatif “âge”, 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 le champ est
facultatif.
Cela laisse place à un petit refactoring. Les nombres sont dupliqués dans le message d'erreur et dans le troisième
paramètre, 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, 'Le 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 vérification. À l'aide des règles de validation, nous contrôlons que les deux mots de passe sont identiques
($form::Equal). Comme paramètre, nous fournissons une référence au premier mot de passe à l'aide des crochets :
$form->addPassword('passwordVerify', 'Mot de passe à nouveau :')
->setRequired('Veuillez saisir à nouveau le mot de passe pour vérification')
->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 : vous pouvez créer des conditions, afficher et 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 souvent des valeurs par défaut pour les champs du formulaire :
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 un enregistrement. Nous lisons l'enregistrement dans la base de données et définissons ses valeurs comme valeurs par
défaut :
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é : tous les labels sont générés 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 :
Si vous avez sous la main le moteur de templates Latte, vous pouvez lui confier le
rendu du formulaire et garder le contrôle total sur le HTML obtenu. Vous créez le moteur, enregistrez l'extension des
formulaires et passez le formulaire au template comme variable :
$latte = new Latte\Engine;
$latte->addExtension(new Nette\Bridges\FormsLatte\FormsExtension);
$latte->render('form.latte', ['form' => $form]);
Dans le template, vous travaillez ensuite avec le formulaire via la variable $form et des balises comme
{input}, {label} ou n:name. Un exemple complet, template compris, se trouve dans le
répertoire des exemples (les fichiers latte.php et
latte/). Les différentes balises sont décrites dans le chapitre sur le rendu.
Mapping vers des classes
Revenons au traitement des données du formulaire. La méthode getValues() renvoyait les données soumises sous
forme d'objet ArrayHash. 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 le 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 en paramètre le nom de la classe ou l'objet à hydrater :
Vous pouvez aussi indiquer 'array' comme paramètre, et les données seront renvoyées sous forme de tableau.
Si les formulaires forment 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 mapping sait alors, d'après le 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'affichera 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é. La méthode
isSubmittedBy() du bouton nous donne cette information :
$form->addSubmit('save', 'Enregistrer');
$form->addSubmit('delete', 'Supprimer');
if ($form->isSuccess()) {
if ($form['save']->isSubmittedBy()) {
// ...
}
if ($form['delete']->isSubmittedBy()) {
// ...
}
}
N'omettez pas la vérification $form->isSuccess() ; c'est elle qui contrôle la validité des données.
Lorsqu'un formulaire est envoyé en appuyant sur la touche Entrée, il est traité comme s'il avait été envoyé
par le premier bouton.
Protection contre les vulnérabilités
Nette Framework accorde une grande importance à la sécurité et veille donc scrupuleusement à la bonne sécurisation des
formulaires.
Outre la protection des formulaires contre les vulnérabilités bien connues 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 seront toujours propres. Pour les listes déroulantes et les listes de boutons radio, il
vérifie que les éléments choisis figuraient bien parmi les options proposées 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 actuellement 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. Les navigateurs plus anciens, qui n'envoient pas
ces en-têtes, ne passeront pas le contrôle. 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.
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.