Symfony2: Service Container Compiler Passes

In this post I am going to look at compiler passes. This is not something that you will often need to worry about when making an app with Symfony2. They do come in useful for some needs though and in particular for releasable bundles.

What are Compiler Passes

The lifecycle of Symfony2 service container is roughly along these lines. The configuration is loaded from the various bundles configurations and the app level config files are processed by the Extension classes in each bundle. Various compiler passes are then run against this configuration before it is all cached to file. This cached configuration is then used on subsequent requests. To read more about the loading of config files by a bundle's extension class and processing the configuration see my posts Symfony2: Controller as Service and Symfony2: Writing a Dependency Injection Extension Configuration.

The compilation of the configuration is itself done first using a compiler pass. Further compiler passes are then used for various tasks to optimise the configuration before it is cached. For example, private services and abstract services are removed, and aliases are resolved.

Many of these compiler passes are part of the Dependency Injection component but individual bundles can register their own compiler passes. A common use is to inject tagged services into that bundle's services. This functionality allows for services to be defined outside of a bundles config but still be used by that bundle. The bundle is not aware of these services in its own static config and a fresh container is passed to the Extension class meaning it does not have access to any other services, so compiler passes which are executed after all the configuration has been loaded are neccessary to inject tagged services. By using a compiler pass you know that all the other service config files have been already been processed.

Creating a Compiler Pass

To create a compiler pass it needs to implements the Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface interface.

It is standard practice to put the compiler passes in the DependencyInjection/Compiler folder of a bundle. They are not automatically registered though, to add the pass to the container, override the build method of the bundle definition class:

<?php

namespace LimeThinking\ExampleBundle;

use LimeThinking\ExampleBundle\DependencyInjection\Compiler\ExamplePass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

class ExampleBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(new ExamplePass());
    }
}

Uses for Compiler Passes

You can implement tags for your own services to allow other people to create services to be injected into your bundle. This of course only make sense for shared bundles which are released for other people to use. For bundles only used in a single application you have full control over its config and can just inject the services in there. There is a cookbook article in the Symfony docs on writing a compiler pass that makes use of tags.

Basically the compiler gives you an opportunity to manipulate the service definitions that have been compiled. Hence this being not something needed in everyday use. In most cases the service definitions in the config can be changed.

There are other uses such as providing more complicated checks on the configuration than is possible during configuration processing. For example, the configuration builder does not provide a way of checking that a value is required only when a certain service is present. The following example from AsseticBundle shows checking if a parameter has been set in the container if a particular service has been defined:

class CheckCssEmbedFilterPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if ($container->hasDefinition('assetic.filter.cssembed') &&
            !$container->getParameterBag()->resolveValue($container->getParameter('assetic.filter.cssembed.jar'))) {
            throw new \RuntimeException('The "assetic.filters.cssembed" configuration requires a "jar" value.');
        }
    }
}

In my next post on this topic I will look at how to manipulate service container definitions from within a compiler pass.