Archives Wild Code School
Symfony

16 - Symfony : Validation

Quête 19 sur 35

Objectifs

  • ✅ Écrire une validation dans Symfony
  • ✅ Découvrir les différentes contraintes existantes
  • ✅ Exécuter la validation

Pré-requis

​ Avoir terminé la quête

Quêtes liées
title-book
Introduction

Lorsque des données proviennent d’un utilisateur (URL, soumission de formulaire…), il faut toujours vérifier l’intégrité des données côté serveur.

Ne fais jamais confiance à l’utilisateur, jamais, JAMAIS ! Et ce pour n’importe quel type d’utilisateur (visiteur, administrateur ou même toi).

Sinon, un jour ou l’autre, de manière volontaire ou par inadvertance, tu auras des problèmes (données tronquées, injection SQL…)

Résumé : les données de l’utilisateur, à qui on ne fait JAMAIS confiance, doivent TOUJOURS être vérifiées côté SERVEUR.

Sommaire

🕵️‍♀️ Validations dans Symfony, comment cela se passe-t-il ?

Dans Symfony, les vérifications des données côté serveur se font via un composant dédié à la validation (le composant Validator), installé par défaut par le website-skeleton (option --webapp de l’installateur Symfony). Ce composant permet d’écrire des validations rapidement, de gérer des messages d’erreur adaptés et est facilement extensible pour des cas plus complexes. Le composant formulaire de Symfony est directement lié à ce dernier et permet également d’afficher sans effort les éventuelles erreurs dans la vue du formulaire.

Il est possible de configurer les validations de différentes manières. Tu vas te servir à nouveau des attributs que tu commences à bien connaître puisque que tu les as déjà utilisés pour les routes ou pour Doctrine.

Ces attributs vont être écrits dans tes fichiers d’Entity (c’est assez logique, puisque les entités caractérisent tes données). Il faut tout d’abord faire un use Symfony\Component\Validator\Constraints as Assert. Pour rappel, le as permet de donner un alias (ici Assert) qui sera utilisé dans l’attribut. Cette dernière est sous la forme #[Assert\TypeDeContrainte()] et se place au-dessus d’une propriété dans la classe. Il existe un grand nombre de contraintes pré-disponibles dans Symfony, pour tester si la donnée n’est pas vide, sa longueur, son type, etc.

Par exemple, ici :

php

use Symfony\Component\Validator\Constraints as Assert;

class Category
{
    #[Assert\NotBlank]
    public $name;

    //...
}

Ici, la contrainte NotBlank permet de vérifier qu’une valeur associée à la propriété name d’un objet de type Category n’est pas "vide". En ajoutant cette contrainte, si l’utilisateur outrepasse les validations côté client, il ne passera pas la méthode ->isValid() que tu utilises dans le controller.

Info

Remarque : blank signifie ici soit une chaîne vide, soit une chaîne vide une fois un trim effectué dessus, comme un espace seul, par exemple.

Il est bien entendu possible (et souvent nécessaire) de cumuler plusieurs #[Assert]. En effet, il est également intéressant ici de tester la longueur maximale du champ. De plus, si la propriété est reliée à une table de base de données via Doctrine, les attributs de type #[ORM] seront également présents. Voici un exemple plus complet :

php

use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\ORM\Mapping as ORM;


#[ORM\Entity(repositoryClass: CategoryRepository::class)]
class Category
{
    #[ORM\Column(length: 255)]
    #[Assert\NotBlank]
    #[Assert\Length(max: 255)]
    private $name;
}

Info

Depuis Symfony 4.4, si aucune validation n’est définie sur une propriété mais que celle-ci possède également un attribut #[ORM], alors le composant Validator s’appuie par défaut sur ce qui est défini pour Doctrine et applique les validations correspondantes, ce qui évite un certain nombre de "répétitions" assez triviales entre ce qui est défini en #[Assert] et en #[ORM]. Il faut activer spécifiquement cette fonctionnalité en ajoutant au dessus du nom de la classe, l'attribute * #[Assert\EnableAutoMapping]. Une fois cela fait, pour reprendre l’exemple ci-dessus, puisqu’on définit une longueur de 255 caractères pour le champ (via le #[ORM\Column] et que ce dernier n’est pas défini comme nullable, le Validator ajoute tout seul que la contrainte sur la longueur de la chaîne correspond et sur le fait que celle-ci ne soit pas nulle. Tu peux en apprendre plus à ce sujet dans cette documentation. Fais cependant attention, car il n’y a qu’un petit nombre de contraintes qui peuvent ainsi être automatiquement validées. Pour tout le reste, il faudra toujours le définir manuellement.

Les contraintes possèdent également des paramètres (optionnels ou non, en fonction des cas). Par exemple, NotBlank peut rester sans paramètre, tandis que Length demandera au moins de lui indiquer une taille minimum ou maximum.

