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.

Si vous utilisez Nette Application et les presenters, un guide dédié vous attend en revanche : les formulaires dans les presenters.

Premier formulaire

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 :

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

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

Essayez d'ajouter aussi d'autres champs de formulaire.

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 :

$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 :

<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 :

<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] :

$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 :

$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 :

// $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 :

$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.

Rendu avec Latte

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 :

$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 :

$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.

version: 4.x