De base, Symfony met à disposition un nombre important de contrainte. Cependant, il peut être utile de créer ses propres contraintes. Pour illustrer le propos, nous allons créer une contrainte sur la position des images présentes dans un article d’une page web. Dans un article, chaque image a une position et chaque position ne peut être occupée que par une seule image à la fois.
L’entité Image
Notre contrainte est une contrainte de classe qui va porter sur l’entité Image et en particulier l’attribut position. Je ne mets qu’une partie du code de l’entité :
[…]
class Image
{
[…]
/**
* @var integer
*
* @ORM\Column(name="position", type="smallint")
*/
private $position;
[…]
/**
* Set position
*
* @param integer $position
* @return Image
*/
public function setPosition($position)
{
$this->position = $position;
return $this;
}
/**
* Get position
*
* @return integer
*/
public function getPosition()
{
return $this->position;
}
[…]
}
Création de la classe de contrainte
La classe va se nommer ImagePosition. Elle doit étendre la classe Constraint :
<?php
namespace Xxx\YyyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
class ImagePosition extends Constraint
{
public $message = 'La position "%position%" est déjà occupée par une autre image.';
/**
* @inheritdoc
*/
public function validatedBy()
{
return 'validator.image_position';
}
/**
* @inheritdoc
*/
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
2 méthodes sont surchargées :
- validatedBy() : cette méthode permet d’indiquer l’alias du service de validation qui va valider la contrainte. Ce service va être créée dans le chapitre suivant.
- getTargets() : cette méthode permet de définir s’il s’agit d’une contrainte qui porte uniquement sur un attribut de classe (PROPERTY_CONSTRAINT) ou bien sur la classe entière (CLASS_CONSTRAINT). Dans cet exemple, nous créons une contrainte de classe.
On stocke dans l’attribut de classe message le message d’erreur qui va apparaitre si une entité Image n’est pas conforme à notre contrainte.
Création du service de validation
Notre classe de validation va se nommer ImagePositionValidator. Elle doit étendre la classe ConstraintValidator :
<?php
namespace Xxx\YyyBundle\Validator\Constraints;
use Xxx\YyyBundle\Entity\Image;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class ImagePositionValidator extends ConstraintValidator
{
private $em;
public function __construct(ObjectManager $em)
{
$this->em = $em;
}
/**
* Vérifie si la position d'une image est valide.
* Elle est valide si elle n'est pas déjà utilisée par une autre image du même article.
*
* @param Image $image Instance d'Image qui doit être validée
* @param Constraint $constraint Instance de ImagePosition
*
* @api
*/
public function validate($image, Constraint $constraint)
{
$positionExiste = $this->em->getRepository('XxxYyyBundle:Image')->isPositionExiste($image);
if ($positionExiste) {
$this->context->buildViolation($constraint->message, array('%position%' => $image->getPosition()))
->atPath('position')
->addViolation();
}
}
}
Comme vous pouvez le voir, vous pouvez injecter d’autre service de manière classique. Ici, on récupère le manager d'entité.
Au minimum, il faut implémenter la méthode validate(…) qui est responsable de la validation ou non de la contrainte. Comme nous sommes en train de créer une contrainte sur la classe Image, le premier argument de la méthode est donc une instance de la classe Image. Le deuxième argument est quant à lui, une instance de notre classe de contrainte.
Si la contrainte n’est pas respecté, on envoie une violation avec le message d’erreur contenu dans la classe de la contrainte grâce à la méthode buildViolation(...). La méthode atPath(...) permet d'indiquer l'attribut de la classe qui ne respecte pas la contrainte. Ici la validation se fait grâce à une requête sur la base de données : on vérifie que la position n’est pas déjà utilisée par une autre image du même article.
Une fois la classe créée, il faut la déclarer en tant que service :
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
[...]
<service id="xxx_yyy.validator.image_position" class="Xxx\YyyBundle\Validator\Constraints\ImagePositionValidator">
<argument type="service" id="doctrine.orm.default_entity_manager" />
<tag name="validator.constraint_validator" alias="validator.image_position" />
</service>
[...]
</services>
</container>
Il faut taguer le service en tant que validator.constraint_validator et donner le même alias que celui défini dans la méthode validatedBy() de notre classe de contrainte ImagePosition.
Utilisation de notre contrainte personnalisée
Il ne reste plus qu’à utiliser notre contrainte. Dans le fichier de validation Resources/config/validation.yml, il faut appliquer notre contrainte au niveau de la classe Image :
Xxx\YyyBundle\Entity\Image:
constraints:
- Xxx\YyyBundle\Validator\Constraints\ImagePosition: ~
properties:
position:
- NotNull:
message: "La position est obligatoire."
Dorénavant, si un utilisateur indique dans un formulaire de création d’image la même position qu’une image existante, le formulaire ne se validera pas.
Version
Cet article a été testé avec :
Symfony : 2.7