Formulaires dans les presenters

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

Si vous vous demandez comment les utiliser de manière totalement autonome sans le reste du framework, le guide pour une utilisation autonome est fait pour vous.

Premier formulaire

Essayons d'écrire un formulaire d'inscription simple. 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 comme ceci :

Le formulaire dans le 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 éléments appelés nom, mot de passe et un bouton d'envoi. Et enfin, la ligne avec $form->onSuccess indique qu'après l'envoi et la validation réussie, la méthode $this->formSucceeded() doit être appelée.

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

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

Et dans le template, nous affichons le formulaire avec la balise {control} :

<h1>Inscription</h1>

{control registrationForm}

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

Et maintenant, vous vous dites 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 en effet un mécanisme rafraîchissant que nous appelons le style Hollywood. Au lieu que vous, en tant que développeur, deviez constamment demander si quelque chose s'est passé (“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 “une fois que le formulaire sera valablement rempli, appelle cette méthode” et vous lui laissez le reste du travail. Si vous programmez en JavaScript, vous connaissez bien 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 précisément ainsi qu'est construit le code du presenter ci-dessus. Le tableau $form->onSuccess représente une liste de callbacks PHP que Nette appellera au moment où le formulaire est envoyé et correctement rempli (c'est-à-dire qu'il est valide). Dans le cadre du cycle de vie du presenter, il s'agit d'un signal, ils sont donc appelés après la méthode action* et avant la méthode render*. Et à chaque callback, il passe comme premier paramètre le formulaire lui-même et comme second les données envoyées sous forme d'objet ArrayHash. Vous pouvez omettre le premier paramètre si vous n'avez pas besoin de l'objet formulaire. Et le second paramètre peut être plus malin, mais nous en reparlerons plus tard.

L'objet $data contient les clés name et password avec les informations que l'utilisateur a remplies. Habituellement, nous envoyons directement les données pour un traitement ultérieur, ce qui peut être par exemple une insertion dans la base de données. Cependant, une erreur peut survenir pendant le traitement, par exemple le nom d'utilisateur est déjà pris. Dans ce cas, nous renvoyons l'erreur au formulaire à l'aide de addError() et le laissons se réafficher, avec le message d'erreur.

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

En plus de onSuccess, il existe également onSubmit : les callbacks sont toujours appelés après l'envoi du formulaire, même s'il n'est pas correctement rempli. Et ensuite onError : les callbacks ne sont appelés que si l'envoi n'est pas valide. Ils sont même appelés si, dans onSuccess ou onSubmit, nous invalidons le formulaire avec addError().

Après le traitement du formulaire, nous redirigeons vers la page suivante. Cela évite la ré-soumission involontaire du formulaire par le bouton actualiser, retour ou par le déplacement dans l'historique du navigateur.

Essayez d'ajouter d'autres éléments de formulaire.

Accès aux éléments

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

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

Les éléments 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 éléments sont supprimés avec unset :

unset($form['name']);

Règles de validation

Le mot valide a été mentionné, 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 s'affichera si l'utilisateur ne remplit pas le nom. Si l'argument n'est pas fourni, le message d'erreur par défaut sera utilisé.

$form->addText('name', 'Nom :')
	->setRequired('Veuillez entrer un nom');

Essayez d'envoyer le formulaire sans remplir le nom et vous verrez que le message d'erreur s'affichera et que le navigateur ou le serveur le refusera jusqu'à ce que vous remplissiez le champ.

En même temps, vous ne tromperez pas le système en tapant, par exemple, uniquement des espaces dans le champ. Non. Nette supprime automatiquement les espaces de début et de fin. Essayez. C'est quelque chose que vous devriez toujours faire avec chaque input sur une seule ligne, mais on l'oublie souvent. Nette le fait automatiquement. (Vous pouvez essayer de tromper le formulaire et envoyer une chaîne multiligne comme nom. Même ici, Nette ne se laissera pas berner et changera les sauts de ligne en espaces.)

Le formulaire est toujours validé côté serveur, mais une validation JavaScript est également générée, qui s'exécute instantanément et l'utilisateur est informé de l'erreur immédiatement, sans avoir besoin d'envoyer le formulaire au serveur. C'est le script netteForms.js qui s'en charge. Insérez-le dans le template de layout :

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

