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
declare(strict_types=1);
// src/Form/EventCreateType.php
namespace App\Form;
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.
*/
final 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.
*
* @param array<string,mixed> $data
*/
public function validate(array $data, ExecutionContextInterface $context): void
{
if (($data['end_date'] instanceof \DateTime) && $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.
*
* @param array<string,mixed> $options
*/
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\Form\EventCreateType;
use Symfony\Component\Form\FormFactoryInterface;
/**
* I am using a PHP trait to isolate each snippet in a file.
* This code should be called from a Symfony controller extending AbstractController (as of Symfony 4.2)
* or Symfony\Bundle\FrameworkBundle\Controller\Controller (Symfony <= 4.1).
* Services are injected in the main controller constructor.
*
* @property FormFactoryInterface $formFactory
*/
trait Snippet20Trait
{
/**
* Test the validation with a set of values.
*/
public function snippet20(): void
{
$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()) {
/** @var array{start_date: \DateTime, end_date: \DateTime|null} */
$data = $form->getData();
$startDate = $data['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! 😁
}
}
Run this snippet ≪ this.showUnitTest ? this.trans.hide_unit_test : this.trans.show_unit_test ≫ More on Stackoverflow Read the doc Random snippet
<?php
declare(strict_types=1);
namespace App\Tests\Integration\Controller\Snippets;
use App\Form\EventCreateType;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Form\FormFactoryInterface;
/**
* @see Snippet20Trait
*/
final class Snippet20Test extends KernelTestCase
{
private FormFactoryInterface $formFactory;
protected function setUp(): void
{
$this->formFactory = self::getContainer()->get('form.factory');
}
/**
* @return iterable<int, array{0: string, 1: ?string, 2: bool}>
*/
public function provide(): iterable
{
yield ['2019-03-26', null, true];
yield ['2019-03-27', '2019-03-20', false];
yield ['2019-03-28', '2019-03-29', true];
}
/**
* @see Snippet20Trait::snippet20
*
* @dataProvider provide
*/
public function testSnippet20(string $startDate, ?string $endDate, bool $isValid): void
{
$form = $this->formFactory->create(EventCreateType::class);
$form->submit([
'start_date' => $startDate,
'end_date' => $endDate,
]);
self::assertSame($form->isValid(), $isValid);
}
}