Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom Validation, Callback and Constraints

Custom Validation, Callback and Constraints

From Rafael:

Hi, I am coding one events calendar, It is adding events however how can I validate if the event I am placing does not conflict time with another one event? I was thinking about entity validation callback but should it be in entity? or repository? I don’t want to lose symfony validation that display errors on the forms

Answer

This is a great question because it touches on a few interesting and related concepts: custom validation, assigning errors, and the best practices around all of this.

Let’s follow along with your example. Suppose we have an Event entity that looks like this (with some extras, like getter and setter methods):

// src/KnpU/QADayBundle/Entity/Event.php
namespace KnpU\QADayBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="KnpU\QADayBundle\Entity\EventRepository")
 */
class Event
{
    /**
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /** @ORM\Column(name="name", type="string", length=255) */
    private $name;

    /** @ORM\Column(name="startDate", type="datetime") */
    private $startDate;

    /** @ORM\Column(name="endDate", type="datetime") */
    private $endDate;

    // ...
}

I also have a really basic route, controller and form setup which allows the user to create a new Event (check out the code download to see this). Ok, let’s get to work!

The Callback Constraint

The goal is to throw a validation error if the event will conflict with the start and end times of some existing event. There are a few ways to add custom validation, including the Callback constraint, which executes an arbitrary method in your model/entity class and lets you apply any custom logic you want:

// src/KnpU/QADayBundle/Entity/Event.php
// ...
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ExecutionContextInterface;

/**
 * @Assert\Callback(methods={"checkCustomValidation"})
 */
class Event
{
    // ...

    public function checkCustomValidation(ExecutionContextInterface $context)
    {
        $context->addViolationAt('name', 'Pick a cooler name!');
    }
}

This is my favorite way to handle custom validation because it’s so easy. The problem is that the method lives in your entity. This means that you don’t have access to the entity manager or any other services. In this case, there’s no way to query to see if any other event has a conflicting date.

A bit Ugly, but Easy: Callback + constraints

Normally, we add validation constraints to our model class (i.e. Event). However, as of Symfony 2.1, additional constraints can be added directly to the form key using a constraints option. Like with annotations, you can apply constraints to the whole object, or individual properties.

For simplicity, I’ve built my form in the controller instead of using a form type class. Let’s re-use the Callback validator, but now tell it to execute a method on my controller when called:

// src/KnpU/QADayBundle/Controller/EventController.php

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\ExecutionContextInterface;
use KnpU\QADayBundle\Entity\Event;
// ...

public function newAction(Request $request)
{
    $form = $this->createFormBuilder(null, array(
        'data_class' => 'KnpU\QADayBundle\Entity\Event',
        'constraints' => array(
            new Assert\Callback(array($this, 'validateEventDates'))
        )
    ))
        ->add('name', 'text')
        ->add('startDate', 'datetime')
        ->add('endDate', 'datetime')
        ->getForm()
    ;

    // ...
}

And for now, I’ve just put some dummy code into the validateEventDates function, which lives right inside this same class:

// src/KnpU/QADayBundle/Entity/EventController.php
public function validateEventDates(Event $event, ExecutionContextInterface $context)
{
    $context->addViolationAt('startDate', 'There is already an event during this time!');
}

Phew! Let’s walk through this step-by-step:

1) We eventually want to validate our object based on multiple pieces of data (the startDate and endDate). So instead of applying a validator to a single field, we apply it to the whole object. This means that when the validateEventDates is called, the whole Event object is passed to it.

2) To attach validation constraints directly to the form, we use the constraints key and create a new instance of the constraint. Whether you realized it or not, all those Callback, NotBlank, etc keys that you use every day for validation are each a real class.

3) When the Callback constraint is executed, it detects that we’re no longer inside the Event class. To help us out, it now passes our method two arguments: the Event object and the execution context.

Note

The Callback constraint - or any other constraint - can also be applied to just an individual field by adding a third argument to the add function, which would be an array with a constraints key.

Tip

If your form lives in a form type class, simply add the constraints key to the setDefaulOptions method.

This solution is a bit ugly because it lives in our Controller, so we can’t re-use it or unit test it. We’ll improve that in a second, but let’s get it working first!

