Symfony2: Using Validation Groups

Validation groups are covered in the Symfony2 documentation but are split between the sections of validation and forms. In this post I will look at using them when you have multiple forms for a single entity which is one of the most common use cases.

A Brief Introduction to Validation in Symfony2

In Symfony2 applications validation constraints are applied to the Entity and not to the Form. Strictly speaking any PHP class can be validated, there is nothing special about the class the validation is being applied to. The class does need to extend anything, it is passed to the validation service which performs the validation based on the metadata about the class whether that is stored as XML, YAML, PHP or as annotations in the class file. I'm just going to give examples with annotations here, details of using the others can be found in the official docs.

So let's say we have a Profile class representing the information about a user on our application. We can set the validation constraints we want on our fields with annotations:

use Symfony\Component\Validator\Constraints as Assert;

class Profile
{
    /**
     * @Assert\MaxLength(100)
     * @Assert\NotBlank
     */
    private $givenName;

    /**
     * @Assert\MaxLength(100)
     * @Assert\NotBlank
     */
    private $familyName;

    /**
     * @Assert\Email
     * @Assert\NotBlank
     */
    private $email;

    /**
     * @Assert\MaxLength(50)
     * @Assert\NotBlank
     */
    private $phoneNumber;

    /**
     * @Assert\MaxLength(50)
     */
    private $mobileNumber;

    /**
     * @Assert\Country
     * @Assert\NotBlank
     */
    private $country;

    //--
}

The validator service will let us validate the fields of our Profile class, this can be injected into any of our services that need to validate an object:


class ProfileManager
{
    private $validator;

    public function __construct($validator)
    {
        $this->validator = $validator
    }

    public function createProfile()
    {
        $profile = new Profile();
        // ... set the fields on the profile

        $errors = $this->validator->validate($profile);

        if (count($errors) > 0) {
            // boo, its invalid
        } else {
            //hooray, everything is valid
        }
    }
}
<services>

     <service id="profile_manager" class="ProfileManager">
         <argument type="service" id="validator"/>
     </service>

</services>

Whilst this is very useful, a lot of the time we will want to use a form to allow users to add/edit their profile and then validate the model via the form. So we may have a form class that looks like this:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class ProfileFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
	    ->add('givenName')
	    ->add('familyName')
            ->add('email')
            ->add('phoneNumber')
            ->add('mobileNumber')
            ->add('country')
        ;
    }

    public function getName() {
        return 'profile';
    }
}

We can then make use of this form class in a controller action:

$form = $this->createForm(new ProfileFormType(), new Profile);
if ('POST' === $request->getMethod()) {
    $form->bindRequest($request);
    if ($form->isValid()) {
       //whoop-de-doo its valid
    }
}

It can look from this as though the validation is tied to the form since we ask if the form is valid. This is a very useful shortcut method but slightly misleading. What is actually being validated is the Profile object we created and passed to the createForm method of the controller after it has had the data from the POST request added to it.

The advantage to letting the form interact with the validation service really comes into its own when you have forms with collections and embedded forms as it ensures that all the objects involved are valid for you. This is then a real time saver but it is helpful to remember that the form is actually using the validator service to validate the associated entities.

Multiple Forms with one Entity

One potential disadvantage of associating validation with the entity is that for many applications you do not have a one to one relationship between models and forms. If we discover that asking for a lot of profile information during registration is putting users off signing up, then we might decide to split up asking for this information. We just want to ask for some basic information as part of registration and then ask for the rest of the profile information afterwards. We now have the following forms. The first for pre-registration, where we just ask for their name and email address:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class PreRegProfileFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
	    ->add('givenName')
	    ->add('familyName')
            ->add('email')
        ;
    }

    public function getName() {
        return 'profile_pre_reg';
    }
}

The second contains those fields so they can edit them and also their phone numbers and country:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class ProfileFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
	    ->add('givenName')
	    ->add('familyName')
            ->add('email')
            ->add('phoneNumber')
            ->add('mobileNumber')
            ->add('country')
        ;
    }

    public function getName() {
        return 'profile';
    }
}

The problem we now have is that when we validate the pre-registration form it will fail because phoneNumber and country are required fields but the user will not have filled out these fields as they were not in the form. We do not want to just remove the validation relevant to the second form from the Profile class, whilst the first form will then be able to be successfully validated, the second will not have its additional fields validated.

