2
votes

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 'ahahs@mail.fr' pour la clef 'UNIQ_8D93D649E7927C74'

2

2 Answers

3
votes

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,
        ])
        ;
0
votes

You can create custom validation constraint

class UniqueEmailValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint instanceof UniqueEmail) {
            throw new UnexpectedTypeException($constraint, UniqueEmail::class);
        }

        if (!\is_array($value) && !$value instanceof \IteratorAggregate) {
            throw new UnexpectedValueException($value, 'array|IteratorAggregate');
        }

        $emails = [];
        foreach ($value as $element) {
            if (\in_array($element->getEmail(), $emails, true)) {
                $this->context->buildViolation($constraint->message)
                    ->addViolation();

                return;
            }

            $emails[] = $element->getEmail();
        }
    }
}

and add to your property a validation annotation

class RegistrationCollection
{

    /**
     * @AppAssert\UniqueEmailValidator
     */
    private $utilisateurs = [];

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

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

        return $this;
    }
}