Symfony2: Comment créer une Contrainte de Validation Doctrine Personnalisée

Une des tâches les plus courantes que vous devez effectuer lors de la validation d’un formulaire est de vérifier les données d’un champ dans votre base de données comme le cas de vérification de l’unicité personnalisée.

Dans ce Tutorial, le but de notre contrainte est de vérifier qu’un code postal a été utilisé avec une autre entité.

La première étape est la création de la classe de la contrainte :

// src/Nm/CoreBundle/Validator/ZipCodeExist.php

namespace Nm\CoreBundle\Validator;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */

class ZipCodeExist extends Constraint
{

    public $message = "Le code '%value%' n'est pas utilisé";
    public $entity;
    public $property;

    public function validatedBy()
    {
        return 'validator.zip.exist';
    }

    public function requiredOptions()
    {
        return array('entity', ' property');
    }

    public function targets()
    {
        return self::PROPERTY_CONSTRAINT;
    }
}

Comme vous pouvez voir, la déclaration de la classe est simple. Nous avons défini le message d’erreur et deux autres options nécessaires l’Entité et sa propriété. La méthode validatedBy() retourne le nom du service que nous allons déclarer juste après qui se chargera de valider la contrainte. Enfin, la fonction des targets() indique que la contrainte sera appliqué seulement sur les propriétés.

// src/Nm/CoreBundle/Validator/ZipCodeExistValidator.php

namespace Nm\CoreBundle\Validator;

use Doctrine\ORM\EntityManager;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class ZipCodeExistValidator extends ConstraintValidator
{

    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function validate($value, Constraint $constraint)
    {
        $entities = $this->entityManager->getRepository("CoreBundle:Restaurant")
                ->findByZipCode($value);
        
        if (empty($entities)) {
            
            $this->context->addViolation($constraint->message, array('%value%' => $value));

            return false;
        }

        return true;
    }

}

La méthode validate() vérifie la validité de la contrainte et renvoie par conséquent TRUE ou FALSE et ajoute le message d’erreur.

Ici, notre contrainte fonctionne contrairement à celle de symfony @UniqueEntity, dans notre cas la valeur est valide seulement lorsqu’elle est déjà utilisée dans l’entité Restaurant.

ZipCodeExistValidator a une dépendance c’est Doctrine EntityManager. Nous aurons besoin alors de le déclarer en tant que service :

<!-- src/Nm/CoreBundle/Resources/config/services.xml -->
    <parameters>
        <parameter key="nm.validator.zip.exist.class">Nm\CoreBundle\Validator\ZipCodeExistValidator</parameter>
    </parameters>
    <services>
        <service id="nm.validator.zip.exist" class="%nm.validator.zip.exist.class%">
            <argument type="service" id="doctrine.orm.entity_manager" />
            <tag name="validator.constraint_validator" alias="validator.zip.exist" />
        </service>
    </services>

La dernière étape est l’utilisation de notre validateur personnalisé, c’est très facile, tout comme ceux fournis par Symfony2 lui-même :

// src/Nm/CoreBundle/Entity/Address.php

use Symfony\Component\Validator\Constraints as Assert;
use Nm\CoreBundle\Validator as NmAssert;

class Address
{
    // ...


    /**
     * @var string
     * @Assert\Length(max="5", min="5", minMessage="Code postal invalide", maxMessage="Code postal invalide")
     * @Assert\Regex("/[0-9]{2}[0-9]{3}/", message="Code postal invalide")
     * 
     * @NmAssert\ZipCodeExist(message="Aucun de nos restaurants ne peux livré à cette adresse")
     * 
     * @ORM\Column(name="zip_code", length=5)
     */
    private $zipCode

    // ...
}