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!