• 30 hours
  • Medium

Free online content available in this course.

course.header.alt.is_video

course.header.alt.is_certifying

Got it!

Last updated on 5/13/19

Validez vos données

Log in or subscribe for free to enjoy all this course has to offer!



Au chapitre précédent nous avons vu comment créer des formulaires avec Symfony. Mais qui dit formulaire dit vérification des données rentrées ! Symfony 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 Symfony. 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 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 Symfony, 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'un objet est valide ou pas. Plus concrètement, il nous faudra établir des règles précises pour dire que tel attribut (le nom d'auteur 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  Doctrine 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é Advert 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 ;

  • Le nom de 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 :

<?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é Advert 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 Advert :

<?php
// src/OC/PlatformBundle/Entity/Advert.php

namespace OC\PlatformBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
// N'oubliez pas de rajouter ce « use », il définit le namespace pour les annotations de validation
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Table(name="oc_advert")
 * @ORM\Entity(repositoryClass="OC\PlatformBundle\Repository\AdvertRepository")
 * @ORM\HasLifecycleCallbacks()
 */
class Advert
{
  /**
   * @ORM\Column(name="id", type="integer")
   * @ORM\Id
   * @ORM\GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @ORM\Column(name="date", type="datetime")
   * @Assert\DateTime()
   */
  private $date;

  /**
   * @ORM\Column(name="title", type="string", length=255)
   * @Assert\Length(min=10)
   */
  private $title;

  /**
   * @ORM\Column(name="author", type="string", length=255)
   * @Assert\Length(min=2)
   */
  private $author;

  /**
   * @ORM\Column(name="content", type="text")
   * @Assert\NotBlank()
   */
  private $content;

  /**
   * @ORM\OneToOne(targetEntity="OC\PlatformBundle\Entity\Image", cascade={"persist", "remove"})
   * @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 ?

Syntaxe

Revenons un peu sur les annotations que l'on a ajoutées. Nous avons utilisé la forme simple, qui est construite comme ceci :

@Assert\Contrainte(valeur de l'option par défaut)

Avec :

  • La Contrainte, qui peut être, comme vous l'avez vu, NotBlank ou Length, 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 Type est la valeur du type à restreindre.

Mais on peut aussi utiliser la forme étendue qui permet de personnaliser la valeur de plusieurs options en même temps, comme ceci :

@Assert\Contrainte(option1="valeur1", option2="valeur2", …)

Les différentes options diffèrent d'une contrainte à une autre, mais voici un exemple avec la contrainte Length :

@Assert\Length(min=10, minMessage="Le titre doit faire au moins {{ limit }} caractères.")

Bien entendu, vous pouvez mettre plusieurs contraintes sur un même attribut. Par exemple pour un attribut représentant une URL, on pourrait mettre les deux contraintes suivantes :

<?php
/**
 * @Assert\Length(max=255)
 * @Assert\Url()
 */
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".

-

NotNull

Null

La contrainte NotNull vérifie que la valeur est strictement différente de null.

-

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, Symfony va vérifier les MX de l'e-mail via la fonction checkdnsrr.

Length

La contrainte Length vérifie que la valeur donnée fait au moins X ou au plus Y caractères de long.

min : le nombre de caractères minimum à respecter.
max : le nombre de catactères maximum à respecter.
minMessage : le message d'erreur dans le cas où la contrainte minimum n'est pas respectée.
maxMessage : le message d'erreur dans le cas où la contrainte maximum n'est pas respectée.
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

Range

La contrainte Range vérifie que la valeur ne dépasse pas X, ou qu'elle dépasse Y.

min : la valeur minimum à respecter.
max : la valeur maximum à respecter.
minMessage : le message d'erreur dans le cas où la contrainte minimum n'est pas respectée.
maxMessage : le message d'erreur dans le cas où la contrainte maximum n'est pas respectée.
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 pointe 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.

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 $advert->setTitle('abc') alors que ce titre a moins de 10 caractères. Il est invalide mais rien ne se passera.

Pour valider l'objet, on passe par un acteur externe : le service validator. Ce service s'obtient comme n'importe quel autre service :

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

<?php
// Depuis un contrôleur

// …