Chaque contrainte affiche un message d’erreur par défaut (en anglais). Il est parfois utile de le personnaliser. Pour ce faire, il faut utiliser le paramètre correspondant. Il est possible d’y afficher la valeur actuelle erronée, via le placeholder {{ value }}, ainsi que la valeur ou contrainte attendue (si cela est pertinent), via {{ limit }}.

php
#[ORM\Column(length: 255)]
#[Assert\NotBlank(message: 'Ne me laisse pas tout vide')]
#[Assert\Length(
    max: 255,
    maxMessage: 'La catégorie saisie {{ value }} est trop longue, elle ne devrait pas dépasser {{ limit }} caractères',
)]
private $name;

Info

Symfony intègre par défaut des traductions dans différentes langues. Si tu veux que tous les messages d'erreur soient automatiquement en français (et non plus en anglais), tu peux modifier la langue "par défaut" de Symfony dans le fichier config/packages/translation.yaml en modifiant la ligne default_locale: en en default_locale: fr.

Chaque contrainte a ses propres options. Length, par exemple, permet de configurer un message d’erreur différent pour un dépassement de la taille minimale ou de la taille maximale. Il faut se référer plus en détail à la documentation de chaque contrainte pour correctement les paramétrer.

↗
Documentation officielle sur la validation

C’est la référence, parcours-la au fur et à mesure de la quête.

symfony.com
↗
La contrainte Length en détail

Exemple de la documentation d’une contrainte en particulier : tu vois que le nombre d’options peut être important. Ce n’est pas la peine de toutes les apprendre par cœur, il faut par contre que tu sois capable de trouver rapidement et efficacement les informations dans la documentation officielle.

symfony.com
↗
Validating Objects

[OPTIONNEL] The Symfony validator reuses Doctrine metadata to perform some basic validation tasks

symfony.com

🚷 Les différentes contraintes

Tu sais maintenant comment configurer les contraintes. Il en existe actuellement une cinquantaine, et Symfony en ajoute régulièrement de nouvelles. Elles te permettent de répondre à la grande majorité des besoins en termes de vérification de données. Sache juste qu’il est également possible de configurer des contraintes personnalisées plus complexes, si besoin, même si cela ne sera pas détaillé ici.

Ces contraintes sont toutes listées sur la documentation officielle. Elles sont organisées en plusieurs catégories, voici quelques exemples (non exhaustifs) :

  • Contraintes basiques : vérification d’existence d’une valeur (null ou vide), vérification du type d’une valeur (string, int…)

  • Contraintes sur les chaînes de caractères : tests sur les longueurs, vérification sur les formats (email, url, adresse IP) ou personnalisée via la contrainte Regex permettant de définir une expression régulière.

  • Contraintes de comparaison : vérification si la valeur saisie est égale, différente, plus grande ou plus petite qu’une valeur attendue.

  • Contraintes de Date : test si la valeur est bien de format date (ou datetime ou time).

  • Contraintes de Choix : test si la valeur fait partie d’une liste préétablie par le développeur, ou de listes préconfigurées dans Symfony (liste des pays, des langues, des devises, etc.) correspondant à certains standards ISO.

  • Contraintes sur les fichiers : utile pour l’upload notamment. Sert à vérifier si un fichier a la bonne extension, ne dépasse pas la taille requise. Il existe même des contraintes propres aux fichiers images, sur la taille de l’image en pixels, son ratio, etc.

  • Contraintes financières : vérifie si la valeur correspond bien à un format de code BIC, de carte bleue, IBAN, etc. Utile pour du paiement en ligne, par exemple.

  • Autres contraintes : vérifier l’unicité d’une valeur, créer des contraintes personnalisées, etc.

Attention

Tu as l’embarras du choix ! A toi de les utiliser de manière pertinente. Par exemple, lorsque tu associes ta validation à l’ajout en base de données dans un champ lastname de type VARCHAR(150) non nullable, il faut obligatoirement tester si la valeur est NotBlank et si la Length maximale est bien 150. Tester systématiquement une longueur minimale supérieure à 2 peut être tentant, mais ce n’est pas forcément une bonne idée, car il existe des noms de familles faisant seulement un caractère ! Et entrer une chaîne de 1 caractère, même si cela paraît peu probable, ne provoquera pas de problème au niveau de ta base. Il faut que tes validations assurent l’intégrité des données (afin d’éviter tout bug), et non qu’elles soient bloquantes pour l’utilisateur quand cela n’est pas absolument nécessaire. Il faut donc bien réfléchir au type de contraintes à imposer et aux paramètres à utiliser pour chacune d’entre elles.

↗
Documentation officielle

listant toutes les contraintes disponibles

symfony.com
↗
Comment créer une contrainte personnalisée

