Bases de validation personnalisées dans Symfony 4/5 avec des exemples

À Symfony, à mon avis, une fonctionnalité très pratique pour la validation d'entité. En particulier, l'utilisation du format d'annotation pour configurer les règles de validation me soudoie vraiment. Dans la grande majorité des tâches, les solutions clé en main couvrent les cas standard. Mais, comme vous le savez, la validation est une question délicate, et vous ne savez jamais quelles restrictions vous devrez imposer cette fois. D'un autre côté, une validation plus flexible et bien pensée contribuera toujours à éviter les erreurs des utilisateurs.


Sous la coupe, je vous invite à voir à quel point il est facile d'écrire vos restrictions et d'étendre les comparaisons de deux champs disponibles en utilisant l'exemple de validation de base et de validation. L'article peut intéresser ceux qui connaissent encore mal la validation dans Symfony ou qui ont ignoré la possibilité d'écrire leurs propres validateurs.


Dans cet article, nous garderons à l'esprit le contexte classique de la validation. Il existe une certaine entité, dont les données sont remplies à partir du formulaire correspondant, tandis que la validation impose des restrictions sur tout aspect des données saisies. Il convient de noter que les validateurs peuvent être utilisés sans restrictions pour contrôler la logique interne, la validation de l'API, etc.


Nous déterminons immédiatement que les exemples proposés ne sont pas les seules solutions possibles et ne revendiquent pas une optimalité maximale ou une utilisation extensive, mais sont proposés à des fins de démonstration. D'un autre côté, des parties des exemples sont prises et simplifiées à partir de projets de combat, et j'espère qu'elles pourront être utiles pour illustrer la simplicité d'écrire des validations personnalisées sans interruption de la réalité.


Validation de base


Pour effectuer une validation entièrement fonctionnelle, vous devez créer seulement deux classes - les héritières de Constraint et ConstraintValidator. Constraint, comme son nom l'indique, définit et décrit les contraintes, tandis que ConstraintValidator les valide. A titre d'exemple, nous allons écrire un validateur d'heure au format "hh: mm" stocké au format texte. Dans la documentation officielle, il est proposé que Constraint décrive les propriétés publiques de la contrainte. Alors faisons-le.


namespace App\Custom\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 * @Target({"PROPERTY"})
 */
class TextTime extends Constraint
{
    public $message = '  ';
}

Ici, l'annotation Target détermine si la validation sera utilisée: pour une propriété ou pour une classe. Vous pouvez également définir ce paramètre en remplaçant la fonction.


public function getTargets()
{
    //   self::CLASS_CONSTRAINT
    return self::PROPERTY_CONSTRAINT;
}

La propriété du message, comme vous pouvez le deviner, est utilisée pour afficher des informations sur une erreur de validation.


, .


public function validatedBy()
{
    return \get_class($this).'Validator';
}

, , ConstraintValidator. ConstraintValidator validate.


namespace App\Custom\Constraints;

use Symfony\Component\Validator\ConstraintValidator;

class TextTimeValidator extends ConstraintValidator
{
    /**
     *    
     *
     * @param mixed $value  
     * @param Constraint $constraint   
     */
    public function validate($value, Constraint $constraint)
    {
        $time = explode(':', $value);

        $hours = (int) $time[0];
        $minutes = (int) $time[1];

        if ($hours >= 24 || $hours < 0)
            $this->fail($constraint);
        else if ((int) $time[1] > 60 || $minutes < 0)
            $this->fail($constraint);
    }

    private function fail(Constraint $constraint) 
    {
        $this->context->buildViolation($constraint->message)
            ->addViolation();
    }
}

, , , .


...
    /**
     * @Assert\NotBlank()
     * @Assert\Regex(
     *     pattern="/\d{2}:\d{2}/",
     *     message=" "
     * )
     * @CustomAssert\TextTime()
     * @ORM\Column(type="string", length=5)
     */
    private $timeFrom;
...

, , . , , , message .


. , Symfony Time. : . , "hh:mm:ss". , .



, , - . , , : "hh:mm", "hh:mm"-"hh:mm". , , .


, AbstractComparsion. AbstractComparsion Constraint — .


namespace App\Custom\Constraints;

use Symfony\Component\Validator\Constraints\AbstractComparison;

/**
 * @Annotation
 * @Target({"PROPERTY"})
 */
class TextTimeInterval extends AbstractComparison
{   
    public $message = '       {{ compared_value }}.';
}


namespace App\Custom\Constraints;

use Symfony\Component\Validator\Constraints\AbstractComparisonValidator;

class TextTimeIntervalValidator extends AbstractComparisonValidator
{
    /**
     *       
     *
     * @param mixed $timeFrom   
     * @param mixed $timeTo   
     *
     * @return  true   , false 
     */
    protected function compareValues($timeFrom, $timeTo)
    {
        $compareResult = true;

        $from = explode(':', $timeFrom);
        $to = explode(':', $timeTo);

        try {
            if ((int) $from[0] > (int) $to[0])
                $compareResult = false;
            else if (((int) $from[0] == (int) $to[0]) && ((int) $from[1] > (int) $to[1])) {
                $compareResult = false;
            }
        } catch (\Exception $exception) {
            $compareResult = false;
        }

        return $compareResult;
    }
}

Ce n'est que dans ce cas (l'une des implémentations possibles) que nous remplaçons la fonction compareValues, qui retourne vrai / faux en cas de succès de la validation et est traitée dans AbstractComparisonValidator par la fonction validate ().


Ceci est essentiellement décrit comme suit


...
 /**
     * @Assert\NotBlank()
     * @Assert\Regex(
     *     pattern="/\d{2}:\d{2}/",
     *     message=" "
     * )
     * @CustomAssert\TextTime()
     * @CustomAssert\TextTimeInterval(
     *     propertyPath="timeTo",
     *     message="       "
     * )
     * @ORM\Column(type="string", length=5)
     */
    private $timeFrom;

    /**
     * @Assert\NotBlank()
     * @Assert\Regex(
     *     pattern="/\d{2}:\d{2}/",
     *     message=" "
     * )
     * @CustomAssert\TextTime()
     * @ORM\Column(type="string", length=5)
     */
    private $timeTo;
...

J'espère que cette petite analyse et ces exemples ont montré à quel point il est facile d'utiliser des validateurs personnalisés. Étant donné que, pour commencer, nous avons considéré les validateurs les plus simples, bien sûr, afin de ne pas surcharger la présentation de descriptions de sujets, nous n'avons pas donné d'exemples exceptionnels. Dans les prochains articles, je prévois de considérer des validateurs plus complexes et des cas spécifiques.


All Articles