In this post I will talk about how we have employed various strategies in order to make the Symfony CMF ResourceBundle extendable; firstly through the use of Service Providers, then DI Definition Factories, and finally Tags.

Bundles take care of plumbing various services into the DI container. The ResourceBundle creates a registry object and handles the instantiation of and configuration of repositories.

Repositories are implementations of the ResourceRepository interface and implementations are provided for PHPCR and PHPCR-ODM, but in short it should facilitate the connection of any implementation to the registry.

Repositories are created based on a configuration, each entry under repositorires represents a repository instance which needs to be created:

            type: doctrine/phpcr_odm
            base_path: /cms/pages
            type: filesystem
            base_path: %kernel.root_dir%/template
            symlink: true

I wanted to solve this problem for purely selfish reasons, having spent some time developing a wrapper for the Doctrine ORM which made it behave like a hierarchical object manager (like the PHPCR-ODM), it is preliminarily called "Doctrine CR" - I wanted to experminent with integrating this without touching the ResourceBundle, and that was actually not possible.

The aims are as follows:

  • Allow the bundle to be extended, and not distinguish between "third party" and "native" repository types.
  • If the user configures an invalid repository type, an exception should be thrown listing the available repository types.
  • The container should not be compiled with references to non-existent services.
  • The repository options (e.g. base_path, symlink) should be validated.

Iteration 1: Service Providers

My first attempt at solving this problem had me creating factories to instantiate the repository:

