How to have a custom symfony validator constraint with dependency injection?

I am trying to create a custom symfony form validator constraint. I created two class, one constraint and one validator and it works fine. But I need to pass doctrine entitymanager instance to validator class, as I am using them standalone and not framework, I don’t have yaml configuration file. I created a constructor in validator class to have $em, and in controller I have:

->add('email', EmailType::class, [                                              
    'constraints' => [
        new Assert\Email(['message' => 'invalid.email', 'mode' => 'strict', 'normalizer' => 'trim']),
        new Assert\EmailExists($em),
    ],
 ]);

But I am not getting $em, in my validator class, what should I do? I also tried to have constructor in main constraint class, then in validator I had parent::construct(), still not working.

I did read this too https://stackoverflow.com/questions/40601866/how-to-configure-dependencies-on-custom-validator-with-symfony-components but instead of making the factory class, I used the current factor class and used this:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\ContainerConstraintValidatorFactory;

$container = new ContainerBuilder();
$container
    ->register('customEmailUniqueEntity', 'EmailExistsValidator')
    ->addArgument($em);
$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->setConstraintValidatorFactory(
    new ContainerConstraintValidatorFactory($container)
);

$validator = $validatorBuilder->getValidator();
$violations = $validator->validate('email address', [ 
    new EmailExists() 
]);

if (0 !== count($violations)) {
    // there are errors, now you can show them
    foreach ($violations as $violation) {
        echo $violation->getMessage().'<br>';
    }
}

With this code both dependency injection and validation works fine, but is there a trick to have this custom constraint as ‘constraint’ array argument within form builder rather than validating it manually?

->add('email', EmailType::class, [                                              
    'constraints' => [
        new Assert\Email(['message' => 'invalid.email', 'mode' => 'strict', 'normalizer' => 'trim']),
        new Assert\EmailExists($em),
    ],
 ]);

With code above I cannot pass $em to the constructor of my custom Validator. Any trick possible?

You need to pass the Entity Manager to the constraint validator, not to the constraint itself.

Also I’m pretty sure you need to tag your customEmailUniqueEntity service for it to be picked up by the ConstraintValidatorFactory. You’d have to check the docs on which tag to use.

I know I was injecting to validator class.

In order to inject doctrine EntityManager, in EmailExists class I had:

 public function validatedBy()
 {
        return 'customEmailUniqueEntity';
        //return \get_class($this).'Validator';
 }

then I had:

$container = new ContainerBuilder();
$container
    ->register('customEmailUniqueEntity', 'EmailExistsValidator')
    ->addArgument($em);

because if I was returning validator class from validatedBy() I could not inject $em to the constructor of validator. With your answer below I used:

->addTag('validator.constraint_validator');

But now I am getting customEmailUniqueEntity class not found error when I had EmailExists constraint as array param to constraint array of add() of form, as if I return validator from validatedBy() , injection will not work, what should I do? I tried to return

 public function validatedBy()
 {
        return 'EmailExists';
        //return \get_class($this).'Validator';
 }

but this one, of course I am getting initialize() error. Please advise.

Here I try to clarify what exactly I did. Please advise.

I added addTag to:

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Validator\ContainerConstraintValidatorFactory;

$container = new ContainerBuilder();
$container
    ->register('customEmailUniqueEntity', 'EmailExistsValidator')
    ->addArgument($em),
    ->addTag('validator.constraint_validator');

$validatorBuilder = Validation::createValidatorBuilder();
$validatorBuilder->setConstraintValidatorFactory(
    new ContainerConstraintValidatorFactory($container)
);

$validator = $validatorBuilder->getValidator();
$violations = $validator->validate('email address', [ 
    new EmailExists() 
]);

if (0 !== count($violations)) {
    // there are errors, now you can show them
    foreach ($violations as $violation) {
        echo $violation->getMessage().'<br>';
    }
}

and in constructor of EmailExistsValidator I have:

var_dump($em);

and I got $em object in validator, so $em is injected and adding addTag() did not cause any error. If I remove validatedBy() of EmailExists contsraint, injection will not be done. In that method I am doing

return `customEmailUniqueEntity;`

because if I return EmailExistsValidator, injection will not be done.
Now how to use validator.constraint_validator or EmailExists() as constraints array param of the form? if I use new EmailExists() I will get Two Few Aguments for validator class as $em wll not be injected this way. What to do?

validatedBy() should return EmailExistsValidator and then you need to rename your service to EmailExistsValidator as well, then the ContainerConstraintValidatorFactory will pick it up from the container and DI will work. If you name it differently then ContainerConstraintValidatorFactory will not get it from the container but create a new instance, and that will indeed fail.

See https://github.com/symfony/validator/blob/master/ContainerConstraintValidatorFactory.php#L42

I changed the code to:

$container
    ->register('Namespace\EmailExistsValidator', 'Namespace\EmailExistsValidator')
    ->addArgument($em);
    ->addTag('validator.constraint_validator');
        public function validatedBy()
         {
                   return \get_class($this).'Validator';
         }

As you suggested, but this time I am getting Too few arguments to function Namespace\EmailExistsValidator::__construct(), 0 passed. And DI is not done, it seems service name cannot be the same as the validator class, if naming is different then injection is working. Please help to fix it?

In which namespace is your EmailExistsValidator class? It started in the root namespace a few posts ago and now it’s in the Namespace namespace?

Just a single custom namespace. I created for composer autoloader. here we can name them customNamespace\EmailExistsValidator and customNamespace\EmailExists

And you passed the correct namespace to the service definition?

I just did this

$container
    ->register('Namespace\CustomEmailExists', 'Namespace\EmailExistsValidator')
    ->addArgument($em);
    ->addTag('validator.constraint_validator');

Where else should I do?
Also DI injection will not work if validatedBy returns Namespace\EmailExistsValidator and I get error 0 argument passed to its constructor. So I created another service name that actually doesn’t exists as a class file but DI is working this way as I passed validator as second param of register(). What else should I do?

If you don’t use the framework the easiest way to get this working is to use the full class name for the service name and the service class and make the service public:

$container
    ->register('Namespace\CustomEmailExists', 'Namespace\EmailExistsValidator')
    ->addArgument($em)
    ->setPublic(true);

(the tag is no longer needed in that case)


More advanced:

The framework creates a ServiceLocator for all validators, which is basically a second container that contains only a subset of all services as publicly accessible. You could do that too if you add the Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass to your container. Then you can pass it the service name of your validator factory and a tag name, and then all services you tag with that will be stuffed into a ServiceLocator and then that will be passed to the validator factory.


Why are you not using Symfony framework btw? It seems you’re running into lots of problems that you wouldn’t have if you used the framework.

As I said Namespace\CustomEmailExists doesn’t actually exist as real class file but DI in validator is working fine. If I changed it to Namespace\EmailExists or the validator that are really existing, DI will not work anymore. So with approach you said I can call EmailExists constaint as pseudo name `Namespace\customEmailExists as use the constraint via this pseudo name?

It will if you make the service public