[Symfony] Utilisation du validateur UniqueEntity sans utiliser les annotations
Publié le 04/07/2019 • Actualisé le 04/07/2019
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
<?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);
}
}