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.