symfonyvalidationsymfony-validator

Use UniqueEntity outside of entity and without forms


I need to validate an email passed by user:

private function validate($value): bool
{
    $violations = $this->validator->validate($value, [
        new Assert\NotBlank(),
        new Assert\Email(),
        new UniqueEntity([
            'entityClass' => User::class,
            'fields' => 'email',
        ])
    ]);

    return count($violations) === 0;
}

But UniqueEntity constraint throws an exception:

Warning: get_class() expects parameter 1 to be object, string given

Seems like ValidatorInterface::validate() method's first argument awaiting for Entity object with getEmail() method, but it looks ugly.

Is there any elegant way to validate uniqueness of field passing only scalar value to ValidatorInterface::validate() method?


Solution

  • Seems like there is no built-in Symfony solution to do what I want, so I created custom constraint as Jakub Matczak suggested.

    UPD: This solution throws a validation error when you're sending form to edit your entity. To avoid this behavior you'll need to improve this constraint manually.

    Constraint:

    namespace AppBundle\Validator\Constraints;
    
    use Symfony\Component\Validator\Constraint;
    
    class UniqueValueInEntity extends Constraint
    {
        public $message = 'This value is already used.';
        public $entityClass;
        public $field;
    
        public function getRequiredOptions()
        {
            return ['entityClass', 'field'];
        }
    
        public function getTargets()
        {
            return self::PROPERTY_CONSTRAINT;
        }
    
        public function validatedBy()
        {
            return get_class($this).'Validator';
        }
    } 
    

    Validator:

    namespace AppBundle\Validator\Constraints;
    
    use Doctrine\ORM\EntityManager;
    use InvalidArgumentException;
    use Symfony\Component\Validator\Constraint;
    use Symfony\Component\Validator\ConstraintValidator;
    
    class UniqueValueInEntityValidator extends ConstraintValidator
    {
        /**
         * @var EntityManager
         */
        private $em;
    
        public function __construct(EntityManager $em)
        {
            $this->em = $em;
        }
    
        public function validate($value, Constraint $constraint)
        {
            $entityRepository = $this->em->getRepository($constraint->entityClass);
    
            if (!is_scalar($constraint->field)) {
                throw new InvalidArgumentException('"field" parameter should be any scalar type');
            }
    
            $searchResults = $entityRepository->findBy([
                $constraint->field => $value
            ]);
    
            if (count($searchResults) > 0) {
                $this->context->buildViolation($constraint->message)
                    ->addViolation();
            }
        }
    }
    

    Service:

    services:
        app.validator.unique_value_in_entity:
            class: AppBundle\Validator\Constraints\UniqueValueInEntityValidator
            arguments: ['@doctrine.orm.entity_manager']
            tags:
                - { name: validator.constraint_validator }
    

    Usage example:

    private function validate($value): bool
    {
        $violations = $this->validator->validate($value, [
            new Assert\NotBlank(),
            new Assert\Email(),
            new UniqueValueInEntity([
                'entityClass' => User::class,
                'field' => 'email',
            ])
        ]);
    
        return count($violations) === 0;
    }