Validation Groups to the Rescue

This is not a reason to change from having the association between the entity and validation and go to associate validation with forms. Conceptually validation belongs with the entity, it is what is being validated, the form is just one means of setting the data on the entity.

This issue can instead be solved using validation groups. The validation groups allow you to specify which validation assertions should be used when you are validating a model. The group is specified in the configuration for the constraint, if no group is specified the constraint will be part of the Default group. In our case we want to set a group for fully validating a profile but by default only validate the pre-registration fields:

use Symfony\Component\Validator\Constraints as Assert;

class Profile
{
    /**
     * @Assert\MaxLength(100)
     * @Assert\NotBlank
     */
    private $givenName;

    /**
     * @Assert\MaxLength(100)
     * @Assert\NotBlank
     */
    private $familyName;

    /**
     * @Assert\Email
     * @Assert\NotBlank
     */
    private $email;

    /**
     * @Assert\MaxLength(limit=50, groups={"full"})
     * @Assert\NotBlank(groups={"full"})
     */
    private $phoneNumber;

    /**
     * @Assert\MaxLength(50);
     */
    private $mobileNumber;

    /**
     * @Assert\Country(groups={"full"})
     * @Assert\NotBlank(groups={"full"})
     */
    private $country;

We can now choose which group of constraints we are interested in validating against. So when the user first joins up we will not want the second stage constraints to be enforced so it is only the Default group we want to be enforced. This is the group that will be used anyway for the form, so when we check if our pre-registration form is valid only the constraints that do not have a group specified will be checked and our full constraints will be ignored.

For the full form we want to specify that both the default and the full constraints are applied which we can do in the getDefaultOptions method of our form type class:

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;

class ProfileFormType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
	    ->add('givenName')
	    ->add('familyName')
            ->add('email')
            ->add('phoneNumber')
            ->add('mobileNumber')
            ->add('country')
        ;
    }

    public function getName() {
        return 'profile';
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'validation_groups' => array('full', 'Default')
        );
    }
}

Edit: In Symfony 2.1 the getDefaultOptions method has been deprecated, you should use setDefaultOptions instead:

public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            //...
            'validation_groups' =>  array('full', 'Default'),
        ));
    }

Note that we have to specify the Default group as this will not automatically be included if we add a different group to the array. All the fields will now be validated, if we only want to validate the fields in the full group we could have just left out Default from the groups.

You can also use the groups outside of forms by specifying them as the second argument when using the validation service directly:

$profile = new Profile();
// ... set the fields on the profile

$errors = $this->validator->validate($profile, array('full', 'Default'));

Multiple Groups

It's forcing it a bit with our example, but let's imagine that we want to split our number of forms further into a series of steps then we can use multiple groups, you can even assign multiple groups to a constraint:

use Symfony\Component\Validator\Constraints as Assert;

class Profile
{
    /**
     * @Assert\MaxLength(limit=100, groups={"step1"})
     * @Assert\NotBlank(groups={"step1"})
     */
    private $givenName;

    /**
     * @Assert\MaxLength(limit=100, groups={"step1"})
     * @Assert\NotBlank(groups={"step1"})
     */
    private $familyName;

    /**
     * @Assert\Email(groups={"step1", "step2"})
     * @Assert\NotBlank(groups={"step1", "step2"})
     */
    private $email;

    /**
     * @Assert\MaxLength(limit=50, groups={"step2"})
     * @Assert\NotBlank(groups={"step2"})
     */
    private $phoneNumber;

    /**
     * @Assert\MaxLength(limit=50, groups={"step2"});
     */
    private $mobileNumber;

    /**
     * @Assert\Country(groups={"step3"})
     * @Assert\NotBlank(groups={"step3"})
     */
    private $country;

These groups can then be combined as you see fit for a form. This is particularly useful if you need to create a wizard type form with a quite a few steps.

Wrapping Up

Using validation groups allows us to make validation much more flexible than it would be otherwise. Without the validation groups we would not be able to selectively validate an entity which would cause some real issues with applications with multiple forms. They can be used to separate an entity's constraints into separate forms but this is just a way of using them rather than all there is to them. There is still no direct tie introduced between the groups and the forms, so a form can use multiple groups and the groups can also be used outside of the context of forms.