[Symfony] Utilisation du validateur UniqueEntity sans utiliser les annotations

Publié le 04/07/2019 • Actualisé le 04/07/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

Quand on a un formulaire relatif à une entité, il est fréquent d'avoir des contraintes d'unicité pour certains champs, par exemple sur le nom d'utilisateur ou l'email. Je ne suis pas un grand fan de l'utilisation des annotations pour la validation donc voici comment utiliser ce validateur dans un FormType relatif à une entité. L'astuce ici réside dans le fait d'ajouter la contrainte au niveau du formulaire et pas d'essayer d'assigner la contrainte au champ dont nous voulons contrôler l'unicité. Dans cet exemple nous allons créer un formulaire factice pour l'entité Article de ce site, puis nous allons vérifier que la contrainte fonctionne en essayant de soumettre un slug déjà utilisé (celui de cet article), celui-ci devant être unique pour chaque article. Comme vous pouvez le constater, il n'y a pas d'identifiant de base de donnée dans les URLs des articles ou des snippets de ce site. Le slug (en anglais) est l'identifiant principal. Celui-ci est traduit en français quand on accède au contenu dans cette langue.


<?php

declare(strict_types=1);

// src/Form/ArticleType.php

namespace App\Form;

use App\Entity\Article;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\NotBlank;

/**
 * Fake article form for snippet30.
 */
final class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $article = $builder->getData();
        if (!$article instanceof Article) {
            throw new \RuntimeException('Invalid entity.');
        }

        if ($article->isArticle() || $article->isSnippet()) {
            $builder->add('slug', TextType::class, ['constraints' => [new NotBlank()]]);
        }
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'csrf_protection' => false,
            'data_class' => Article::class,
            'constraints' => [
                new UniqueEntity(['fields' => ['slug']]),
            ],
        ]);
    }
}
En bonus, le snippet permettant d'utiliser ce code : 🎉
<?php

declare(strict_types=1);

namespace App\Controller\Snippet;

use App\Entity\Article;
use App\Form\ArticleType;
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 Snippet30Trait
{
    /**
     * Test the validation with a set of values.
     */
    public function snippet30(): void
    {
        $slugs = [
            // This is the reference slug of this snippet (in english) so the validation must fail
            'on-using-the-uniqueentity-validator-without-annotation',

            // These ones should be OK
            'new-unique-slug',
            'other-new-unique-slug',
        ];

        // Manually submit values to the form. Note that the form creation is in the loop because a form can only be submitted once!
        foreach ($slugs as $slug) {
            $form = $this->formFactory->create(ArticleType::class, new Article());
            $form->submit(compact('slug'));
            if ($form->isValid()) {
                echo sprintf('✅ Form is valid! slug: "%s"', $slug);
            } else {
                echo sprintf('❌ Form is not valid: Error: "%s" (slug: "%s")', trim((string) $form->getErrors(true)), $slug);
            }
            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\Entity\Article;
use App\Form\ArticleType;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Form\FormFactoryInterface;

/**
 * @see Snippet30Trait
 */
final class Snippet30Test extends KernelTestCase
{
    private FormFactoryInterface $formFactory;

    protected function setUp(): void
    {
        $this->formFactory = self::getContainer()->get('form.factory');
    }

    /**
     * @return iterable<int, array{0: string, 1: bool}>
     */
    public function provide(): iterable
    {
        yield ['on-using-the-uniqueentity-validator-without-annotation', false];
        yield ['new-unique-slug', true];
        yield ['other-new-unique-slug', true];
    }

    /**
     * @see Snippet30Trait::snippet30
     *
     * @dataProvider provide
     */
    public function testSnippet30(string $slug, bool $isValid): void
    {
        $article = new Article();
        $article->setAuthor('COil');
        $form = $this->formFactory->create(ArticleType::class, $article);
        $form->submit(compact('slug'));
        self::assertSame($form->isValid(), $isValid);
    }
}