Dynamic Validation Files in Symfony

Recently, I implemented my first medium-sized project based on the popular PHP framework Symfony. One major challenge of the project was that it should be highly customizable for each client. This is why I decided to create a multitenant application where most features and settings could be configured from the database.

One area where this customization became a bit tricky was input validation. Although Symfony provides an extensive validation component, it isn’t really obvious how validation rules can be added dynamically after the initialization of the framework. By default, the component supports validation rules both as annotations in the php code and from static configuration files (yml or xml).

What I wanted to do was to load additional yml files conditionally based on the current tenant, which required some kind of hook in the validator component. After a lot of frustration and anger (hello Twitter followers ✌️), this is the solution I came up with:

Inspecting validation services

The first step was to inspect the services that belong to the validation component:

> php bin/console debug:container validator

The validator service implements the ValidatorInterface and is created by a the validator.builder factory:

 ------------------ ---------------------------------------------------------- 
  Option             Value                                                     
 ------------------ ---------------------------------------------------------- 
  Service ID         validator                                                 
  Class              Symfony\Component\Validator\Validator\ValidatorInterface  

[...]

  Factory Service    validator.builder                                         
  Factory Method     getValidator                                              
 ------------------ ---------------------------------------------------------- 

The validator.builder service implements the ValidatorBuilderInterface:

 ------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------ 
  Option             Value                                                                                                                                                 
 ------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------ 
  Service ID         validator.builder                                                                                                                                     
  Class              Symfony\Component\Validator\ValidatorBuilderInterface                                                                                                 

[...]

  Factory Class      Symfony\Component\Validator\Validation                                                                                                                
  Factory Method     createValidatorBuilder                                                                                                                                
 ------------------ ------------------------------------------------------------------------------------------------------------------------------------------------------ 

After checking with the API documentation, I noticed that the Validator itself doesn’t seem to provide a way to load additional files. However, the ValidatorBuilder does:

  • addXmlMapping()
  • addYamlMapping()

After a bit of research in the Symfony documentation, I stumbled upon Service Configurators, which seemed to be the solution to my problem:

The Service Configurator is a feature of the Dependency Injection Container that allows you to use a callable to configure a service after its instantiation.

That’s exactly what I wanted to do: I wanted to configure the validator.builder service after its instantiation, but before the instantiation of the actual validator.

Adding a compiler pass to the service container

To add a configurator to the existing validator.builder service, I had to create a custom compiler pass for the service container. It sets my own service named app.validator.builder.configurator as the configurator class:

// src/AppBundle/DependencyInjection/Compiler/DynamicValidationPass.php
namespace AppBundle\DependencyInjection\Compiler;

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

class DynamicValidationPass implements CompilerPassInterface  
{
    public function process(ContainerBuilder $container)
    {
        // Skip if validator builder doesn't exist
        if (!$container->hasDefinition('validator.builder')) {
            return;
        }

        $validatorBuilder = $container->getDefinition('validator.builder');

        // Add configurator to validator builder which loads
        // dynamic validation rules
        $validatorBuilder->setConfigurator(array(
            new Reference('app.validator.builder.configurator'),
            'configure'
        ));
    }
}

Of course, the compiler pass needs to be registered first in order to be executed during compilation:

// src/AppBundle/AppBundle.php
use Symfony\Component\HttpKernel\Bundle\Bundle;  
use Symfony\Component\DependencyInjection\ContainerBuilder;  
use AppBundle\DependencyInjection\Compiler\DynamicValidationPass;

class AppBundle extends Bundle  
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        // Add compiler pass to load dynamic validation rules
        $container->addCompilerPass(new DynamicValidationPass());
    }
}

Last but not least, I needed to create my configurator class in which I could add whichever yml files I wanted:

# app/config/services.yml
services:  
    app.validator.builder.configurator:
        class: AppBundle\Validator\ValidatorBuilderConfigurator
// src/AppBundle/Validator/ValidatorBuilderConfigurator.php
namespace AppBundle\Validator;

use Symfony\Component\Validator\ValidatorBuilderInterface;

class ValidatorBuilderConfigurator  
{
    public function configure(ValidatorBuilderInterface $builder)
    {
        // Add static files containing validation rules
        // e. g. using Finder/Locator
        $validationFiles = array();
        $validationFiles[] = '../src/AppBundle/Resources/some/file.yml';
        $validationFiles[] = '../src/AppBundle/Resources/another/file.yml';

        // Add files dynamically
        // e. g. by fetching file paths from database
        $validationFiles[] = $this->fetchDynamicFilePath();

        // Add validation configuration to validator builder
        $builder->addYamlMappings($validationFiles);
    }
}

If you have any further suggestions or comments about my solution, feel free to get in contact with me.