How to use external services with the Symfony Validator

In this article we are going to see a particular case of validation using the Symfony Validator expression language and accessing third party services.

Validation is one of the most common tasks when building many types of software applications.

Talking more specifically about the Symfony Framework, its Validator component offers a very powerful set of APIs to validate objects, arrays, forms and much more.

We are going to see a particular case of validation that requires third party services. This question emerged during my talk at Symfony Live in Berlin last week.

The problem

Let's consider the following class:

<?php
use Symfony\Component\Validator\Constraints as Assert;

class User 
{
    /**
     * @var string
     * @Assert\Length(min="2", max="32")
     */
    public $nickname;
}

Using the @Assert\Length annotation we want to ensure that the nickname length is between 2 and 32 characters. What about having a list of not allowed nicknames? That can be done using the @Assert\Expression assertion, that allows us to use the Symfony Expression Language in our validation rules.

<?php
use Symfony\Component\Validator\Constraints as Assert;

class User 
{
    /**
     * @var string
     *
     * @Assert\Expression(
     *     "this.nickname not in ['admin', 'superuser']",
     *     message="This nickname is not allowed!"
     * )
     */
    public $nickname;
}

In this way we do not allow "admin" and "superuser" as nicknames. What if we want this list of not allowed nicknames to be provided by another "service" available in our application? With the default symfony validator setup this is not possible.

The expression language used by the validator component has a very limited set of features and can not refer to other parts of the application except of the object on which the validation is performed (through the object variable).

The solution

Luckily it is pretty easy to re-configure the expression language provider and much more powerful.

We need to change the validator.expression expression language with a different instance having a wider access to system services.

This can be done using a compiler pass and hooking into the Symfony Dependency Injection process. We create src/DependencyInjection/CompilerPass/ValidatorExpressionLanguagePass.php with the following content:

<?php

namespace App\DependencyInjection\CompilerPass;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class ValidatorExpressionLanguagePass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $container->getDefinition('validator.expression')->setArguments([
            null, 
            new Reference('security.expression_language')
        ]);
    }
}

We change the default validation expression language to security.expression_language (provided with the symfony security component), so we can use the provided service and parameter functions to interact easily with the Symfony DI Container.

Next step is to register the ValidatorExpressionLanguagePass compiler pass in the Kernel.php.

<?php

namespace App;

//..
use App\DependencyInjection\CompilerPass\ValidatorExpressionLanguagePass;

class Kernel extends BaseKernel
{
    //..
    protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader)
    {
        // ...
        $container->addCompilerPass(new ValidatorExpressionLanguagePass());
        // ...
    }
}

With this change, now is possible to use other public services inside the expression language.

<?php
use Symfony\Component\Validator\Constraints as Assert;

class User 
{
    /**
     * @var string
     *
     * @Assert\Expression(
     *     "this.nickname not in service('app.repo.forbidden_nicknames').getForbiddenNicknames()",
     *     message="This nickname is not allowed!"
     * )
     */
    public $nickname;
}

Et voilĂ !

In this case the list of not allowed nicknames is provided by a hypothetical app.repo.forbidden_nicknames service, that can be any other service part of your application.

P.S.

We can use any other expression language instance instead of security.expression_language.

To make an example, the jms/serializer-bundle provides the jms_serializer.expression_language that has built in functions such as service, is_granted and parameter. You can choose it if you want to use the is_granted function that is not provided by the security expression language instance.

Conclusion

Hope you enjoyed this article, if you have some feedback, do not contact me or to send me a tweet!

php, symfony, validator, validation, expression-language

Do you need something similar for your company?