Cette ressource pourra t’être utile si tu as besoin de créer une contrainte plus complexe.

symfony.com

Lancer la validation

Le composant formulaire de Symfony est lié au composant validator. De ce fait, il est extrêmement facile de valider un formulaire en fonction des contraintes écrites dans l’Entity. Pour cela, il suffit d’utiliser la méthode isValid() du formulaire.

php
// $form initialization

if ($form->isSubmitted() && $form->isValid()) {
    // handle data, in example, an insert into database
    // redirection
}

// form rendering

La méthode isValid() retourne un booléen. Elle doit être appelée au moment de la soumission du formulaire et juste après $form->isSubmitted(). En effet, il est logique de ne valider les données d’un formulaire qu’une fois celles-ci soumises.

Sur l’exemple de code ci-dessus, si le formulaire n’est pas valide, ni l’insertion en base de données, ni la redirection ne sont effectuées et le formulaire est donc ré-affiché. Cependant, dans ce cas, les erreurs vont être récupérées automatiquement dans le formulaire et apparaître à l’écran (l’helper Twig {{ form_errors() }} permet d’en personnaliser l’affichage).

Info

Remarque : l’ajout de certaines contraintes va directement affecter l’affichage d’un formulaire relié à cette entité. Par exemple, si tu ajoutes une contrainte Length avec un maximum de 10 caractères sur une propriété donnée, un attribut maxlength=10 est ajouté automatiquement à l’input correspondant dans le formulaire. C’est extrêmement pratique, car cela permet d’avoir automatiquement une cohérence entre les vérifications d’erreur côté client et côté serveur.

Pour résumer, lorsque tu crées un formulaire relié à une entité, il suffit de configurer correctement les contraintes dans la classe d’entité, et l’appel de $form->isValid() permet d’assurer la cohérence des données saisies par l’utilisateur, et d’afficher les éventuelles erreurs.

↗
[OPTIONNEL] How to Apply only a Subset of all Your Validation Constraints (Validation Groups)

La lecture de cette ressource est optionnelle. Dans certains cas, une entité peut avoir besoin d’être partiellement validée à un moment donné. Par exemple, à la création d’une commande par un utilisateur, la date d’expédition est vide. Par contre, si la même commande passe plus tard à un statut "envoyée", la propriété date d’expédition devient alors obligatoire. Ce type de validation partielle, conditionnée à un statut par exemple, n’est pas rare. Symfony propose donc un mécanisme permettant de réaliser cela facilement.

symfony.com

💪 Challenge

Validation d’une série

Dans la quête, tu as vu des exemples pour configurer une validation sur les catégories. Maintenant, il faut que tu prennes le reflexe d’ajouter systématiquement des contraintes de validation sur tous les champs de toutes tes entités qui en ont besoin. Dans le cadre du challenge, tu vas configurer ton entité Program afin qu’elle suive ces règles simples de validation :

  • le champ title ne doit pas être vide et doit faire moins de 255 caractères
  • le champ title doit être unique. Si le titre existe déjà en base de données, le message "ce titre existe déjà" doit apparaître. Regarde du côté de la contrainte UniqueEntity (qui a la particularité de se configurer au niveau de la classe et non d’une propriété)
  • le champ synopsis de la série ne doit pas être vide
  • [BONUS 🤩 ] le champ synopsis ne doit pas contenir la chaîne "plus belle la vie", sinon le message d’erreur "On parle de vraies séries ici" apparaît. Regarde en détail les options de la contrainte "Regex" pour t’aider.

Ton formulaire d’ajout de programme doit effectuer les vérifications de validation. S’il y a des erreurs, les messages correspondants doivent s’afficher.

Poste une branche "validation" sur ton repository Github et prends une vidéo montrant le bon fonctionnement de ces validations (dans lequel on verra les messages d’erreur appropriés s’afficher) et poste un lien vers la vidéo dans le README.

🧐 Critères de validation

Info

Pour tester les cas d’erreur côté serveur, il faut enlever les attributs bloquants dans le HTML du formulaire (les maxLength et autre required) ou utiliser l’attribut « novalidate » sur la balise form afin de "forcer" l’erreur. Utilise la console du navigateur pour faire cela 😉

  • Une erreur appropriée apparaît au niveau de ton formulaire si un titre vide ou un titre supérieur à 255 caractères est envoyé au serveur.
  • Le message "ce titre existe déjà" doit apparaître dans ton formulaire, si le titre saisi existe déjà pour une autre série
  • Une erreur appropriée apparaît au niveau de ton formulaire si un synopsis vide est envoyé au serveur.
  • [Bonus] Si le summary de la série contient le texte "plus belle la vie", le message d’erreur "On parle de vraies séries ici" apparaît à la place.
  • Le code est disponible sur Github.