2
votes

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 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?

EDIT:

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 the answer below I used:

->addTag('validator.constraint_validator');

But now I am getting customEmailUniqueEntity class not found error, 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.

EDIT2:

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?

2
You have symfony4 - typehint an argument for constructor, either of form or of validator. – u_mulder
just constructor of EmailExists constraint, but I know it should be in validator, but how to pass it to validator? – user4271704

2 Answers

1
votes

Constraints are not validators. Symfony will take a constraint and search for its validator by attaching Validator to the classname.
So in symfony you register your constraint by EmailExists but the class/service which actually does validation is EmailExistsValidator.
And this is also the place to inject EntityManagerInterface into it.

All information can be found here: Symfony - How to Create a custom Validation Constraint

1
votes

Your customEmailUniqueEntity service will never be taken into account by the ContainerConstraintValidatorFactory when it determines the actual validator to be used for the EmailExists constraint.

In order to let the factory know which services are constraint validators you need to tag it with the validator.constraint_validator tag like this:

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

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