  public function testAction()
  {
    $advert = new Advert;
        
    $advert->setDate(new \Datetime());  // Champ « date » OK
    $advert->setTitle('abc');           // Champ « title » incorrect : moins de 10 caractères
    //$advert->setContent('blabla');    // Champ « content » incorrect : on ne le définit pas
    $advert->setAuthor('A');            // Champ « author » incorrect : moins de 2 caractères
        
    // On récupère le service validator
    $validator = $this->get('validator');
        
    // On déclenche la validation sur notre object
    $listErrors = $validator->validate($advert);

    // Si $listErrors n'est pas vide, on affiche les erreurs
    if(count($listErrors) > 0) {
      // $listErrors est un objet, sa méthode __toString permet de lister joliement les erreurs
      return new Response((string) $listErrors);
    } else {
      return new Response("L'annonce est valide !");
    }
  }

Vous pouvez vous amuser avec le contenu de l'entité Advert pour voir comment réagit le validateur.

La validation automatique sur les formulaires

En pratique, on ne se servira que très peu du service validator nous-mêmes. En effet, le formulaire de Symfony 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 :

<?php
if ($form->handleRequest($request)->isValid()) {
  // ...
}

Dans la méthode handleRequest, le formulaire $form va lui-même faire appel au service validator, et valider l'objet qui vient d'être hydraté par le formulaire. Ensuite, la méthode isValid vient compter le nombre d'erreur et retourne false s'il y a au moins une erreur. 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

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. Vous le savez, un getter est une méthode qui commence le plus souvent par « get », mais qui peut également commencer par « is ».

Tout de suite, un exemple d'utilisation :

<?php
class Advert
{

  // …

  /**
   * @Assert\IsTrue()
   */
  public function isAdvertValid()
  {
    return false;
  }
}

Cet exemple vraiment basique considère toujours l'annonce comme non valide, car l'annotation @Assert\IsTrue() attend que la méthode retourne true, alors qu'ici la méthode 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 IsTrue()) 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 isTitle si l'on veut valider le title. Essayez par vous-mêmes le code suivant :

<?php
class Advert
{

  // …

  /**
   * @Assert\IsTrue()
   */
  public function isTitle()
  {
    return false;
  }
}

Vous verrez que l'erreur « Cette valeur doit être vraie » s'affiche bien à côté du champ title.

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 :

<?php
class A
{
  /**
   * @Assert\Length(min=5)
   */
  private $title;

  /**
   * @Assert\Valid()
   */
  private $b;
}

class B
{
  /**
   * @Assert\Range(max=10)
   */
  private $number;
}

Avec cette règle, lorsqu'on déclenche la validation sur l'objet A, le service validator va valider l'attribut title selon le Length(), puis va aller chercher les règles de l'objet B pour valider l'attribut number de B selon le Range(). N'oubliez pas cette contrainte, car valider un sous-objet n'est pas le comportement par défaut : sans cette règle Valid  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 number. 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 Length par exemple.

L'exemple classique est la censure de mots non désirés dans un attribut texte. Reprenons notre Advert, et considérons que l'attribut content ne peut pas contenir les mots « démotivation » et « abandon ». Voici comment mettre en place une règle qui va rendre invalide le contenu s'il contient l'un de ces mots :

<?php
// src/OC/PlatformBundle/Entity/Advert.php

namespace OC\PlatformBundle\Entity;

use Symfony\Component\Validator\Constraints as Assert;
// Ajoutez ce use pour le contexte
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * @ORM\Entity
 */
class Advert
{
  // …
	
