Search code examples
symfonyvalidationcollectionsconstraintsunique

Symfony - Unique entity in CollectionType field?


I have a formBuilder that contains a collectionType. I would like to be able to put a constraint on the email field to be sure that when the user validates, there is not the same email address several times in the form

I've :

RegistrationCollectionType.php

$builder
        ->add('utilisateurs', CollectionType::class, [
            'entry_type' => RegistrationType::class,
            'entry_options' => [
                'label' => false,
                'entreprise' => $entreprise,
            ],
            'allow_add' => true,
            'allow_delete' => true,
            'delete_empty' => true,
            'by_reference' => true,
            'prototype' => true,
            'label' => false,
            'attr' => [
                'class' => 'my-selector',
                'label' => false,
            ],
            'by_reference' => false,
        ])
        ;

With his class :

RegistrationCollection.php

class RegistrationCollection
{


    private $utilisateurs = [];

    public function getUtilisateurs(): ?array
    {
        return $this->utilisateurs;
    }

    public function setUtilisateurs(?array $utilisateurs): self
    {
        $this->utilisateurs = $utilisateurs;

        return $this;
    }
}

And in my RegistrationType.php which is associated with my User entity, I've :

RegistrationType.php

->add('email', EmailType::class, [
                'attr' => [
                    'placeholder' => "Adresse email"
                ],
            ])

enter image description here

Now if I valid, I've :

SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicata du champ '[email protected]' pour la clef 'UNIQ_8D93D649E7927C74'


Solution

  • I kept the idea of a custom constraint, but which would not apply only to emails but to any field that we want Unique:

    #App\Validator\Constraints\UniqueProperty.php
    
    <?php
    
    namespace App\Validator\Constraints;
    
    use Symfony\Component\Validator\Constraint;
    
    /**
     * @Annotation
     */
    class UniqueProperty extends Constraint
    {
        public $message = 'This collection should contain only elements with uniqe value.';
        public $propertyPath;
    
        public function validatedBy()
        {
            return UniquePropertyValidator::class;
        }
    }
    

    and

    #App\Validator\Constraints\UniquePropertyValidator.php
    
    <?php
    
    namespace App\Validator\Constraints;
    
    use Symfony\Component\PropertyAccess\PropertyAccess;
    use Symfony\Component\Validator\Constraint;
    use Symfony\Component\Validator\ConstraintValidator;
    use Symfony\Component\Validator\Exception\UnexpectedTypeException;
    use Symfony\Component\Validator\Exception\UnexpectedValueException;
    
    class UniquePropertyValidator extends ConstraintValidator
    {
        /**
         * @var \Symfony\Component\PropertyAccess\PropertyAccessor
         */
        private $propertyAccessor;
    
        public function __construct()
        {
            $this->propertyAccessor = PropertyAccess::createPropertyAccessor();
        }
    
        /**
         * @param mixed $value
         * @param Constraint $constraint
         * @throws \Exception
         */
        public function validate($value, Constraint $constraint)
        {
            if (!$constraint instanceof UniqueProperty) {
                throw new UnexpectedTypeException($constraint, UniqueProperty::class);
            }
    
            if (null === $value) {
                return;
            }
    
            if (!\is_array($value) && !$value instanceof \IteratorAggregate) {
                throw new UnexpectedValueException($value, 'array|IteratorAggregate');
            }
    
            if ($constraint->propertyPath === null) {
                throw new \Exception('Option propertyPath can not be null');
            }
    
            $propertyValues = [];
            foreach ($value as $key => $element) {
                $propertyValue = $this->propertyAccessor->getValue($element, $constraint->propertyPath);
                if (in_array($propertyValue, $propertyValues, true)) {
    
                    $message = sprintf("%s (%s)", $constraint->message, $propertyValue);
    
                    $this->context->buildViolation($message)
                        // ->atPath(sprintf('[%s]', $key))
                        ->atPath(sprintf('[%s][%s]', $key, $constraint->propertyPath))
                        ->addViolation();
                }
    
                $propertyValues[] = $propertyValue;
            }
        }
    }
    

    and

    class RegistrationCollection
    {
    
    
        /**
         * @App\UniqueProperty(
         *      message = "Adresse mail déjà utilisée",
         *      propertyPath = "email"
         * )
         *
         */
        private $utilisateurs = [];
    

    It works very well, except that I can't target the child field for the error. Systematically, the error will go to the parent entity, and therefore the error will be put all over it.

    I tried in the validator to redirect to the fields of the child entity concerned, but nothing to do, the error continues to put everything above..

    enter image description here

    enter image description here

    In my FormType I tried to disable error_bubbling but same thing

    ->add('utilisateurs', CollectionType::class, [
                'entry_type' => RegistrationType::class,
                'entry_options' => [
                    'label' => false,
                    'entreprise' => $entreprise,
                ],
                'allow_add' => true,
                'allow_delete' => true,
                'delete_empty' => true,
                'by_reference' => true,
                'prototype' => true,
                'label' => false,
                'attr' => [
                    'class' => 'my-selector',
                    'label' => false,
                ],
                'by_reference' => false,
                'error_bubbling' => false,
            ])
            ;