Validez vos données

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 ?

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 :

  1. 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.
  2. 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 ou MinLength, 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 de MinLength (« 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

NotBlank Blank

La contrainte NotBlank vérifie que la valeur soumise n'est ni une chaîne de caractères vide, ni NULL. La contrainte Blank fait l'inverse.

-

True False

La contrainte True vérifie que la valeur vaut true, 1 ou "1". La contrainte False vérifie que la valeur vaut false, 0 ou "0".

-

Type

La contrainte Type vérifie que la valeur est bien du type donné en argument.

type (option par défaut) : le type duquel doit être la valeur, parmi array, bool, int, object, etc.

Contraintes sur des chaînes de caractères :

Contrainte

Rôle

Options

Email

La contrainte Email vérifie que la valeur est une adresse e-mail valide.

checkMX (défaut : false) : si défini à true, Symfony2 va vérifier les MX de l'e-mail via la fonction checkdnsrr.

MinLength MaxLength

La contrainte MinLength vérifie que la valeur donnée fait au moins X caractères de long. MaxLength, au plus X caractères de long.

limit (option par défaut) : le seuil de longueur, en nombre de caractères. charset (défaut : UTF-8) : le charset à utiliser pour calculer la longueur.

Url

La contrainte Url vérifie que la valeur est une adresse URL valide.

protocols (défaut : array('http', 'https')) : définit les protocoles considérés comme valides. Si vous voulez accepter les URL en ftp://, ajoutez-le à cette option.

Regex

La contrainte Regex vérifie la valeur par rapport à une regex.

pattern (option par défaut) : la regex à faire correspondre. match (défaut : true) : définit si la valeur doit (true) ou ne doit pas (false) correspondre à la regex.

Ip

La contrainte Ip vérifie que la valeur est une adresse IP valide.

type (défaut : 4) : version de l'IP à considérer. 4 pour IPv4, 6 pour IPv6, all pour toutes les versions, et d'autres.

Language

La contrainte Language vérifie que la valeur est un code de langage valide selon la norme.

-

Locale

La contrainte Locale vérifie que la valeur est une locale valide. Exemple : fr ou fr_FR.

-

Country

La contrainte Country vérifie que la valeur est un code pays en 2 lettres valide. Exemple : fr.

-

Contraintes sur les nombres :

Contrainte

Rôle

Options

Max Min

La contrainte Max vérifie que la valeur ne dépasse pas X. Min, que la valeur dépasse ce X.

limit (option par défaut) : la valeur seuil. invalidMessage : message d'erreur lorsque la valeur n'est pas un nombre.

Contraintes sur les dates :

Contrainte

Rôle

Options

Date

La contrainte Date vérifie que la valeur est un objet de type Datetime, ou une chaîne de caractères du type YYYY-MM-DD.

-

Time

La contrainte Time vérifie que la valeur est un objet de type Datetime, ou une chaîne de caractères du type HH:MM:SS.

-

DateTime

La contrainte Datetime vérifie que la valeur est un objet de type Datetime, ou une chaîne de caractères du type YYYY-MM-DD HH:MM:SS.

-

Contraintes sur les fichiers :

Contrainte

Rôle

Options

File

La contrainte File vérifie que la valeur est un fichier valide, c'est-à-dire soit une chaîne de caractères qui point vers un fichier existant, soit une instance de la classe File (ce qui inclut UploadedFile).

maxSize : la taille maximale du fichier. Exemple : 1M ou 1k. mimeTypes : mimeType(s) que le fichier doit avoir.

Image

La contrainte Image vérifie que la valeur est valide selon la contrainte précédente File (dont elle hérite les options), sauf que les mimeTypes acceptés sont automatiquement définis comme ceux de fichiers images. Il est également possible de mettre des contraintes sur la hauteur max ou la largeur max de l'image.

maxSize : la taille maximale du fichier. Exemple : 1M ou 1k. minWidth / maxWidth : la largeur minimale et maximale que doit respecter l'image. minHeight / maxHeight : la hauteur minimale et maximale que doit respecter l'image.

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 sur Commentaire, 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 @Annotation 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.