class DoctrineCRFactory
    private $container;

    public function __construct(ContainerInterface $container)
        $this->container = $container;

    public function create(array $config)
        return new DoctrineCrRepository(

    public function getDefaultConfig()
        return [
            'base_path' => '/'

This is similar to the service provider pattern.

We inject the container in order to lazily demand the service dependencies. In this way the factories can be registered regardless of any missing dependencies, allowing us to throw a useful exception if the user requires a non-existent repository type:

The requested repository "doctrine_foobar" is not available, available
repositories: "filesystem", "doctrine/phpcr", "doctrine/phpcr-odm"

And if the user requests an existent repository type with unmet dependencies, then they will encounter an error (e.g. "sonata.admin_pool is not available") and they reasonably be expected to install SonataAdminBundle.

We then provided a default configuration allowing the registry to perform some basic sanity checks and letting the user know if they have provided an invalid key:

Unknown keys "foobar", available keys: "base_path"

This approached worked, and I was quite happy with it, but then WouterJ pointed me to the Symfony Security component, he had a better idea.

Iteration 2: DI Definition Factories

The problem with the repository factories is that we are removing the responsibility of creating the objects from the DI container and any errors are encountered lazily, we only discover them when we request a repository and the registry invokes the factory.

Traditionally we extend the DI container in Symfony using tags. We can't do that here because we need to process the repository configuration, we need to validate and normalize it, and pass configuration values to the constructor of the repository class.

So how can we achieve this at the container level?

We can move down a level and extend the Extension directly, first we create a factory. The factory creates DI service Definition classes and configures the options it requires using the Symfony OptionsResolver component:

class DoctrineCrDefinitionFactory
    public function create(array $options)
        return new Definition(
            new Reference('doctine_cr.entity_manager')

    public function configure(OptionsResolver $options)
        $options->setRequiredTypes('base_path, [ 'string' ]);

and we register them as follows:

class CmfResourceBundle extends Bundle
    public function build(ContainerBuilder $container)
        // retrive the `cmf_resource` DI extension
        $extension = $container->getExtension('cmf_resource');

        // add our definition factories
        $extension->addRepositoryFactory('filesystem', new FilesystemFactory());
        $extension->addRepositoryFactory('doctrine/cr', new DoctrineCrFactory());

and in the bundle extension add the addRepositoryFactory method, process the configuration, and add the definitions to the container, something like:

class CmfResourceExtension
    private $definitionFactories = [];

    // ...

    public function addRepositoryFactory($name, DefinitionFactory $factory)
        $this->definitionFactories[$name] = $factory;

    public function load(array $configs, ContainerBuilder $container)
        $config = // process configuration
        // ... 

        forach ($config['repositories'] $repositoryConfig) {

            // validate the repository type
            if (!isset($this->definitionFactories[$repositoryConfig['type'])) {
                throw new InvalidArgumentException(sprintf(
                    'Unknown repository type "%s", known repository types "%s",
                    implode('", "', array_keys($this->definitionFactories)

            $factory = $this->definitionFactories[$repositoryConfig['type']];

            // resolve and validate the options
            $resolver = new OptionsResolver();
            $options = $resolver->resolve($repositoryConfig);

            $definition = $factory->create($options);

            // add the definition to the DI container.

So now we:

  • Validate the repository type and let the user know which types are available if they make a typo or request an unavailable repository.
  • Use the OptionsResolver to normalize and validate the options at compile-time, catching errors early and adding no overhead in production.
  • Add the definition to the container just like any other.

Compiler Pass for Non-Configurable Services

In my zeal I subsequently tried to apply this method to another similar problem, the registration of "description enhancers" - I want go into detail, suffice to say they are also services which are registered with a "registry-like" objects.

Again Wouter pointed out that this was perhaps not the best approach.

Like repositories enhancers may contain references to services which are potentially not available at runtime (i.e. services from Sonata Admin, or Sylius), so I thought configuring them with DI definition factories would be a good approach.

But there is one crucial difference: enhancers do not require configuration.

In the case of the repositories, each new repository was configured by the user, the DI definitions needed to have the user configuration values before they were added to the container, this is not the case with enhancers.

This means that we can use DI tags as normal, but filter out non-enabled enhancers during a compiler pass:

class DescriptionEnhancerPass implements CompilerPassInterface
    public function process(ContainerBuilder $container)
        // ...

        // retrieve the tagged enhancer descriptions
        $taggedIds = $container->findTaggedServiceIds('cmf_resource.description.enhancer');
        $enabledEnhancers = $container->getParameter('cmf_resource.description.enabled_enhancers');

        // build up a map of all available enhancer services
        foreach ($taggedIds as $serviceId => $attributes) {
            $enhancers[$name] = new Reference($serviceId);

        // throw an exception if an invalid enhancer name was given and say
        // which enhancers ARE available.
        $enhancerNames = array_keys($enhancers);
        $diff = array_diff($enabledEnhancers, $enhancerNames);
        if ($diff) {
            throw new InvalidArgumentException(sprintf(
                'Unknown description enhancer(s) "%s", available enhancers: "%s"',
                implode('", "', $diff),
                implode('", "', $enhancerNames)

        $inactiveEnhancers = array_diff($enhancerNames, $enabledEnhancers);
        foreach ($inactiveEnhancers as $inactiveEnhancer) {
            $container->removeDefinition((string) $enhancers[$inactiveEnhancer]);

        $registryDef = $container->getDefinition('cmf_resource.description.registry');
        $registryDef->replaceArgument(0, array_values($enhancers));

Above we remove inactive enhancers[1] from the container to prevent the container trying to resolve non-existent references and meet our aim of letting the user know which services are available if they make a typo or request a non-existent service.

[1] Alternatively we could mark the enhancers as private and they would be automatically removed from the container when they are not referenced. This is the approach we opted for in the end.


We have discussed three different approaches to being able to extend a bundle with services with optional dependencies without making the bundle explicitly aware of the services.

Do your extendable services require configuration?

  • Yes: Use DI Definition Factories.
  • No: Use a compiler pass.

Do you have other ideas? Let me know!

Posted on: 2016-06-03 00:00:00


Recent Posts

Bundles: Service Providers, Definition Factories and Tags.

In this post I will talk about how we have employe...

PHPBench 0.11

PHPBench 0.11 (Dornbirn) has been released, and it...

Bundles of Joy

Recently I decided to create a new organization wh...

Books, 2015

At the end of last year I saw some people post lis...