Formulaires dans Presenters
Les formulaires Nette facilitent considérablement la création et le traitement des formulaires Web. Dans ce chapitre, vous allez apprendre à utiliser les formulaires dans les Presenters.
Si vous souhaitez les utiliser de manière complètement autonome sans le reste du framework, il existe un guide pour les formulaires autonomes.
Premier formulaire
Nous allons essayer d'écrire un formulaire d'inscription simple. Son code ressemblera à ceci :
use Nette\Application\UI\Form;
$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];
et dans le navigateur, le résultat devrait ressembler à ceci :
Le formulaire dans le présentateur est un objet de la classe Nette\Application\UI\Form
, son prédécesseur
Nette\Forms\Form
est destiné à une utilisation autonome. Nous lui avons ajouté les champs nom, mot de passe et
bouton d'envoi. Enfin, la ligne avec $form->onSuccess
indique qu'après soumission et validation réussie, la
méthode $this->formSucceeded()
doit être appelée.
Du point de vue du présentateur, le formulaire est un composant commun. Par conséquent, il est traité comme un composant et incorporé dans le présentateur à l'aide de la méthode factory. Cela ressemblera à ceci :
use Nette;
use Nette\Application\UI\Form;
class HomePresenter extends Nette\Application\UI\Presenter
{
protected function createComponentRegistrationForm(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');
$form->onSuccess[] = [$this, 'formSucceeded'];
return $form;
}
public function formSucceeded(Form $form, $data): void
{
// ici nous allons traiter les données envoyées par le formulaire
// $data->name contient le nom
// $data->password contient password
$this->flashMessage('Vous vous êtes inscrit avec succès.');
$this->redirect('Home:');
}
}
Et le rendu dans le modèle est effectué à l'aide de la balise {control}
:
<h1>Registration</h1>
{control registrationForm}
Et c'est tout :-) Nous avons un formulaire fonctionnel et parfaitement sécurisé.
Maintenant, vous pensez probablement que c'était trop rapide, vous vous demandez comment il est possible que la méthode
formSucceeded()
soit appelée et quels sont les paramètres qu'elle reçoit. Bien sûr, vous avez raison, cela
mérite une explication.
Nette propose un mécanisme sympa, que nous appelons le style Hollywood. Au lieu de devoir constamment demander si quelque chose s'est produit („le formulaire a-t-il été soumis ?“, „a-t-il été soumis de manière valide ?“ ou „n'a-t-il pas été forgé ?“), vous dites au framework „lorsque le formulaire est valablement rempli, appelez cette méthode“ et laissez la suite du travail. Si vous programmez en JavaScript, vous êtes familier avec ce style de programmation. Vous écrivez des fonctions qui sont appelées lorsqu'un certain événement se produit. Et le langage leur transmet les arguments appropriés.
C'est ainsi que le code du présentateur ci-dessus est construit. Le tableau $form->onSuccess
représente la
liste des callbacks PHP que Nette appellera lorsque le formulaire sera soumis et rempli correctement. Dans le cycle de vie du présentateur, il s'agit de ce
que l'on appelle un signal. Ils sont donc appelés après la méthode action*
et avant la méthode
render*
. Il transmet à chaque callback le formulaire lui-même dans le premier paramètre et les données envoyées
sous forme d'objet ArrayHash dans le second. Vous pouvez omettre le
premier paramètre si vous n'avez pas besoin de l'objet formulaire. Le second paramètre peut être encore plus pratique, mais
nous y reviendrons plus tard.
L'objet $data
contient les propriétés name
et password
avec les données saisies par
l'utilisateur. Habituellement, nous envoyons les données directement pour un traitement ultérieur, qui peut être, par exemple,
l'insertion dans la base de données. Toutefois, une erreur peut se produire pendant le traitement, par exemple, le nom
d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire en utilisant addError()
et le
laissons se redessiner, avec un message d'erreur :
$form->addError('Sorry, username is already in use.');
En plus de onSuccess
, il y a aussi onSubmit
: les callbacks sont toujours appelés après l'envoi du
formulaire, même s'il n'est pas rempli correctement. Et enfin onError
: les callbacks ne sont appelés que si la
soumission n'est pas valide. Ils sont même appelés si nous invalidons le formulaire dans onSuccess
ou
onSubmit
en utilisant addError()
.
Après avoir traité le formulaire, nous redirigeons vers la page suivante. Cela permet d'éviter que le formulaire soit involontairement resoumis en cliquant sur le bouton refresh, back, ou en déplaçant l'historique du navigateur.
Essayez d'ajouter d'autres contrôles de formulaire.
Accès aux contrôles
Le formulaire est un composant du présentateur, nommé dans notre cas registrationForm
(d'après le nom de la
méthode d'usine createComponentRegistrationForm
), donc n'importe où dans le présentateur vous pouvez accéder au
formulaire en utilisant :
$form = $this->getComponent('registrationForm');
// syntaxe alternative: $form = $this['registrationForm'];
Les contrôles individuels du formulaire sont également des composants, vous pouvez donc y accéder de la même manière :
$input = $form->getComponent('name'); // ou $input = $form['name'];
$button = $form->getComponent('send'); // ou $button = $form['send'];
Les contrôles sont supprimés à l'aide de unset :
unset($form['name']);
Règles de validation
Le mot valide a été utilisé plusieurs fois, mais le formulaire n'a pas encore de règles de validation. Corrigeons cela.
Le nom sera obligatoire, nous le marquerons donc avec la méthode setRequired()
, dont l'argument est le texte du
message d'erreur qui sera affiché si l'utilisateur ne le remplit pas. Si aucun argument n'est donné, le message d'erreur par
défaut est utilisé.
$form->addText('name', 'Name:')
->setRequired('Please fill your name.');
Essayez de soumettre le formulaire sans que le nom soit rempli et vous verrez qu'un message d'erreur s'affiche et que le navigateur ou le serveur le rejettera jusqu'à ce que vous le remplissiez.
En même temps, vous ne pourrez pas tromper le système en ne tapant que des espaces dans la saisie, par exemple. Pas question. Nette supprime automatiquement les espaces à gauche et à droite. Essayez-le. C'est quelque chose que vous devriez toujours faire avec chaque entrée d'une seule ligne, mais on l'oublie souvent. Nette le fait automatiquement. (Vous pouvez essayer de tromper les formulaires et envoyer une chaîne de caractères multiligne comme nom. Même dans ce cas, Nette ne sera pas dupe et les sauts de ligne se transformeront en espaces).
Le formulaire est toujours validé du côté du serveur, mais la validation JavaScript est également générée, ce qui est
rapide et l'utilisateur connaît l'erreur immédiatement, sans avoir à envoyer le formulaire au serveur. Ceci est géré par le
script netteForms.js
. Insérez-le dans le modèle de mise en page :
<script src="https://unpkg.com/nette-forms@3"></script>
Si vous regardez dans le code source de la page avec formulaire, vous pouvez remarquer que Nette insère les champs
obligatoires dans des éléments avec une classe CSS required
. Essayez d'ajouter le style suivant au modèle, et
l'étiquette „Nom“ sera rouge. De manière élégante, nous marquons les champs obligatoires pour les utilisateurs :
<style>
.required label { color: maroon }
</style>
Des règles de validation supplémentaires seront ajoutées par la méthode addRule()
. Le premier paramètre est
la règle, le second est à nouveau le texte du message d'erreur, et l'argument facultatif de la règle de validation peut suivre.
Qu'est-ce que cela signifie ?
Le formulaire recevra une autre entrée facultative âge avec la condition qu'il s'agisse d'un nombre
(addInteger()
) et qu'il soit dans certaines limites ($form::Range
). Et ici, nous utiliserons le
troisième argument de addRule()
, la plage elle-même :
$form->addInteger('age', 'Age:')
->addRule($form::Range, 'You must be older 18 years and be under 120.', [18, 120]);
Si l'utilisateur ne remplit pas le champ, les règles de validation ne seront pas vérifiées, car le champ est facultatif.
Il y a évidemment de la place pour un petit remaniement. Dans le message d'erreur et dans le troisième paramètre, les
nombres sont listés en double, ce qui n'est pas idéal. Si nous créions un formulaire
multilingue et que le message contenant les chiffres devait être traduit en plusieurs langues, il serait plus difficile de
modifier les valeurs. C'est pourquoi on peut utiliser les caractères de substitution %d
:
->addRule($form::Range, 'You must be older %d years and be under %d.', [18, 120]);
Revenons au champ mot de passe, rendons-le obligatoire et vérifions la longueur minimale du mot de passe
($form::MinLength
), en utilisant à nouveau les caractères de substitution du message :
$form->addPassword('password', 'Password:')
->setRequired('Pick a password')
->addRule($form::MinLength, 'Your password has to be at least %d long', 8);
Nous ajouterons un champ passwordVerify
au formulaire, dans lequel l'utilisateur saisira à nouveau le mot de
passe, pour vérification. En utilisant les règles de validation, nous vérifions si les deux mots de passe sont les mêmes
($form::Equal
). Et comme argument, nous donnons une référence au premier mot de passe en utilisant des crochets:
$form->addPassword('passwordVerify', 'Password again:')
->setRequired('Fill your password again to check for typo')
->addRule($form::Equal, 'Password mismatch', $form['password'])
->setOmitted();
Avec setOmitted()
, nous marquons un élément dont la valeur ne nous intéresse pas vraiment et qui n'existe que
pour la validation. Sa valeur n'est pas transmise à $data
.
Nous avons un formulaire entièrement fonctionnel avec validation en PHP et JavaScript. Les capacités de validation de Nette sont beaucoup plus larges, vous pouvez créer des conditions, afficher et masquer des parties d'une page en fonction de celles-ci, etc. Vous pouvez tout découvrir dans le chapitre sur la validation des formulaires.
Valeurs par défaut
Nous définissons souvent des valeurs par défaut pour les contrôles de formulaires :
$form->addEmail('email', 'Email')
->setDefaultValue($lastUsedEmail);
Il est souvent utile de définir des valeurs par défaut pour tous les contrôles à la fois. Par exemple, lorsque le formulaire est utilisé pour modifier des enregistrements. Nous lisons l'enregistrement depuis la base de données et le définissons comme valeur par défaut :
//$row = ['name' => 'John', 'age' => '33', /* ... */];
$form->setDefaults($row);
Appelez setDefaults()
après avoir défini les contrôles.
Rendu du formulaire
Par défaut, le formulaire est rendu sous forme de tableau. Les contrôles individuels suivent les directives de base en
matière d'accessibilité du Web. Toutes les étiquettes sont générées en tant qu'éléments <label>
et sont
associées à leurs entrées. Un clic sur l'étiquette déplace le curseur sur l'entrée.
Nous pouvons définir n'importe quel attribut HTML pour chaque élément. Par exemple, ajouter un espace réservé :
$form->addInteger('age', 'Age:')
->setHtmlAttribute('placeholder', 'Please fill in the age');
Il y a vraiment beaucoup de façons de rendre un formulaire, c'est pourquoi ce chapitre est consacré au rendu.
Mappage vers les classes
Revenons à la méthode formSucceeded()
, qui, dans le second paramètre $data
, reçoit les données
envoyées sous la forme d'un objet ArrayHash
. Comme il s'agit d'une classe générique, un peu comme
stdClass
, il nous manquera certaines commodités pour travailler avec elle, comme la complétion du code pour les
propriétés dans les éditeurs ou l'analyse statique du code. Cela pourrait être résolu en ayant une classe spécifique pour
chaque formulaire, dont les propriétés représentent les contrôles individuels. Par exemple :
class RegistrationFormData
{
public string $name;
public int $age;
public string $password;
}
Vous pouvez également 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 également être des enums et elles seront automatiquement mappées.
Comment dire à Nette de retourner les données sous forme d'objets de cette classe ? C'est plus facile que vous ne le pensez.
Tout ce que vous avez à faire est de spécifier la classe comme type du paramètre $data
dans le gestionnaire :
public function formSucceeded(Form $form, RegistrationFormData $data): void
{
// $name est une instance de RegistrationFormData
$name = $data->name;
// ...
}
Vous pouvez également spécifier array
comme type et les données seront alors transmises sous forme de
tableau.
De la même manière, vous pouvez utiliser la méthode getValues()
, que nous passons comme nom de classe ou objet
à hydrater en tant que paramètre :
$data = $form->getValues(RegistrationFormData::class);
$name = $data->name;
Si les formulaires consistent en une structure à plusieurs niveaux composée de conteneurs, créez une classe distincte pour chacun d'eux :
$form = new Form;
$person = $form->addContainer('person');
$person->addText('firstName');
/* ... */
class PersonFormData
{
public string $firstName;
public string $lastName;
}
class RegistrationFormData
{
public PersonFormData $person;
public int $age;
public string $password;
}
Le mappage sait alors, à partir du type de propriété $person
, qu'il doit mapper le conteneur à la classe
PersonFormData
. Si la propriété contient un tableau de conteneurs, fournissez le type array
et passez
la classe à mapper directement au conteneur :
$person->setMappedType(PersonFormData::class);
Vous pouvez générer une proposition pour la classe de données d'un formulaire à l'aide de la méthode
Nette\Forms\Blueprint::dataClass($form)
, qui l'imprimera sur la page du navigateur. Il vous suffit ensuite de cliquer
pour sélectionner et copier le code dans votre projet.
Boutons d'envoi multiples
Si le formulaire comporte plus d'un bouton, nous avons généralement besoin de distinguer lequel a été pressé. Nous pouvons
créer notre propre fonction pour chaque bouton. Définissez-la comme gestionnaire de l'événement onClick
:
$form->addSubmit('save', 'Save')
->onClick[] = [$this, 'saveButtonPressed'];
$form->addSubmit('delete', 'Delete')
->onClick[] = [$this, 'deleteButtonPressed'];
Ces gestionnaires sont également appelés uniquement dans le cas où le formulaire est valide, comme dans le cas de
l'événement onSuccess
. La différence est que le premier paramètre peut être l'objet bouton de soumission au lieu
du formulaire, selon le type que vous spécifiez :
public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data)
{
$form = $button->getForm();
// ...
}
Lorsqu'un formulaire est soumis avec la touche Entrée, il est traité comme s'il avait été soumis avec le premier bouton.
Événement onAnchor
Lorsque vous construisez un formulaire à l'aide d'une méthode d'usine (comme createComponentRegistrationForm
),
il ne sait pas encore s'il a été soumis ou avec quelles données il a été soumis. Mais il existe des cas où nous avons besoin
de connaître les valeurs soumises, peut-être que l'aspect du formulaire en dépend, ou qu'elles sont utilisées pour des boîtes
de sélection dépendantes, etc.
Par conséquent, vous pouvez faire en sorte que le code qui construit le formulaire soit appelé lorsqu'il est ancré,
c'est-à-dire qu'il est déjà lié au présentateur et qu'il connaît ses données soumises. Nous placerons ce code dans le
tableau $onAnchor
:
$country = $form->addSelect('country', 'Country:', $this->model->getCountries());
$city = $form->addSelect('city', 'City:');
$form->onAnchor[] = function () use ($country, $city) {
// cette fonction sera appelée lorsque le formulaire connaîtra les données avec lesquelles il a été soumis
// pour que vous puissiez utiliser la méthode getValue()
$val = $country->getValue();
$city->setItems($val ? $this->model->getCities($val) : []);
};
Protection contre les vulnérabilités
Nette Framework fait de gros efforts pour être sûr et comme les formulaires sont les entrées les plus courantes de l'utilisateur, les formulaires Nette sont aussi bien impénétrables. Tout est géré de manière dynamique et transparente, rien ne doit être réglé manuellement.
En plus de protéger les formulaires contre les attaques visant des vulnérabilités bien connues comme le Cross-Site Scripting (XSS) et le Cross-Site Request Forgery (CSRF), il effectue de nombreuses petites tâches de sécurité auxquelles vous ne devez plus penser.
Par exemple, il filtre tous les caractères de contrôle des entrées et vérifie la validité de l'encodage UTF-8, de sorte que les données du formulaire sont toujours propres. Pour les cases de sélection et les listes radio, il vérifie que les éléments sélectionnés font bien partie des éléments proposés et qu'il n'y a pas eu de falsification. Nous avons déjà mentionné que pour les entrées de texte sur une seule ligne, il supprime les caractères de fin de ligne qu'un attaquant pourrait y envoyer. Pour les entrées multilignes, il normalise les caractères de fin de ligne. Et ainsi de suite.
Nette corrige pour vous les failles de sécurité dont la plupart des programmeurs ignorent l'existence.
L'attaque CSRF mentionnée consiste en ce qu'un attaquant attire la victime à visiter une page qui exécute silencieusement une requête dans le navigateur de la victime vers le serveur où la victime est actuellement connectée, et le serveur croit que la requête a été faite par la victime à volonté. Par conséquent, Nette empêche que le formulaire soit soumis via POST à partir d'un autre domaine. Si, pour une raison quelconque, vous souhaitez désactiver la protection et autoriser la soumission du formulaire à partir d'un autre domaine, utilisez :
$form->allowCrossOrigin(); // ATTENTION ! Désactive la protection !
Cette protection utilise un cookie SameSite nommé _nss
. La protection par cookie SameSite peut ne pas être
fiable à 100 %, il est donc préférable d'activer la protection par jeton :
$form->addProtection();
Il est fortement recommandé d'appliquer cette protection aux formulaires d'une partie administrative de votre application qui
modifie des données sensibles. Le framework protège contre une attaque CSRF en générant et en validant le jeton
d'authentification qui est stocké dans une session (l'argument est le message d'erreur affiché si le jeton a expiré). C'est
pourquoi il est nécessaire de démarrer une session avant d'afficher le formulaire. Dans la partie administration du site, la
session est généralement déjà démarrée, en raison du login de l'utilisateur. Sinon, démarrez la session avec la méthode
Nette\Http\Session::start()
.
Utilisation d'un formulaire dans plusieurs présentateurs
Si vous devez utiliser un formulaire dans plusieurs présentateurs, nous vous recommandons de créer une fabrique pour ce
formulaire, que vous transmettrez ensuite au présentateur. Un emplacement approprié pour une telle classe est, par exemple, le
répertoire app/Forms
.
La classe de fabrique pourrait ressembler à ceci :
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addSubmit('send', 'Log in');
return $form;
}
}
Nous demandons à la classe de produire le formulaire dans la méthode d'usine pour les composants du présentateur :
public function __construct(
private SignInFormFactory $formFactory,
) {
}
protected function createComponentSignInForm(): Form
{
$form = $this->formFactory->create();
// nous pouvons modifier le formulaire, ici par exemple nous changeons le libellé du bouton
$form['login']->setCaption('Continue');
$form->onSuccess[] = [$this, 'signInFormSubmitted']; // et ajouter le gestionnaire
return $form;
}
Le gestionnaire de traitement du formulaire peut également être fourni par la fabrique :
use Nette\Application\UI\Form;
class SignInFormFactory
{
public function create(): Form
{
$form = new Form;
$form->addText('name', 'Name:');
$form->addSubmit('send', 'Log in');
$form->onSuccess[] = function (Form $form, $data): void {
// nous traitons ici notre formulaire soumis
};
return $form;
}
}
Ainsi, nous avons une introduction rapide aux formulaires dans Nette. Essayez de regarder dans le répertoire des exemples dans la distribution pour plus d'inspiration.