  /**
   * @Assert\Callback
   */
  public function isContentValid(ExecutionContextInterface $context)
  {
    $forbiddenWords = array('démotivation', 'abandon');

    // On vérifie que le contenu ne contient pas l'un des mots
    if (preg_match('#'.implode('|', $forbiddenWords).'#', $this->getContent())) {
      // La règle est violée, on définit l'erreur
      $context
        ->buildViolation('Contenu invalide car il contient un mot interdit.') // message
        ->atPath('content')                                                   // attribut de l'objet qui est violé
        ->addViolation() // ceci déclenche l'erreur, ne l'oubliez pas
      ;
    }
  }
}

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 chacune se trouve grâce à la méthode atPath (en mettant content ou title, 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 (qui lui est indépendant de Doctrine), mais dans le bridge entre Doctrine et Symfony (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 :

use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

Voici comment on pourrait, dans notre exemple avec Advert, contraindre nos titres à être tous différents les uns des autres :

<?php
// src/OC/PlatformBundle/Entity/Advert.php

namespace OC\PlatformBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
// On rajoute ce use pour la contrainte :
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Entity
 * @UniqueEntity(fields="title", message="Une annonce existe déjà avec ce titre.")
 */
class Advert
{
  /**
   * @var string
   *
   * Et pour être logique, il faudrait aussi mettre la colonne titre en Unique pour Doctrine :
   * @ORM\Column(name="title", type="string", length=255, unique=true)
   */
  private $title;
  
  // ...
}
Le validateur indique au formulaire que le titre existe déjà. Le formulaire m'affiche l'erreur.
Le validateur indique au formulaire que le titre existe déjà en base de données. Le formulaire m'affiche ensuite l'erreur.

Je vous invite à faire un tour dans l'onglet Forms du profiler, c'est souvent une mine d'informations sur vos formulaires et leurs erreurs. La figure suivante montre par exemple l'erreur que nous avons sur ce titre déjà existant.

L'erreur est bien attaché au champ titre.
L'erreur est bien attaché au champ titre.

Valider selon nos propres contraintes

Vous commencez à vous habituer : avec Symfony 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 Advert, mais également sur Application, 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 (que ce soit une annonce ou une candidature).

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 :

<?php
// src/OC/PlatformBundle/Validator/Antiflood.php

namespace OC\PlatformBundle\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.";
}

Les options de l'annotation correspondent en réalité aux attributs publics de la classe d'annotation. Ici, on a l'attribut message, on pourra donc faire :

@Antiflood(message="Mon message personnalisé")

C'est tout pour la contrainte ! Passons au validateur.

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 :

<?php
// src/OC/PlatformBundle/Validator/AntifloodValidator.php

namespace OC\PlatformBundle\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 de la contrainte
      $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 :

/**
 * @Antiflood()
 */
private $content;

… 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 (accessible publiquement dans l'attribut de la contrainte).

Il y a deux moyens de définir une violation :

  • Lorsque vous n'avez que le message de l'erreur à passer, vous pouvez utiliser la méthode addViolation  qu'on a utilisé ici.

  • Lorsque vous avez plus, comme dans notre précédent callback où on définissait l'attribut sur laquelle attacher la violation, alors vous pouvez utiliser la méthode buildViolation qu'on a utilisé précédemment.

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 utiliser la deuxième méthode pour définir la violation, comme ceci :

<?php
$this->context
  ->buildViolation($constraint->message)
  ->setParameters(array('%string%' => $value))
  ->addViolation()
;

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

<?php
// src/OC/PlatformBundle/Entity/Advert.php

namespace OC\PlatformBundle\Entity;

use OC\PlatformBundle\Validator\Antiflood;

class Advert
{
  /**
   * @Assert\NotBlank()
   * @Antiflood()
   */
  private $content;

  // …
}

Votre annotation sera ainsi prise en compte au même titre que le @Assert\NotBlank par exemple ! Et bien sûr, vous pouvez l'utiliser sur tous les objets que vous voulez : Advert, Application, etc. N'hésitez pas à la tester dès maintenant (essayez de créer une annonce avec un contenu qui a moins de 3 caractères), elle fonctionne déjà.

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 !

Transformer son validateur en service

Un service on l'a déjà vu, c'est un objet qui remplit une fonction et auquel on peut accéder de presque n'importe où dans votre code Symfony. 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 ?

Rappelez vous notre objectif pour cette contrainte d'anti-flood : on veut empêcher quelqu'un de poster à moins de 15 secondes d'intervalle. Il nous faut donc un accès à son IP pour le reconnaitre, et à la base de données pour savoir quand était son dernier post. Tout cela est impossible sans service.

L'intérêt est donc 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 !

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

# src/OC/PlatformBundle/Resources/config/services.yml

services:
    oc_platform.validator.antiflood:                              # Le nom du service
        class: OC\PlatformBundle\Validator\AntifloodValidator     # La classe du service, ici notre validateur déjà créé
        arguments: ["@request_stack", "@doctrine.orm.entity_manager"] # Les données qu'on injecte au service : la requête et l'EntityManager
        tags:
            - { name: validator.constraint_validator, alias: oc_platform_antiflood }  # C'est avec l'alias qu'on retrouvera le service
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 oc_platform_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) :

<?php
// src/OC/PlatformBundle/Validator/Antiflood.php

namespace OC\PlatformBundle\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 'oc_platform_antiflood'; // Ici, on fait appel à l'alias du service
  }
}
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 :

<?php
// src/OC/PlatformBundle/Validator/AntifloodValidator.php

namespace OC\PlatformBundle\Validator;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class AntifloodValidator extends ConstraintValidator
{
  private $requestStack;
  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(RequestStack $requestStack, EntityManagerInterface $em)
  {
    $this->requestStack = $requestStack;
    $this->em           = $em;
  }

  public function validate($value, Constraint $constraint)
  {
    // Pour récupérer l'objet Request tel qu'on le connait, il faut utiliser getCurrentRequest du service request_stack
    $request = $this->requestStack->getCurrentRequest();

    // On récupère l'IP de celui qui poste
    $ip = $request->getClientIp();

    // On vérifie si cette IP a déjà posté une candidature il y a moins de 15 secondes
    $isFlood = $this->em
      ->getRepository('OCPlatformBundle:Application')
      ->isFlood($ip, 15) // Bien entendu, il faudrait écrire cette méthode isFlood, c'est pour l'exemple
    ;

    if ($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 !

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 Symfony, il ne manque plus que la sécurité à aborder, car pour l'instant, sur notre plateforme d'annonce, 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.

  • Le code du cours tel qu'il doit être à ce stade est disponible sur la branche iteration-15 du dépot Github.

Example of certificate of achievement
Example of certificate of achievement