Custom Validation Basics in Symfony 4/5 with Examples

In Symfony, in my opinion, a very convenient functionality for entity validation. In particular, the use of the annotation format for configuring validation rules really bribes me. In the vast majority of tasks, turnkey solutions cover standard cases. But, as you know, validation is a delicate matter, and you never know what restrictions you will have to impose this time. On the other hand, more flexible and well-thought-out validation will always help to avoid user errors.


Under the cut, I invite you to see how easy it is to write your restrictions and extend the comparisons of two fields that are available using the example of basic validation and validation. The article may be of interest to those who are still little familiar with validation in Symfony or who have bypassed the possibility of writing their own validators.


In this article we will keep in mind the classic context of validation. There is a certain entity, the data on which is filled out from the corresponding form, while validation imposes restrictions on any aspect of the data entered. It is worth noting that validators can be used without restrictions for controlling internal logic, API validation, etc.


We immediately determine that the proposed examples are not the only possible solutions and do not claim maximum optimality or extensive use, but are offered for demonstration purposes. On the other hand, parts of the examples are taken and simplified from combat projects, and I hope that they can be useful in illustrating the simplicity of writing custom validations without interruption from reality.


Basic validation


In order to make a fully functional validation, you need to create only two classes - the heirs of Constraint and ConstraintValidator. Constraint, as the name implies, defines and describes the constraints, while the ConstraintValidator validates them. As an example, we will write a validator for time in the format "hh: mm" stored in a text format. In the official documentation, it is proposed that Constraint describe the public properties of the constraint. So let's do it.


namespace App\Custom\Constraints;

use Symfony\Component\Validator\Constraint;

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

Here, the Target annotation determines whether validation will be used: for a property or for a class. You can also set this parameter by overriding the function.


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

The message property, as you might guess, is used to display information about a validation error.


, .


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

Only in this case (one of the possible implementations) we override the compareValues ​​function, which returns true / false on the validation success and is processed in the AbstractComparisonValidator by the validate () function.


This is essentially described as follows


...
 /**
     * @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;
...

Hopefully this little analysis and examples have shown how easy it is to use custom validators. Since for starters we considered the simplest validators, of course, in order not to overload the presentation with subject descriptions, we failed to give exceptional examples. In future articles I plan to consider more complex validators and specific cases.


All Articles