Si vous regardez le code source de la page avec le formulaire, vous remarquerez peut-être que Nette insère les éléments requis dans des éléments avec la classe CSS required. Essayez d'ajouter la feuille de style suivante au template et l'étiquette “Nom” sera rouge. Nous marquons ainsi élégamment les éléments requis 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 second est à nouveau le texte du message d'erreur et il peut y avoir un argument de règle de validation. Qu'est-ce que cela signifie ?

Nous étendons le formulaire avec un nouveau champ facultatif “âge”, qui doit être un entier (addInteger()) et de plus dans une plage autorisée ($form::Range). Et c'est ici que nous utilisons le troisième paramètre de la méthode addRule(), avec lequel nous passons la plage requise au validateur sous forme de paire [de, à] :

$form->addInteger('age', 'Âge :')
	->addRule($form::Range, 'L\'âge doit être compris entre 18 et 120', [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.

Il y a ici place pour un petit refactoring. Dans le message d'erreur et dans le troisième paramètre, les nombres sont indiqués en double, ce qui n'est pas idéal. Si nous créions des formulaires multilingues et que le message contenant des nombres était traduit dans plusieurs langues, un éventuel changement de valeurs serait compliqué. Pour cette raison, il est possible d'utiliser les placeholders %d et Nette complétera les valeurs :

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

Revenons à l'élément password, que nous rendrons également obligatoire et vérifierons la longueur minimale du mot de passe ($form::MinLength), en utilisant à nouveau le placeholder :

$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 champ passwordVerify, où l'utilisateur saisira à nouveau le mot de passe, pour vérification. À l'aide des règles de validation, nous vérifions si les deux mots de passe sont identiques ($form::Equal). Et comme paramètre, nous donnons une référence au premier mot de passe en utilisant des crochets :

$form->addPassword('passwordVerify', 'Mot de passe pour vérification :')
	->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é l'élément dont la valeur ne nous importe pas vraiment et qui n'existe que pour la validation. La valeur n'est pas transmise à $data.

Nous avons ainsi un formulaire entièrement fonctionnel avec validation en PHP et JavaScript. Les capacités de validation de Nette sont beaucoup plus larges, on peut créer des conditions, afficher et masquer des parties de la page en fonction d'elles, 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 éléments de formulaire :

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

Il est souvent utile de définir les valeurs par défaut de tous les éléments en même temps. Par exemple, lorsque le formulaire sert à modifier des enregistrements. Nous lisons l'enregistrement de 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 éléments.

Rendu du formulaire

Par défaut, le formulaire est rendu sous forme de tableau. Les éléments individuels respectent la règle d'accessibilité de base – toutes les étiquettes sont écrites comme <label> et liées à l'élément de formulaire correspondant. En cliquant sur l'étiquette, le curseur apparaît automatiquement dans le champ de formulaire.

Nous pouvons définir des attributs HTML arbitraires pour chaque élément. Par exemple, ajouter un placeholder :

$form->addInteger('age', 'Âge :')
	->setHtmlAttribute('placeholder', 'Veuillez remplir l\'âge');

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

Mappage sur les classes

Revenons à la méthode formSucceeded(), qui reçoit dans le deuxième paramètre $data les données envoyées sous forme d'objet ArrayHash. Comme il s'agit d'une classe générique, quelque chose comme stdClass, il nous manquera un certain confort lors de son utilisation, comme la suggestion des 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 éléments individuels. Par exemple :

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

Alternativement, vous pouvez 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 également être des enums et elles seront automatiquement mappées.

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

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

Comme type, on peut également indiquer array et alors les données seront passées sous forme de tableau.

De la même manière, on peut utiliser la méthode getValues(), à laquelle on passe le nom de la classe ou l'objet à hydrater comme paramètre :

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

Si les formulaires forment 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 mappage reconnaîtra alors à partir du type de la propriété $person qu'il doit mapper le conteneur sur la classe PersonFormData. Si la propriété contenait un tableau de conteneurs, indiquez le type array et passez la classe pour le mappage directement au conteneur :

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

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

Plusieurs boutons

Si le formulaire a plus d'un bouton, nous devons généralement distinguer lequel a été pressé. Nous pouvons créer notre propre fonction de gestion pour chaque bouton. Nous la définissons comme handler pour l'événement onClick :

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

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

Ces handlers ne sont appelés que dans le cas d'un formulaire valablement rempli, tout comme dans le cas de l'événement onSuccess. La différence est que comme premier paramètre, au lieu du formulaire, le bouton d'envoi peut être passé, cela dépend du type que vous indiquez :

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

Lorsque le formulaire est soumis avec la touche Entrée, il est considéré comme s'il avait été soumis avec le premier bouton.

Événement onAnchor

Lorsque nous assemblons le formulaire dans la méthode factory (comme par exemple createComponentRegistrationForm), celui-ci ne sait pas encore s'il a été soumis, ni avec quelles données. Mais il y a des cas où nous avons besoin de connaître les valeurs soumises, par exemple si la forme ultérieure du formulaire en dépend, ou si nous en avons besoin pour des selectbox dépendants, etc.

Une partie du code assemblant le formulaire peut donc être appelée uniquement au moment où il est dit ancré, c'est-à-dire qu'il est déjà connecté au presenter et connaît ses données soumises. Nous passons un tel code dans le tableau $onAnchor :

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

$form->onAnchor[] = function () use ($country, $city) {
	// cette fonction sera appelée seulement lorsque le formulaire saura s'il a été soumis et avec quelles données
	// on peut 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 bonne sécurisation des formulaires. Il le fait de manière totalement transparente et ne nécessite aucune configuration manuelle.

En plus de protéger les formulaires contre les attaques Cross Site Scripting (XSS) et Cross-Site Request Forgery (CSRF), il effectue de nombreuses petites sécurisations auxquelles vous n'avez 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 seront toujours propres. Pour les select box et les radio lists, il vérifie que les éléments sélectionnés étaient bien parmi ceux 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 aurait pu envoyer. Pour les entrées multilignes, il normalise les caractères de fin de ligne. Et ainsi de suite.

Nette résout pour vous les risques de sécurité dont de nombreux programmeurs ignorent même l'existence.

L'attaque CSRF mentionnée consiste en ce qu'un attaquant attire la victime sur une page qui exécute discrètement dans le navigateur de la victime une requête vers le serveur sur lequel la victime est connectée, et le serveur croit que la requête a été exécutée par la victime de sa propre volonté. C'est pourquoi Nette empêche l'envoi d'un formulaire POST depuis un autre domaine. Si pour une raison quelconque vous souhaitez désactiver la protection et autoriser l'envoi du formulaire depuis un autre domaine, utilisez :

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

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

$form->addProtection();

Nous recommandons de protéger ainsi les formulaires dans la partie administrative du site web, qui modifient des données sensibles dans l'application. Le framework se défend contre l'attaque CSRF en générant et en vérifiant un jeton d'autorisation qui est stocké dans la session. Par conséquent, il est nécessaire d'avoir une session ouverte avant d'afficher le formulaire. Dans la partie administrative du site web, la session est généralement déjà démarrée en raison de la connexion de l'utilisateur. Sinon, démarrez la session avec la méthode Nette\Http\Session::start().

Même formulaire dans plusieurs presenters

Si vous avez besoin d'utiliser un même formulaire dans plusieurs presenters, nous vous recommandons de créer une factory pour celui-ci, que vous passerez ensuite au presenter via l'injection de dépendances. Un emplacement approprié pour une telle classe est par exemple le répertoire app/Forms.

La classe factory peut 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 fabriquer le formulaire dans la méthode factory pour les composants dans le 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 l'étiquette sur le bouton
	$form['send']->setCaption('Continuer');
	$form->onSuccess[] = [$this, 'signInFormSuceeded']; // et ajoutons un handler
	return $form;
}

Le handler pour le traitement du formulaire peut également être fourni par la factory :

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 {
			// ici nous effectuons le traitement du formulaire
		};
		return $form;
	}
}

Voilà, nous avons eu une introduction rapide aux formulaires dans Nette. Essayez de regarder également dans le répertoire exemples de la distribution, où vous trouverez plus d'inspiration.

version: 4.0