[Symfony] On using the UniqueEntity validator without annotation

Published on 2019-07-04 • Modified on 2019-07-04

When having form for entities, it is common to have unique constraints, for example on the username or the email of a user. I am not a big fan of using annotations for validation so here is how to add this kind of validation when having a form type for an entity. The trick here is to add the constraint at the form level and not a the field level. In this example we will create a fake type for the Article entity of this website, then we will check that the unique constraint is working for the slug as it must be unique for each article. As you can see there is no database id in the URL to identify the snippets or articles: the slug is the main identifier.


<?php declare(strict_types=1);

// src/Type/ArticleType.php

namespace App\Type;

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.
 */
class ArticleType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $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']])
            ],
        ]);
    }
}
Bonus, the snippet to run this code: 🎉
<?php declare(strict_types=1);

namespace App\Controller\Snippet;

use App\Type\ArticleType;
use Symfony\Component\Form\FormFactoryInterface;

/**
 * I am using a PHP trait in order 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.
 */
trait Snippet30Trait
{
    /**
     * Test the validation with a set of values.
     */
    public function snippet30(): void
    {
        if (!$this->formFactory instanceof FormFactoryInterface) {
            throw new \RuntimeException("Houston, we've got a problem! 💥");
        }

        $slugs = [
            // This is the reference slug of this snippet (in english) so the validation must fail
            'on-using-the-uniqueentity-validator-without-annotation',

            // This one should be OK
            '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);
            $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")', $form->getErrors(true), $slug);
            }
            echo PHP_EOL;
        }

        // That's it! 😁
    }
}

 Run this snippet   Read the doc