Ajouter une validation conditionnelle à un formulaire Symfony

Publié le 26/03/2019 • Actualisé le 28/03/2019


English language detected! 🇬🇧

  We noticed that your browser is using English. Do you want to read this post in this language?

Read the english version 🇬🇧 Close

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

  Travaillez avec moi !

<?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);
    }
}