• 30 heures
  • Facile

Ce cours est visible gratuitement en ligne.

Ce cours est en vidéo.

Ce cours existe en livre papier.

Vous pouvez obtenir un certificat de réussite à l'issue de ce cours.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 04/09/2017

Validez vos données

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

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 composantValidatorqui, 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 composantValidatorde 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, levalidatorest un service indépendant et n'a nul besoin d'un formulaire pour exister. Ayez-le en tête, avec levalidator, 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'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  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é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'objetImage.

À 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;

Ceuseest à 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 objetAdvert :

<?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\Entity(repositoryClass="OC\PlatformBundle\Entity\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 :

  • LaContrainte, qui peut être, comme vous l'avez vu,NotBlankouLength, etc. Nous voyons plus loin toutes les contraintes possibles.

  • LaValeurentre 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 deType 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 contrainteLength:

@Assert\Length(min=10, message="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'optionmessage, 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 contrainteNotBlankvérifie que la valeur soumise n'est ni une chaîne de caractères vide, niNULL.
La contrainteBlankfait l'inverse.

-

True
False

La contrainteTruevérifie que la valeur vauttrue,1ou"1".
La contrainteFalsevérifie que la valeur vautfalse,0ou"0".

-

NotNull
Null

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

-

Type

La contrainteTypevérifie que la valeur est bien du type donné en argument.

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

Contraintes sur des chaînes de caractères :

Contrainte

Rôle

Options

Email

La contrainteEmailvé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 fonctioncheckdnsrr.

Length

La contrainteLengthvé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 contrainteUrlvé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 enftp://, ajoutez-le à cette option.

Regex

La contrainteRegexvé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 contrainteIpvérifie que la valeur est une adresse IP valide.

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

Language

La contrainteLanguagevérifie que la valeur est un code de langage valide selon la norme.

-

Locale

La contrainteLocalevérifie que la valeur est une locale valide. Exemple :froufr_FR.

-

Country

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

-

Contraintes sur les nombres :

Contrainte

Rôle

Options

Range

La contrainteRange 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 contrainteDatevérifie que la valeur est un objet de typeDatetime, ou une chaîne de caractères du typeYYYY-MM-DD.

-

Time

La contrainteTimevérifie que la valeur est un objet de typeDatetime, ou une chaîne de caractères du typeHH:MM:SS.

-

DateTime

La contrainteDatetimevérifie que la valeur est un objet de typeDatetime, ou une chaîne de caractères du typeYYYY-MM-DD HH:MM:SS.

-

Contraintes sur les fichiers :

Contrainte

Rôle

Options

File

La contrainteFilevé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 contrainteImagevé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 serviceValidator

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 servicevalidator. 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éthodevalidatedu 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 :

<?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 le tableau n'est pas vide, on affiche les erreurs
    if(count($listErrors) > 0) {
      return new Response(print_r($listErrors, true));
    } 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 servicevalidatornous-mêmes. En effet, le formulaire de Symfony2 le fait à notre place ! Nous venons de voir le fonctionnement du servicevalidatorpour 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éthodehandleRequest, le formulaire$formva lui-même faire appel au servicevalidator, et valider l'objet qui vient d'être hydraté par le formulaire. Ensuite, la méthodeisValid vient compter le nombre d'erreurs 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éthodeisValidd'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 composantValidationaccepte 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\True()
   */
  public function isAdvertValid()
  {
    return false;
  }
}

Cet exemple vraiment basique considère l'annonce comme non valide, car l'annotation@Assert\True()attend que la méthode retournetrue, alors qu'ici la méthode retournefalse. Vous pouvez l'essayer dans votre formulaire, vous verrez le message « Cette valeur doit être vraie » (message par défaut de l'annotationTrue()) 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 exempleisTitlesi l'on veut valider le title. Essayez par vous-mêmes le code suivant :

<?php
class Advert
{

  // …

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

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

Bien entendu, vous pouvez faire plein de traitements et de vérifications dans cette méthode, ici j'ai juste misreturn falsepour 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 contrainteValid, 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 servicevalidatorva valider l'attributtitleselon leLength(), puis va aller chercher les règles de l'objet B pour valider l'attributnumber de B selon leRange(). 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 maximum pour son attributnumber. Vous pourriez donc rencontrer des petits soucis de logique si vous l'oubliez.

Valider depuis unCallback

L'objectif de la contrainteCallbackest 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 unlength.

L'exemple classique est la censure de mots non désirés dans un attribut texte. Reprenons notreAdvert, et considérons que l'attribut content 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 :

<?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('échec', '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 duCallbackpar 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éthodeatPath (en mettantcontenuoutitre, etc). Souvent la contrainte sur un getter suffira, mais pensez à ceCallbackpour 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 composantValidator, mais dans leBridgeentre 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 ceuseà chaque fois que vous l'utilisez :

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

Voici comment on pourrait, dans notre exemple avecAdvert, 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
{
  // 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;
  
  // ...
}

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 surAdvert, mais également surApplication, 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 contrainteAntiFlood, 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 aux attributs publics de la classe d'annotation. Ici, on a l'attributmessage, 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 contrainteXxxdemande à se faire valider par le validateurXxxValidator. Créons donc le validateurAntifloodValidator:

<?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éthodevalidate()qui permet de valider ou non la valeur. Son argument$valuecorrespond à 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$contentau moment de la validation qui sera injecté en tant qu'argument$value.

La méthodevalidate()ne doit pas renvoyertrueoufalsepour confirmer que la valeur est valide ou non. Elle doit juste lever uneViolationsi 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.

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éthodeaddViolation  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 donc utiliser la deuxième méthode pour définir la violation, commme 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. 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 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 ?

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$valued'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 servicesrequest_stack etentity_manager, et en y apposant le tag validator.contraint_validator. Voici ce que cela donne, dans le fichierservices.ymldans 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'aliasoc_platform_antifloodet non plus simplement par l'objet classiqueAntifloodValidator. Pour cela, il suffit de lui rajouter la méthodevalidateBy()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 Symfony2, 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 composantvalidatorpermet 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.

Exemple de certificat de réussite
Exemple de certificat de réussite