Validez des données
Sébastien et vous envoyez tout votre code, y compris les formulaires, sur les serveurs de test pour qu’Amélie puisse vous faire des retours. Quelques heures plus tard, elle vous appelle, paniquée.
Je ne comprends pas, je me suis trompée de case sur le formulaire, j’ai entré le nombre de pages dans la case du numéro ISBN, et ça a fonctionné quand même ! Pourtant il n’y a rien à voir entre les deux, le numéro ISBN doit faire treize caractères, et je n’ai mis que trois chiffres !
C’est effectivement problématique, il va falloir nous pencher sur la question. Par exemple, essayez de remplir le formulaire de livre suivant (le titre et le synopsis sont plusieurs espaces blancs) :
Vous obtenez une erreur qui vous explique que la colonne title
de la table book
ne peut être égale à null
.
Pourquoi ai-je cette erreur ?
Paradoxalement, c’est parce que ça ne posait pas de problème au formulaire d’avoir des espaces vides comme chaîne de caractères. Le numéro ISBN qui est une phrase ne lui a pas non plus posé de problème, ni le nombre de pages, ni la date d’édition. Pour le formulaire, tout s’est bien passé. Donc le controller est passé à la suite et a tenté d’enregistrer ces données en base de données. Cela confirme complètement ce qu’Amélie nous a rapporté : on peut entrer des données erronées sans problème.
Ceci est dû au fait que nous n’avons aucune validation des données dans notre application. Et comme souvent, la validation HTML n’est pas suffisante pour garantir que les données sont conformes. Il nous faut aussi une validation de notre côté.
Sébastien se frappe le front subitement.
Mais oui, j’ai complètement oublié de te parler du composant Validator et de ses contraintes de validation !
Ce composant a un but et un seul, parfaitement résumé par son nom : nous aider à valider des données. Il peut fonctionner seul de manière totalement indépendante, en mode standalone, ou bien incorporé au composant Form.
L’utilisation indépendante se fait en ajoutant un argument typé avec l’interface Symfony\Component\Validator\Validator\ValidatorInterface
à votre controller. Vous n’avez plus ensuite qu’à appeler sa méthode validate
sur l'objet à valider. Vous recevez une liste d’erreurs. Si elle est vide, c’est que tout va bien. Par exemple, sur une route fictive (ne l'ajoutez pas à votre projet) :
<?php
#[Route('/validate', name: 'app_admin_book_validate')]
public function validate(ValidatorInterface $validator): Response
{
// ...
// $book est un objet Book que nous voulons valider, peu importe sa provenance
$errors = $validator->validate($book);
if (0 < \count($errors)) {
// Gérer les erreurs
}
// ...
}
Pour ce qui est de l’utilisation dans le composant Form, vous vous en êtes déjà servi sans le savoir. Ouvrez par exemple votre Admin\BookController
et regardez sa méthode new
:
<?php
public function new(Request $request, EntityManagerInterface $manager): Response
{
$book = new Book();
$form = $this->createForm(BookType::class, $book);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$manager->persist($book);
$manager->flush();
// …
Eh oui ! $form->isValid()
n’est rien d’autre qu’un appel au composant Validator.
Dans ce cas, pourquoi on peut toujours envoyer des données qui ne sont pas valides ?
Parce que pour l’instant, nous ne lui avons pas dit ce qu’il devait valider exactement. Pour ce faire, il va falloir appliquer des contraintes de validation sur les données que nous souhaitons valider.
Les contraintes de validation sont des classes qui ne contiennent pas de logique, seulement un message d’erreur. Elles sont par contre toutes associées à un ConstraintValidator qui leur est spécifique. Concrètement, vous vous contenterez d’appliquer des contraintes sur vos objets. Puis, quand vous demandez au Validator de valider cet objet, il va lire les contraintes que vous y avez associées, et va appeler les ConstraintValidator correspondants.
Et comment on applique ces contraintes ?
C’est précisément ce que nous allons voir. Il y a deux façons de le faire : sur un objet directement, comme une entité par exemple, ou dans un FormType
. Les bonnes pratiques recommandent de mettre les contraintes en priorité sur vos objets, et de ne les mettre sur les FormType
que si vous n’avez pas le choix.
Les contraintes sont des classes qui sont quasiment toutes situées dans le namespace Symfony\Component\Validator\Constraints
, qu’on a coutume d’aliaser en tant que Assert
.
Nous ne la reprendrons pas ici, mais nous allons voir quelques exemples :
NotBlank
: la contrainte par défaut de la plupart des champs de formulaires requis, elle vérifie que le champ n’est pasnull
,false
, ni une chaîne de caractères vides (comme des espaces).Length
: permet de vérifier qu’une chaîne de caractères contient un minimum ou un maximum de caractères, ou les deux.Email
: vérifie que la valeur respecte le schéma standard d’une adresse e-mail (<première partie>@<hote>.<tld>
).EqualTo
/GreaterThan
/LowerThan
: comparaisons logiques classiques, mais qui permettent éventuellement de comparer deux propriétés entre elles.Choice
: vérifie que la valeur fait partie d’une liste de choix autorisés (à fournir à la contrainte).AtLeastOneOf
: prend en paramètre un array d’autres contraintes, et valide la donnée si au moins une de celles-ci est valide.
Contraignez la validation d’une entité
Commençons par voir la bonne pratique et ajoutons des contraintes de validation sur nos entités. Ouvrons notre entité Author. Les données à valider sont les suivantes :
L’id ne sera bien entendu pas validé. Par contre, on peut vérifier qu’un auteur est unique en base de données.
Le nom ne peut pas être vide, et devra faire au moins 10 caractères.
La date de naissance ne peut pas être vide.
La date de décès peut être vide, mais ne doit pas être inférieure à la date de naissance.
La nationalité n’a pas de validation particulière.
Cherchez dans la liste des contraintes et essayez de trouver les bonnes. Vous devriez être à peu près arrivé à ce résultat :
<?php
// …
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(['name'])]
#[ORM\Entity(repositoryClass: AuthorRepository::class)]
class Author
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[Assert\Length(min: 10)]
#[Assert\NotBlank()]
#[ORM\Column(length: 255)]
private ?string $name = null;
#[Assert\NotBlank()]
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
private ?\DateTimeImmutable $dateOfBirth = null;
#[Assert\GreaterThan(propertyPath: 'dateOfBirth')]
#[ORM\Column(type: Types::DATE_IMMUTABLE, nullable: true)]
private ?\DateTimeImmutable $dateOfDeath = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $nationality = null;
#[ORM\ManyToMany(targetEntity: Book::class, mappedBy: 'authors')]
private Collection $books;
// …
}
Ajoutez ces contraintes puis essayez de vous rendre sur votre application, sur la routehttps://127.0.0.1:8000/admin/author/new
. Remplissez le formulaire de manière erronée, comme ceci par exemple :
Soumettez le formulaire, et voyez le résultat :
Et voilà, des erreurs de validation !
Alors oui mais elles sont en anglais ces erreurs. Ça conviendra pour la médiathèque ?
Effectivement non. C’est parce que par défaut, tout Symfony est en anglais. Mais les traductions existent déjà. Ouvrez le fichier config/packages/translations.yaml
et remplacez les deux occurrences de en
par fr
:
framework:
default_locale: fr
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:
- fr
# …
Soumettez à nouveau le formulaire avec vos mauvaises données, et voilà !
Votre entité est maintenant validée, et vous pouvez être un peu plus tranquille par rapport à vos formulaires.
Contraignez la validation d’un FormType
Vous pouvez tout à fait créer des FormTypes qui ne sont pas basés sur des entités.
Dans un FormType basé sur une entité, vous pouvez malgré tout rajouter des champs qui ne correspondent pas aux propriétés de votre entité, mais il faut l'indiquer à Symfony.
Pour cela, dans le tableau d'options à passer en troisième paramètre à la fonction
$builder->add()
, ajoutez une option'mapped' => false
.Vous pouvez ensuite y ajouter des contraintes de validation, directement dans le FormType, grâce à l'option
'constraints' => []
qui prend comme valeur un tableau de vos contraintes, instanciées avec le mot-clénew
.
Exemple :
<?php
// …
use Symfony\Component\Validator\Constraints as Assert;
// …
class BookType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// …
->add('certification', CheckboxType::class, [
'mapped' => false,
'label' => "Je certifie l'exactitude des informations fournies",
'constraints' => [
new Assert\IsTrue(message: "Vous devez cocher la case pour ajouter un livre."),
],
])
// …
À vous de jouer
Contexte
Nous avons ajouté de la validation sur notre entité Author, mais il nous reste à valider les autres pour fournir à Amélie des formulaires sécurisés et fiables. Sébastien est malheureusement absent aujourd'hui, c'est donc à vous de trouver quelles contraintes appliquer sur chaque donnée en vous basant sur la documentation officielle.
Consignes
Vous allez maintenant ajouter des contraintes sur les entités Book et Editor, en respectant les points obligatoires suivants :
À chaque fois qu'une propriété est non nullable en base de données, la validation doit s'assurer qu'elle est remplie.
À chaque fois qu'une propriété représente une donnée spéciale avec un formatage codifié (comme une URL ou un numéro ISBN), ce format doit être validé grâce à une contrainte. Attention, des contraintes spécifiques existent pour beaucoup de formats spéciaux.
À chaque fois qu'une donnée doit être d'un type spécifique (comme un integer, par exemple), vous pouvez aussi valider ce type.
Pour rappel, vous n'avez pas besoin de valider les énumérations PHP qui ne peuvent de toute façon être que d'un seul type, avec des valeurs limitées.
En résumé
Le composant Validator permet d’appliquer des contraintes de validation sur nos entités et nos formulaires.
Cette validation est indépendante de la validation HTML et plus sécurisée.
La bonne pratique est de mettre vos contraintes sur vos entités.
Si c’est impossible, vous pouvez en mettre directement sur un
FormType
.La méthode
$form->isValid()
permet d’appeler le Validator sur un formulaire et sur l’entité qui y est rattachée.Le Validator peut aussi être utilisé seul si on ne valide pas un formulaire.
Et maintenant, voyons comment lire toutes ces données parfaitement validées que nous avons enregistrées en base de données !