Au chapitre précédent nous avons vu comment créer des formulaires avec Symfony2. Mais qui dit formulaire dit vérification des données rentrées ! Symfony2 contient un composant Validator
qui, comme son nom l'indique, s'occupe de gérer tout cela. Attaquons-le donc !
- Pourquoi valider des données ?
- Définir les règles de validation
- Déclencher la validation
- Encore plus de règles de validation
- Valider selon nos propres contraintes
Pourquoi valider des données ?
Never trust user input
Ce chapitre introduit la validation des objets avec le composant Validator
de Symfony2. En effet, c'est normalement un des premiers réflexes à avoir lorsque l'on demande à l'utilisateur de remplir des informations : vérifier ce qu'il a rempli ! Il faut toujours considérer que soit il ne sait pas remplir un formulaire, soit c'est un petit malin qui essaie de trouver la faille. Bref, ne jamais faire confiance à ce que l'utilisateur vous donne (« never trust user input » en anglais).
La validation et les formulaires sont bien sûr liés, dans le sens où les formulaires ont besoin de la validation. Mais l'inverse n'est pas vrai ! Dans Symfony2, le validator
est un service indépendant et n'a nul besoin d'un formulaire pour exister. Ayez-le en tête, avec le validator
, on peut valider n'importe quel objet, entité ou non, le tout sans avoir besoin de formulaire.
L'intérêt de la validation
L'objectif de ce chapitre est donc d'apprendre à définir qu'une entité est valide ou pas. Plus concrètement, il nous faudra établir des règles précises pour dire que tel attribut (le nom d'utilisateur par exemple) doit faire 3 caractères minimum, que tel autre attribut (l'âge par exemple) doit être compris entre 7 et 77 ans, etc. En vérifiant les données avant de les enregistrer en base de données, on est certain d'avoir une base de données cohérente, en laquelle on peut avoir confiance !
La théorie de la validation
La théorie, très simple, est la suivante. On définit des règles de validation que l'on va rattacher à une classe. Puis on fait appel à un service extérieur pour venir lire un objet (instance de ladite classe) et ses règles, et définir si oui ou non l'objet en question respecte ces règles. Simple et logique !
Définir les règles de validation
Les différentes formes de règles
Pour définir ces règles de validation, ou contraintes, il existe deux moyens :
- Le premier est d'utiliser les annotations, vous les connaissez maintenant. Leur avantage est d'être situées au sein même de l'entité, et juste à côté des annotations du mapping Doctrine2 si vous les utilisez également pour votre mapping.
- Le deuxième est d'utiliser le YAML, XML ou PHP. Vous placez donc vos règles de validation hors de l'entité, dans un fichier séparé.
Les deux moyens sont parfaitement équivalents en termes de fonctionnalités. Le choix se fait donc selon vos préférences. Dans la suite du cours, j'utiliserai les annotations, car je trouve extrêmement pratique de centraliser règles de validation et mapping Doctrine au même endroit. Facile à lire et à modifier.
Définir les règles de validation
Préparation
Nous allons prendre l'exemple de notre entité Article
pour construire nos règles. La première étape consiste à déterminer les règles que nous voulons avec des mots, comme ceci :
- La date doit être une date valide ;
- Le titre doit faire au moins 10 caractères de long ;
- Le contenu ne doit pas être vide ;
- L'auteur doit faire au moins 2 caractères de long ;
- L'image liée doit être valide selon les règles attachées à l'objet
Image
.
À partir de cela, nous pourrons convertir ces mots en annotations.
Annotations
Pour définir les règles de validation, nous allons donc utiliser les annotations. La première chose à savoir est le namespace des annotations à utiliser. Souvenez-vous, pour le mapping Doctrine c'était @ORM
, ici nous allons utiliser @Assert
, donc le namespace complet est le suivant :
1 2 | <?php use Symfony\Component\Validator\Constraints as Assert; |
Ce use
est à rajouter au début de l'objet que l'on va valider, notre entité Article
en l'occurrence. En réalité, vous pouvez définir l'alias à autre chose qu'Assert
. Mais c'est une convention qui s'est installée, donc autant la suivre pour avoir un code plus facilement lisible pour les autres développeurs.
Ensuite, il ne reste plus qu'à ajouter les annotations pour traduire les règles que l'on vient de lister. Sans plus attendre, voici donc la syntaxe à respecter. Exemple avec notre objet Article
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | <?php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; // N'oubliez pas de rajouter ce « use », il définit le namespace pour les annotations de validation use Symfony\Component\Validator\Constraints as Assert; use Doctrine\Common\Collections\ArrayCollection; /** * Sdz\BlogBundle\Entity\Article * * @ORM\Table() * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository") */ class Article { /** * @var integer $id * * @ORM\Column(name="id", type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var date $date * * @ORM\Column(name="date", type="date") * @Assert\DateTime() */ private $date; /** * @var string $titre * * @ORM\Column(name="titre", type="string", length=255) * @Assert\MinLength(10) */ private $titre; /** * @var text $contenu * * @ORM\Column(name="contenu", type="text") * @Assert\NotBlank() */ private $contenu; /** * @var string $auteur * * @ORM\Column(name="auteur", type="string", length=255) * @Assert\MinLength(2) */ private $auteur; /** * @ORM\OneToOne(targetEntity="Sdz\BlogBundle\Entity\Image", cascade={"persist"}) * @Assert\Valid() */ private $image; // … } |
Vraiment pratique d'avoir les métadonnées Doctrine et les règles de validation au même endroit, n'est-ce pas ?
J'ai pris l'exemple ici d'une entité, car ce sera souvent le cas. Mais n'oubliez pas que vous pouvez mettre des règles de validation sur n'importe quel objet, qui n'est pas forcément une entité.
Syntaxe
Revenons un peu sur les annotations que l'on a ajoutées. Nous avons utilisé la forme simple, qui est construite comme ceci :
1 | @Assert\Contrainte(valeur de l'option par défaut) |
Avec :
- La
Contrainte
, qui peut être, comme vous l'avez vu,NotBlank
ouMinLength
, etc. Nous voyons plus loin toutes les contraintes possibles. - La
Valeur
entre parenthèses, qui est la valeur de l'option par défaut. En effet chaque contrainte a plusieurs options, dont une par défaut souvent intuitive. Par exemple, l'option par défaut deMinLength
(« longueur minimale » en français) est évidemment la valeur de la longueur minimale que l'on veut appliquer, 10 pour le titre.
Mais on peut aussi utiliser la forme étendue qui permet de personnaliser la valeur de plusieurs options en même temps, comme ceci :
1 | @Assert\Contrainte(option1="valeur1", option2="valeur2", …) |
Les différentes options diffèrent d'une contrainte à une autre, mais voici un exemple avec la contrainte MinLength
:
1 | @Assert\MinLength(limit=10, message="Le titre doit faire au moins {{ limit }} caractères.") |
Oui, vous pouvez utilisez {{ limit }}
, qui est en fait le nom de l'option que vous voulez faire apparaître dans le message.
Bien entendu, vous pouvez mettre plusieurs contraintes sur un même attribut. Par exemple pour un attribut numérique telle une note, on pourrait mettre les deux contraintes suivantes :
1 2 3 4 5 6 | <?php /** * @Assert\Min(0) * @Assert\Max(20) */ private $note |
Vous savez tout ! Il n'y a rien de plus à connaître sur les annotations. À part les contraintes existantes et leurs options, évidemment.
Liste des contraintes existantes
Voici un tableau qui regroupe la plupart des contraintes, à avoir sous la main lorsque vous définissez vos règles de validation ! Elles sont bien entendu toutes documentées, donc n'hésitez pas à vous référer à la documentation officielle pour toute information supplémentaire.
Toutes les contraintes disposent de l'option message
, qui est le message à afficher lorsque la contrainte est violée. Je n'ai pas répété cette option dans les tableaux suivants, mais sachez qu'elle existe bien à chaque fois.
Contraintes de base :
Contrainte |
Rôle |
Options |
---|---|---|
La contrainte |
- |
|
La contrainte |
- |
|
La contrainte |
|
Contraintes sur des chaînes de caractères :
Contrainte |
Rôle |
Options |
---|---|---|
La contrainte |
|
|
La contrainte |
|
|
La contrainte |
|
|
La contrainte |
|
|
La contrainte |
|
|
La contrainte |
- |
|
La contrainte |
- |
|
La contrainte |
- |
Contraintes sur les nombres :
Contrainte |
Rôle |
Options |
---|---|---|
La contrainte |
|
Contraintes sur les dates :
Contrainte |
Rôle |
Options |
---|---|---|
La contrainte |
- |
|
La contrainte |
- |
|
La contrainte |
- |
Contraintes sur les fichiers :
Contrainte |
Rôle |
Options |
---|---|---|
La contrainte |
|
|
La contrainte |
|
Les noms de contraintes sont sensibles à la casse. Cela signifie que la contrainte DateTime
existe, mais que Datetime
ou datetime
n'existent pas ! Soyez attentifs à ce détail pour éviter des erreurs inattendues.
Déclencher la validation
Le service Validator
Comme je l'ai dit précédemment, ce n'est pas l'objet qui se valide tout seul, on doit déclencher la validation nous-mêmes. Ainsi, vous pouvez tout à fait assigner une valeur non valide à un attribut sans qu'aucune erreur ne se déclenche. Par exemple, vous pouvez faire <?php $article->setTitre('abc')
alors que ce titre a moins de 10 caractères, il est invalide.
Pour valider l'objet, on passe par un acteur externe : le service validator
. Ce service s'obtient comme n'importe quel autre service :
1 2 3 4 | <?php // Depuis un contrôleur $validator = $this->get('validator'); |
Ensuite, on doit demander à ce service de valider notre objet. Cela se fait grâce à la méthode validate
du service. Cette méthode retourne un tableau qui est soit vide si l'objet est valide, soit rempli des différentes erreurs lorsque l'objet n'est pas valide. Pour bien comprendre, exécutez cette méthode dans un contrôleur :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?php // Depuis un contrôleur // … public function testAction() { $article = new Article; $article->setDate(new \Datetime()); // Champ « date » OK $article->setTitre('abc'); // Champ « titre » incorrect : moins de 10 caractères //$article->setContenu('blabla'); // Champ « contenu » incorrect : on ne le définit pas $article->setAuteur('A'); // Champ « auteur » incorrect : moins de 2 caractères // On récupère le service validator $validator = $this->get('validator'); // On déclenche la validation $liste_erreurs = $validator->validate($article); // Si le tableau n'est pas vide, on affiche les erreurs if(count($liste_erreurs) > 0) { return new Response(print_r($liste_erreurs, true)); } else { return new Response("L'article est valide !"); } } |
Vous pouvez vous amuser avec le contenu de l'entité Article
pour voir comment réagit le validateur.
La validation automatique sur les formulaires
Vous devez savoir qu'en pratique on ne se servira que très peu du service validator
nous-mêmes. En effet, le formulaire de Symfony2 le fait à notre place ! Nous venons de voir le fonctionnement du service validator
pour comprendre comment l'ensemble marche, mais en réalité on l'utilisera très peu de cette manière.
Rappelez-vous le code pour la soumission d'un formulaire :
1 2 3 4 5 6 7 8 | <?php // Depuis un contrôleur if ($request->getMethod() == 'POST') { $form->bind($request); if ($form->isValid()) { // … |
Avec la ligne 7, le formulaire $form
va lui-même faire appel au service validator
, et valider l'objet qui vient d'être hydraté par le formulaire. Derrière cette ligne se cache donc le code que nous avons vu au paragraphe précédent. Les erreurs sont assignées au formulaire, et sont affichées dans la vue. Nous n'avons rien à faire, pratique !
Conclusion
Cette section a pour objectif de vous faire comprendre ce qu'il se passe déjà lorsque vous utilisez la méthode isValid
d'un formulaire. De plus, vous savez qu'il est possible de valider un objet indépendamment de tout formulaire, en mettant la main à la pâte.
Encore plus de règles de validation
Valider depuis un getter
Vous le savez, un getter est une méthode qui commence le plus souvent par « get », mais qui peut également commencer par « is ». Le composant Validation
accepte les contraintes sur les attributs, mais également sur les getters ! C'est très pratique, car vous pouvez alors mettre une contrainte sur une fonction, avec toute la liberté que cela vous apporte.
Tout de suite, un exemple d'utilisation :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php class Article { // … /** * @Assert\True() */ public function isArticleValid() { return false; } } |
Cet exemple vraiment basique considère l'article comme non valide, car l'annotation @Assert\True()
attend que la méthode retourne true
, alors qu'elle retourne false
. Vous pouvez l'essayer dans votre formulaire, vous verrez le message « Cette valeur doit être vraie » (message par défaut de l'annotation True()
) qui s'affiche en haut du formulaire. C'est donc une erreur qui s'applique à l'ensemble du formulaire.
Mais il existe un moyen de déclencher une erreur liée à un champ en particulier, ainsi l'erreur s'affichera juste à côté de ce champ. Il suffit de nommer le getter « is + le nom d'un attribut » : par exemple isTitre
si l'on veut valider le titre. Essayez par vous-mêmes le code suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php class Article { // … /** * @Assert\True() */ public function isTitre() { return false; } } |
Vous verrez que l'erreur « Cette valeur doit être vraie » s'affiche bien à côté du champ titre
.
Bien entendu, vous pouvez faire plein de traitements et de vérifications dans cette méthode, ici j'ai juste mis return false
pour l'exemple. Je vous laisse imaginer les possibilités.
Valider intelligemment un attribut objet
Derrière ce titre se cache une problématique toute simple : lorsque je valide un objet A, comment valider un objet B en attribut, d'après ses propres règles de validation ?
Il faut utiliser la contrainte Valid
, qui va déclencher la validation du sous-objet B selon les règles de validation de cet objet B. Prenons un exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <?php class A { /** * @Assert\MinLength(5) */ private $titre; /** * @Assert\Valid() */ private $b; } class B { /** * @Assert\Min(10) */ private $nombre; } |
Avec cette règle, lorsqu'on déclenche la validation sur l'objet A, le service validator
va valider l'attribut titre
selon le MinLength()
, puis va aller chercher les règles de l'objet B pour valider l'attribut nombre
de B selon le Min()
. N'oubliez pas cette contrainte, car valider un sous-objet n'est pas le comportement par défaut : sans cette règle dans notre exemple, vous auriez pu sans problème ajouter une instance de B qui ne respecte pas la contrainte de 10 minimum pour son attribut nombre
. Vous pourriez donc rencontrer des petits soucis de logique si vous l'oubliez.
Valider depuis un Callback
L'objectif de la contrainte Callback
est d'être personnalisable à souhait. En effet, vous pouvez parfois avoir besoin de valider des données selon votre propre logique, qui ne rentre pas dans un Maxlength
.
L'exemple classique est la censure de mots non désirés dans un attribut texte. Reprenons notre Article
, et considérons que l'attribut contenu ne peut pas contenir les mots « échec » et « abandon ». Voici comment mettre en place une règle qui va rendre invalide le contenu s'il contient l'un de ces mots :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Doctrine\Common\Collections\ArrayCollection; use Sdz\BlogBundle\Entity\Tag; // On rajoute ce use pour le context : use Symfony\Component\Validator\ExecutionContextInterface; /** * @ORM\Table() * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository") * * N'oubliez pas cet Assert : * @Assert\Callback(methods={"contenuValide"}) */ class Article { // … public function contenuValide(ExecutionContextInterface $context) { $mots_interdits = array('échec', 'abandon'); // On vérifie que le contenu ne contient pas l'un des mots if (preg_match('#'.implode('|', $mots_interdits).'#', $this->getContenu())) { // La règle est violée, on définit l'erreur et son message // 1er argument : on dit quel attribut l'erreur concerne, ici « contenu » // 2e argument : le message d'erreur $context->addViolationAt('contenu', 'Contenu invalide car il contient un mot interdit.', array(), null); } } } |
L'annotation se définit ici sur la classe, et non sur une méthode ou un attribut, faites attention.
Vous auriez même pu aller plus loin en comparant des attributs entre eux, par exemple pour interdire le pseudo dans un mot de passe. L'avantage du Callback
par rapport à une simple contrainte sur un getter, c'est de pouvoir ajouter plusieurs erreurs à la fois, en définissant sur quel attribut chacun se trouve grâce au premier argument de la méthode addViolationAt
(en mettant contenu
ou titre
, etc). Souvent la contrainte sur un getter suffira, mais pensez à ce Callback
pour les fois où vous serez limités.
Valider un champ unique
Il existe une dernière contrainte très pratique : UniqueEntity
. Cette contrainte permet de valider que la valeur d'un attribut est unique parmi toutes les entités existantes. Pratique pour vérifier qu'une adresse e-mail n'existe pas déjà dans la base de données par exemple.
Vous avez bien lu, j'ai parlé d'entité. En effet, c'est une contrainte un peu particulière, car elle ne se trouve pas dans le composant Validator
, mais dans le Bridge
entre Doctrine et Symfony2 (ce qui fait le lien entre ces deux bibliothèques). On n'utilisera donc pas @Assert\UniqueEntity
, mais simplement @UniqueEntity
. Il faut bien sûr en contrepartie faire attention de rajouter ce use
à chaque fois que vous l'utilisez :
1 | use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; |
Voici comment on pourrait, dans notre exemple avec Article
, contraindre nos titres à être tous différents les uns des autres :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <?php namespace Sdz\BlogBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; use Doctrine\Common\Collections\ArrayCollection; use Sdz\BlogBundle\Entity\Tag; // On rajoute ce use pour la contrainte : use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; /** * @ORM\Table() * @ORM\Entity(repositoryClass="Sdz\BlogBundle\Entity\ArticleRepository") * * @UniqueEntity(fields="titre", message="Un article existe déjà avec ce titre.") */ class Article { // … Les autres contraintes ne changent pas, pas même celle(s) sur l'attribut titre // Mais pour être logique, il faudrait aussi mettre la colonne titre en Unique pour Doctrine : /** * @ORM\Column(name="titre", type="string", length=255, unique=true) */ private $titre; } |
Ici aussi, l'annotation se définit sur la classe, et non sur une méthode ou sur un attribut.
Valider selon nos propres contraintes
Vous commencez à vous habituer : avec Symfony2 il est possible de tout faire ! L'objectif de cette section est d'apprendre à créer notre propre contrainte, que l'on pourra utiliser en annotation : @NotreContrainte
. L'avantage d'avoir sa propre contrainte est double :
- D'une part, c'est une contrainte réutilisable sur vos différents objets : on pourra l'utiliser sur
Article
, mais également surCommentaire
, etc. ; - D'autre part, cela permet de placer le code de validation dans un objet externe… et surtout dans un service ! Indispensable, vous comprendrez.
Une contrainte est toujours liée à un validateur, qui va être en mesure de valider la contrainte. Nous allons donc les faire en deux étapes. Pour l'exemple, nous allons créer une contrainte AntiFlood
, qui impose un délai de 15 secondes entre chaque message posté sur le site.
1. Créer la contrainte
Tout d'abord, il faut créer la contrainte en elle-même : c'est celle que nous appellerons en annotation depuis nos objets. Une classe de contrainte est vraiment très basique, toute la logique se trouvera en réalité dans le validateur. Je vous invite donc simplement à créer le fichier suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <?php // src/Sdz/BlogBundle/Validator/AntiFlood.php namespace Sdz\BlogBundle\Validator; use Symfony\Component\Validator\Constraint; /** * @Annotation */ class AntiFlood extends Constraint { public $message = 'Vous avez déjà posté un message il y a moins de 15 secondes, merci d\'attendre un peu.'; } |
L'annotation @Annotatio
n est nécessaire pour que cette nouvelle contrainte soit disponible via les annotations dans les autres classes. En effet, toutes les classes ne sont pas des annotations, heureusement.
Les options de l'annotation correspondent aux attributs publics de la classe d'annotation. Ici, on a l'attribut message
, on pourra donc faire :
1 | @AntiFlood(message="Mon message personnalisé") |
C'est tout pour la contrainte ! Passons au validateur.
2. Créer le validateur
C'est la contrainte qui décide par quel validateur elle doit se faire valider. Par défaut, une contrainte Xxx
demande à se faire valider par le validateur XxxValidator
. Créons donc le validateur AntiFloodValidator
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php // src/Sdz/BlogBundle/Validator/AntiFloodValidator.php namespace Sdz\BlogBundle\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; class AntiFloodValidator extends ConstraintValidator { public function validate($value, Constraint $constraint) { // Pour l'instant, on considère comme flood tout message de moins de 3 caractères if (strlen($value) < 3) { // C'est cette ligne qui déclenche l'erreur pour le formulaire, avec en argument le message $this->context->addViolation($constraint->message); } } } |
C'est tout pour le validateur. Il n'est pas très compliqué non plus, il contient juste une méthode validate()
qui permet de valider ou non la valeur. Son argument $value
correspond à la valeur de l'attribut sur laquelle on a défini l'annotation. Par exemple, si l'on avait défini l'annotation comme ceci :
1 2 3 4 | /** * @AntiFlood() */ private $contenu; |
… alors c'est tout logiquement le contenu de l'attribut $contenu
au moment de la validation qui sera injecté en tant qu'argument $value
.
La méthode validate()
ne doit pas renvoyer true
ou false
pour confirmer que la valeur est valide ou non. Elle doit juste lever une Violation
si la valeur est invalide. C'est ce qu'on a fait ici dans le cas où la chaîne fait moins de 3 caractères : on ajoute une violation, dont l'argument est le message d'erreur.
Sachez aussi que vous pouvez utiliser des messages d'erreur avec des paramètres. Par exemple : "Votre message %string% est considéré comme flood"
. Pour définir ce paramètre %string%
utilisé dans le message, il faut le passer dans le deuxième argument de la méthode addViolation
, comme ceci :
1 2 | <?php $this->context->addViolation($constraint->message, array('%string%' => $value)); |
Et voilà, vous savez créer votre propre contrainte ! Pour l'utiliser, c'est comme n'importe quelle autre annotation : on importe le namespace de l'annotation, et on la met en commentaire juste avant l'attribut concerné. Voici un exemple sur l'entité Commentaire
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php // src/Sdz/BlogBundle/Entity/Commentaire.php namespace Sdz\BlogBundle\Entity\Commentaire; use Sdz\BlogBundle\Validator\AntiFlood; class Commentaire { // … /** * @ORM\Column(name="contenu", type="text") * @AntiFlood() */ private $contenu; // … } |
Votre annotation sera ainsi prise en compte au même titre qu'un @Assert\MaxLength(x)
par exemple ! Mais si vous avez bien suivi, vous savez qu'on n'a pas encore vu le principal intérêt de nos propres contraintes : la validation par un service !
3. Transformer son validateur en service
Un service, c'est un objet qui remplit une fonction et auquel on peut accéder de presque n'importe où dans votre code Symfony2. Dans ce paragraphe, voyons comment s'en servir dans le cadre de nos contraintes de validation.
Quel est l'intérêt d'utiliser un service pour valider une contrainte ?
L'intérêt, comme on l'a vu dans les précédents chapitres sur les services, c'est qu'un service peut accéder à toutes sortes d'informations utiles. Il suffit de créer un service, de lui « injecter » les données, et il pourra ainsi s'en servir. Dans notre cas, on va lui injecter la requête et l'EntityManager comme données : il pourra ainsi valider notre contrainte non seulement à partir de la valeur $value
d'entrée, mais également en fonction de paramètres extérieurs qu'on ira chercher dans la base de données !
3.1. Définition du service
Prenons un exemple pour bien comprendre le champ des possibilités. Il nous faut créer un service, en y injectant les services request
et entity_manager
, et en y apposant le tag validator.contraint_validator. Voici ce que cela donne, dans le fichier services.yml
dans votre bundle :
1 2 3 4 5 6 7 8 9 | # src/Sdz/BlogBundle/Resources/config/services.yml services: sdzblog.validator.antiflood: # Le nom du service class: Sdz\BlogBundle\Validator\AntiFloodValidator # La classe du service, ici notre validateur déjà créé arguments: [@request, @doctrine.orm.entity_manager] # Les données qu'on injecte au service : la requête et l'EntityManager scope: request # Comme on injecte la requête, on doit préciser ce scope tags: - { name: validator.constraint_validator, alias: sdzblog_antiflood } # C'est avec l'alias qu'on retrouvera le service |
Si le fichier services.yml
n'existe pas déjà chez vous, c'est qu'il n'est pas chargé automatiquement. Pour cela, il faut faire une petite manipulation, je vous invite à lire le début du chapitre sur les services.
3.2. Modification de la contrainte
Maintenant que notre validateur est un service en plus d'être simplement un objet, nous devons adapter un petit peu notre code. Tout d'abord, modifions la contrainte pour qu'elle demande à se faire valider par le service d'alias sdzblog_antiflood
et non plus simplement par l'objet classique AntiFloodValidator
. Pour cela, il suffit de lui rajouter la méthode validateBy()
suivante (lignes 15 à 18) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <?php // src/Sdz/BlogBundle/Validator/AntiFlood.php namespace Sdz\BlogBundle\Validator; use Symfony\Component\Validator\Constraint; /** * @Annotation */ class AntiFlood extends Constraint { public $message = 'Vous avez déjà posté un message il y a moins de 15 secondes, merci d\'attendre un peu.'; public function validatedBy() { return 'sdzblog_antiflood'; // Ici, on fait appel à l'alias du service } } |
3.3. Modification du validateur
Enfin, il faut adapter notre validateur pour que d'une part il récupère les données qu'on lui injecte, grâce au constructeur, et d'autre part qu'il s'en serve tout simplement :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <?php // src/Sdz/BlogBundle/Validator/AntiFloodValidator.php namespace Sdz\BlogBundle\Validator; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Doctrine\ORM\EntityManager; use Symfony\Component\HttpFoundation\Request; class AntiFloodValidator extends ConstraintValidator { private $request; private $em; // Les arguments déclarés dans la définition du service arrivent au constructeur // On doit les enregistrer dans l'objet pour pouvoir s'en resservir dans la méthode validate() public function __construct(Request $request, EntityManager $em) { $this->request = $request; $this->em = $em; } public function validate($value, Constraint $constraint) { // On récupère l'IP de celui qui poste $ip = $this->request->server->get('REMOTE_ADDR'); // On vérifie si cette IP a déjà posté un message il y a moins de 15 secondes $isFlood = $this->em->getRepository('SdzBlogBundle:Commentaire') ->isFlood($ip, 15); // Bien entendu, il faudrait écrire cette méthode isFlood, c'est pour l'exemple if (strlen($value) < 3 && $isFlood) { // C'est cette ligne qui déclenche l'erreur pour le formulaire, avec en argument le message $this->context->addViolation($constraint->message); } } } |
Et voilà, nous venons de faire une contrainte qui s'utilise aussi facilement qu'une annotation, et qui pourtant fait un gros travail en allant chercher dans la base de données si l'IP courante envoie trop de messages. Un peu de travail à la création de la contrainte, mais son utilisation est un jeu d'enfant à présent !
Vous trouverez un brin plus d'informations sur la page de la documentation sur la création de contrainte, notamment comment faire une contrainte qui s'applique non pas à un attribut, mais à une classe entière.
Pour conclure
Vous savez maintenant valider dignement vos données, félicitations !
Le formulaire était le dernier point que vous aviez vraiment besoin d'apprendre. À partir de maintenant, vous pouvez créer un site internet en entier avec Symfony2, il ne manque plus que la sécurité à aborder, car pour l'instant, sur notre blog, tout le monde peut tout faire. Rendez-vous au prochain chapitre pour régler ce détail.
En résumé
- Le composant
validator
permet de valider les données d'un objet suivant des règles définies. - Cette validation est systématique lors de la soumission d'un formulaire : il est en effet impensable de laisser l'utilisateur entrer ce qu'il veut sans vérifier !
- Les règles de validation se définissent via les annotations directement à côté des attributs de la classe à valider. Vous pouvez bien sûr utiliser d'autres formats tels que le YAML ou le XML.
- Il est également possible de valider à l'aide de getters, de callbacks ou même de services. Cela rend la procédure de validation très flexible et très puissante.