Applying the Validation Logic

Now that the callback method lives in the controller, we can easily access the entity manager (or any other service) and run the queries we need to. And since we are going to be executing some queries, the best place for that logic is in the EventRepository class:

// src/KnpU/QADayBundle/Entity/EventRepository.php
namespace KnpU\QADayBundle\Entity;

use Doctrine\ORM\EntityRepository;

class EventRepository extends EntityRepository
{
    public function findOverlappingWithRange(\DateTime $startDate, \DateTime $endDate)
    {
        $qb = $this->createQueryBuilder('e');

        return $qb->andWhere('e.startDate < :endDate AND e.endDate > :startDate')
            ->setParameter('startDate', $startDate)
            ->setParameter('endDate', $endDate)
            ->getQuery()
            ->execute()
        ;
    }
}

Great! Now use this function in the callback method in the controller:

// src/KnpU/QADayBundle/Controller/EventController.php
public function validateEventDates(Event $event, ExecutionContextInterface $context)
{
    $conflicts = $this->getDoctrine()
        ->getRepository('QADayBundle:Event')
        ->findOverlappingWithRange($event->getStartDate(), $event->getEndDate())
    ;

    if (count($conflicts) > 0) {
        $context->addViolationAt(
            'startDate',
            'There is already an event during this time!'
        );
    }
}

Tip

If this method lives in your form type class, then you don’t have the entity manager! One option is to pass it in as an option when creating your form:

$form = $this->createForm(new EventType, null, array(
    'em' => $this->getDoctrine()->getManager()
))

The em option is then available in the buildForm method of the form type class:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $em = $options['em'];
}

For this to work, make sure to add em to the “defaults” in your form type’s setDefaultOptions method.

If you try it, it works! It’s a bit dirty, but at least our query logic lives in EventRepository. If you were also handling “edits”, you’d also need to make sure that the result isn’t the exact object being saved. But I’ll leave that to you!

Creating a Proper Custom Validation Constraint

There’s nothing wrong with what we have so far, but for the sake of reusability, clean code and unit testing, it can be much better.

The ultimate solution to custom validation is to create your own constraint. Fortunately, we’ve already done most of the work. Start by creating a new UniqueEventDate class:

// src/KnpU/QADayBundle/Validator/UniqueEventDate.php
namespace KnpU\QADayBundle\Validator;

use Symfony\Component\Validator\Constraint;

/** @Annotation */
class UniqueEventDate extends Constraint
{
    public function validatedBy()
    {
        return 'unique_event_date';
    }

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

Yep, this class is so simple it’s silly. Each custom validation constraint is actually two classes: one “Constraint” (seen here) that holds some options and another “Constraint Validator” (shown next) which does all the work. In fact, you can find these for the built-in constraints, for example NotBlank and NotBlankValidator.

There are 3 interesting parts to this class:

1) The @Annotation will eventually allow us to reference this constraints in the Event class via, well, annotations.

2) The validatedBy tells Symfony about the “Constraint Validator” that will actually do the heavy lifting. The unique_event_date string shouldn’t make sense yet - but it’ll be more obvious in a minute.

3) The getTargets method defines whether this constraint can be applied to an entire class, a property, or both. Again, since we need multiple values on Event in order to make our validation decision, we will apply the constraint to the entire class.

Tip

This example doesn’t use any constraint options. If you do want to see what it looks like to have a constraint that has configurable options, see the core Email and EmailValidator classes.

Next, create the “Constraint Validator” class:

// src/KnpU/QADayBundle/Validator/UniqueEventDateValidator.php
namespace KnpU\QADayBundle\Validator;

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

class UniqueEventDateValidator extends ConstraintValidator
{
    private $em;

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

    public function validate($object, Constraint $constraint)
    {
        die('hold on, we\'ll fill finish this in a second...');
    }
}

In a second, we’ll fill this class in and have it do all the validation work. But first, register it as a service and tag it with a special validator.constraint_validator tag:

# src/KnpU/QADayBundle/Resources/config/services.yml
services:
    unique_event_date_validator:
        class: KnpU\QADayBundle\Validator\UniqueEventDateValidator
        arguments:
            - "@doctrine.orm.entity_manager"
        tags:
            -
                name: validator.constraint_validator
                alias: unique_event_date

Note

Make sure this services.yml file is being imported, either by using an imports key in app/config/config.yml or via a Dependency Injection Extension class (see Episode 3 for more on this).

Notice that the alias we use with the tag corresponds with the value that the Constraint class returns in validateBy. This is how Symfony knows that the UniqueEventDateValidator is the real muscle behind the UniqueEventDate constraint.

Ok! Before we fill in the logic in the validate method, let’s try this out! The new constraint isn’t magically activated - we activate it like any other constraint, with annotations (or YAML, if you prefer):

// src/KnpU/QADayBundle/Entity/Event.php
// ...

use KnpU\QADayBundle\Validator\UniqueEventDate;

/**
 * @ORM\Entity(repositoryClass="KnpU\QADayBundle\Entity\EventRepository")
 * @UniqueEventDate()
 */
class Event
{
    // ...
}

When you submit the form, the UniqueEventDate constraint is triggered, and ultimately the UniqueEventDateValidator::validate method is called. In other words, you’ll see our die statement print.

Ok, let’s finish this! Copy the logic from the controller validateEventDates method and remove it and the constraints option while you’re there. Paste it into UniqueEventDateValidator::validate and adjust it accordingly:

// src/KnpU/QADayBundle/Validator/UniqueEventDateValidator.php
public function validate($object, Constraint $constraint)
{
    $conflicts = $this->em
        ->getRepository('QADayBundle:Event')
        ->findOverlappingWithRange($object->getStartDate(), $object->getEndDate())
    ;

    if (count($conflicts) > 0) {
        $this->context->addViolationAt('startDate', 'There is already an event during this time!');
    }
}

Let’s walk through the differences:

1) Since we’ve injected Doctrine’s Entity Manager, we can access it and get the EventRepository through $this->em.

2) Since we applied the UniqueEventDate constraint to the Event class, the entire Event object is passed as the first argument to this method (i.e. $object).

3) The ExecutionContext is stored automatically on the $this->context property.

That’s it! When you re-submit the form, the UniqueEventDate constraint on Event activates this method, which does all the work.

Through all of this, one nice thing is that we were always in complete control of which field our error was attached to. I chose to attach the error to the startDate field, but you can use whatever makes sense to you. If you use the addViolation method instead, the error will be attached to the whole form and displayed at the top:

$this->context->addViolation('There is already an event during this time!');

Ok, start validating!

Leave a comment!

11
Login or Register to join the conversation
Default user avatar
Default user avatar PM | posted 2 years ago | edited

Not working in phpunit test :(

App\Validator\Constraints\ExistsValidator::__construct(), 0 passed in (...)vendor/symfony/validator/ConstraintValidatorFactory.php on line 43 and exactly 1 expected<br />

Reply
weaverryan Avatar weaverryan | SFCASTS | PM | posted 2 years ago | edited

Hey PM!

Yea, this is a pretty ancient tutorial now :). From the error, it sounds like your ExistsValidator has a constructor argument? What is that constructor argument? And is your code working normally (i.e. outside of phpunit)?

Cheers!

Reply
Default user avatar
Default user avatar Paul Rijke | posted 3 years ago

Although not entirely up to date anymore, I love the way you show how to handle this kind of problems. As it turned out, I exactly needed this logic so I could reuse it easily. Thanks

One question though: Why did you rewrite the query to a much complexer syntax using orX and andX?
/*
return $this->createQueryBuilder('e')
->andWhere('(e.startDate < :endDate AND e.endDate > :startDate) OR (e.endDate > :startDate AND e.startDate < :endDate)')
->setParameter('startDate', $startDate)
->setParameter('endDate', $endDate)
->getQuery()
->execute()
;
*/

$qb = $this->createQueryBuilder('e');

$expr1 = $qb->expr()->andX('e.startDate < :endDate AND e.endDate > :startDate');
$expr2 = $qb->expr()->andX('e.endDate > :startDate AND e.startDate < :endDate');
$orExpr = $qb->expr()->orX($expr1, $expr2);

