Ajouter une validation conditionnelle à un formulaire Symfony
Publié le 26/03/2019 • Actualisé le 28/03/2019
Parfois, valider unitairement chacun des champs d'un formulaire n'est pas suffisant. Dans certains cas on a besoin d'une validation conditionnelle. C'est à dire que la validation d'un champ ou d'un groupe de champs dépend de la valeur d'un autre. Voici un exemple simple permettant de valider une date de fin, mais seulement si une valeur a été saisie. On verra aussi comment soumettre manuellement des valeurs à un formulaire sans utiliser la requête http en cours (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',
]);
}
}
En bonus, le snippet permettant d'utiliser ce code : 🎉<?php
declare(strict_types=1);
namespace App\Controller\Snippet;
use App\Form\EventCreateType;
use Symfony\Component\Form\FormFactoryInterface;
/**
* J'utilise un trait PHP afin d'isoler chaque snippet dans un fichier.
* Ce code doit être apellé d'un contrôleur Symfony étendant AbstractController (depuis Symfony 4.2)
* ou Symfony\Bundle\FrameworkBundle\Controller\Controller (Symfony <= 4.1).
* Les services sont injectés dans le constructeur du contrôleur principal.
*
* @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! 😁
}
}
Exécuter le snippet ≪ this.showUnitTest ? this.trans.hide_unit_test : this.trans.show_unit_test ≫ Plus sur Stackoverflow Lire la doc Snippet aléatoire
<?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);
}
}