Formulaires utilisés de manière autonome

Les formulaires Nette facilitent considérablement la création et le traitement des formulaires Web. Vous pouvez les utiliser dans vos applications de manière complètement autonome, sans le reste du framework, ce que nous allons démontrer dans ce chapitre.

Cependant, si vous utilisez Nette Application et des présentateurs, il existe un guide pour vous : forms in presenters.

Premier formulaire

Nous allons essayer d'écrire un formulaire d'inscription simple. Son code ressemblera à ceci (code complet) :

use Nette\Forms\Form;

$form = new Form;
$form->addText('name', 'Name:');
$form->addPassword('password', 'Password:');
$form->addSubmit('send', 'Sign up');

Et rendons-le :

$form->render();

et le résultat devrait ressembler à ceci :

Le formulaire est un objet de la classe Nette\Forms\Form (la classe Nette\Application\UI\Form est utilisée dans les présentateurs). Nous y avons ajouté les contrôles nom, mot de passe et bouton d'envoi.

Maintenant nous allons relancer le formulaire. En demandant à $form->isSuccess(), nous saurons si le formulaire a été soumis et s'il a été rempli de manière valide. Si c'est le cas, nous allons vider les données. Après la définition du formulaire, nous ajouterons :

if ($form->isSuccess()) {
	echo 'Le formulaire a été rempli et soumis correctement';
	$data = $form->getValues();
	// $data->name contient le nom
	// $data->password contient le mot de passe
	var_dump($data);
}

La méthode getValues() retourne les données envoyées sous la forme d'un objet ArrayHash. Nous verrons plus tard comment modifier cela. La variable $data contient les clé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.');

Après avoir traité le formulaire, nous allons rediriger vers la page suivante. Cela permet d'éviter que le formulaire soit involontairement soumis à nouveau en cliquant sur le bouton refresh, back, ou en déplaçant l'historique du navigateur.

Par défaut, le formulaire est envoyé à la même page en utilisant la méthode POST. Les deux méthodes peuvent être modifiées :

$form->setAction('/submit.php');
$form->setMethod('GET');

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

Essayez d'ajouter d'autres contrôles de formulaire.

Accès aux contrôles

Le formulaire et ses contrôles individuels sont appelés des composants. Ils créent une arborescence de composants, dont la racine est le formulaire. Vous pouvez accéder aux contrôles individuels comme suit :

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

$button = $form->getComponent('send');
// syntaxe alternative: $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é ici, 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 enter a 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. Ajoutez-le à la page :

<script src="https://unpkg.com/nette-forms@3/src/assets/netteForms.js"></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 au traitement des données du formulaire. La méthode getValues() a renvoyé les données soumises 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 lorsque nous travaillerons 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;
}

A partir de PHP 8.0, vous pouvez utiliser cette notation élégante qui utilise un constructeur :

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

Comment dire à Nette de nous 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 le nom de la classe ou de l'objet à hydrater comme paramètre :

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

On peut également spécifier un 'array' comme paramètre, et les données reviennent alors sous forme de tableau.

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 devons généralement distinguer lequel a été pressé. La méthode isSubmittedBy() du bouton nous renvoie cette information :

$form->addSubmit('save', 'Save');
$form->addSubmit('delete', 'Delete');

if ($form->isSuccess()) {
	if ($form['save']->isSubmittedBy()) {
		// ...
	}

	if ($form['delete']->isSubmittedBy()) {
		// ...
	}
}

N'omettez pas le $form->isSuccess() pour vérifier la validité des données.

Lorsqu'un formulaire est soumis avec la touche Entrée, il est traité comme s'il avait été soumis avec le premier bouton.

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 impénétrables que possible.

En plus de protéger les formulaires contre les attaques de 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 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 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. Par conséquent, créez un formulaire avant d'envoyer la première sortie afin que le cookie puisse être envoyé.

La protection par cookie SameSite peut ne pas être fiable à 100%, c'est pourquoi il est 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().

Ainsi, nous avons une introduction rapide aux formulaires dans Nette. Essayez de regarder dans le répertoire d'exemples de la distribution pour plus d'inspiration.

version: 4.0