Add a custom conditional validation on a Symfony form

Published on 2019-03-26 • Modified on 2019-03-28

Sometimes validating each field of a form is not enough. In some cases you need a conditional validation. That means that the validation of a field or a set of fields will be conditioned by another field's value. Here is a simple example showing how to validate an end date, but only if a value was set. It will also show you how to manually submit values to a form without using the current request.


<?php

// src/Type/EventCreateType.php

namespace App\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * We set a global validation on the form. Not on a specific field.
 */
class EventCreateType extends AbstractType
{
    /**
     * We remove csrf as we manually submit values to the form.
     */
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'csrf_protection' => false,
            'constraints' => [
                new Callback([$this, 'validate']),
            ],
        ]);
    }

    /**
     * Valid if the end date if not set or if it is greater than the start date.
     * If the second test, we are sure both fields are DateTime objects.
     */
    public function validate(array $data, ExecutionContextInterface $context): void
    {
        if ($data['end_date'] && $data['start_date'] > $data['end_date']) {
            $context->buildViolation('The end date must be greater than the start date.')
                ->atPath('end_date')
                ->addViolation();
        }
    }

    /**
     * Only the start date is mandatory.
     */
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('start_date', DateType::class, [
            'widget' => 'single_text',
            'constraints' => [new NotBlank()],
        ]);

        $builder->add('end_date', DateType::class, [
            'widget' => 'single_text',
        ]);
    }
}
Bonus, the snippet to run this code: 🎉
<?php declare(strict_types=1);

namespace App\Controller\Snippet;

use App\Type\EventCreateType;
use Symfony\Component\Form\FormFactoryInterface;

/**
 * I am using a PHP trait in order to isolate each snippet in a file.
 * This code should be called from a Symfony controller extending AbstractController (as of Sf 4.2)
 * or Symfony\Bundle\FrameworkBundle\Controller\Controller (Sf <= 4.1)
 * Services are injected in the controller constructor.
 */
trait Snippet20Trait
{
    /**
     * Test the validation with a set of values.
     */
    public function snippet20(): void
    {
        if (!$this->formFactory instanceof FormFactoryInterface) {
            throw new \RuntimeException("Houston, we've got a problem! 💥");
        }
        $formValues = [
            [
                'start_date' => '2019-03-26', // valid
            ],
            [
                'start_date' => '2019-03-27',
                'end_date' => '2019-03-20', // NOT valid
            ],
            [
                'start_date' => '2019-03-28',
                'end_date' => '2019-03-29', // valid
            ],
        ];

        // Manually submit values to the form. Note that the form creation is in
        // the loop because a form can only be submitted once
        foreach ($formValues as $formValue) {
            $form = $this->formFactory->create(EventCreateType::class);
            $form->submit($formValue);
            if ($form->isValid()) {
                $startDate = $form->getData()['start_date'];
                echo 'Form is valid! start_date: '.$startDate->format('Y-m-d');
            } else {
                echo 'Form is not valid: '.$form->getErrors(true);
            }
            echo PHP_EOL;
        }

        // That's it! 😁
    }
}

 More on Stackoverflow   Read the doc