$qb->andWhere($orExpr)
->setParameter('startDate', $startDate)
->setParameter('endDate', $endDate)
;

return $qb
->getQuery()
->execute()
;

Reply

Hey Paul Rijke!

Lol, yes, this is now ancient! But I'm glad it was still somewhat useful for you!

One question though: Why did you rewrite the query to a much complexer syntax using orX and andX?

Wait, do you mean, why didn't I use orX and andX? If I've understood correctly (I don't always!) I've always found the expressions stuff super unreadable. In other words:


// Yay! I like this! Yes, there are some parentheses - it *is* a bit complex, but it makes sense to me
// I might break this into multiple lines and indent it to help make the groups easier to read, but like this
->andWhere('(e.startDate < :endDate AND e.endDate > :startDate) OR (e.endDate > :startDate AND e.startDate < :endDate)')

// this actually confuses me :)
$expr1 = $qb->expr()->andX('e.startDate < :endDate AND e.endDate > :startDate');
$expr2 = $qb->expr()->andX('e.endDate > :startDate AND e.startDate < :endDate');
$orExpr = $qb->expr()->orX($expr1, $expr2);
``

So, totally subjective, and maybe cause I did write raw SQL queries for enough time that the string version feels better. I think part of the problem might be that the idea behind the expression stuff is solid, but the way it's implemented... or just doesn't ultimately "read" very clearly to me.

Or... maybe I answered the wrong question 😂 - let me know!

Cheers!
Reply
Default user avatar

LOL, indeed in the text it use SQL expressions which are much clearer to me as well. But in the downloaded code you? replaced it with the andX and orX expressions. So I wondered why that was. The latter confuses me as well.

Reply

Ah! So I did! I had no idea! The way we built the code downloads back then was not as systematic as it is now, but I can't think of why I would have used that format instead. This is one of those "choose whichever one you like" situations where I am not aware of any technical disadvantage of either. So for you and I, stick with the SQL-style :).

Cheers!

Reply
Default user avatar
Default user avatar Shairyar Baig | posted 5 years ago

Hi Ryan,

What if I want to validate two form fields against each other?

For example there are two fields on a form number 1 and number 2, i want to check if number 2 is not greater than number 1, how would I do this?

Regards,
Baig

Reply

Hey Shairyar!

My goto for this situation is exactly what I'm talking about this post: the Callback constraint. The callback function will be passed your object (e.g. Event) and you can get the values from the two fields and compare them.

Is that what you're looking for?

Reply
Default user avatar
Default user avatar Shairyar Baig | weaverryan | posted 5 years ago

Thanks Ryan,

I was trying to do the validation via custom Validator, this helped alot. Thanks

Reply
Default user avatar

Hello , I'm trying this in symfony 2.8 but apparently it's not working anymore ??
I want to set constraints with a collection in a form class like so

public function configureOptions(OptionsResolver $resolver)

{

//TODO move constraints in the entity

$collectionConstraint = new Collection(array(

'titre' => array(

new Assert\NotNull(),

new Assert\Length(array('min'=>2, 'max'=>255))

),

'livre' => array(

new Assert\NotBlank()

),

'type' => array(

new Assert\NotBlank(),

),

'prix' => array(

new Assert\NotNull(),

new Assert\Type('numeric')

),

'ordre' => array(

new Assert\NotNull(),

new Assert\Type('integer')

)

));

return array(

'data_class' => 'DP\SkoazellBundle\Entity\Chapitres',

'constraints' => $collectionConstraint //or validation_constraint ?? was this removed ??

);

}

public function getBlockPrefix()

{

return 'add_chapitre';

}

But it's not working ? I think this option might have been removed in favour of validation constraints on the entity class somewhere around symfony 2.3 ?

Reply

Hi there!

Sorry for the slow reply - for some reason your comment was put into the spam filter!

I don't know why it's not working, but using the "constraints" option is still as valid in Symfony 2.8, as it was in Symfony 2.3 - and the option *is* called "constraints". So, I'm not sure what could be going wrong :/

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

userVoice