Unpacking Symfony2 compiler passes

December 8th 2014

Introduction

This is a three part article about compiler passes in Symfony2. In this article I will walk through what the compiler pass is, what we can do with it and, some real world examples of it being used. The second article will be a tutorial walking through an example of writing a compiler pass.  The third article will walk through unit testing the compiler pass as I have found unit testing fun, useful and complicated. Most of the information that I will present is my own understanding gained by working with Symfony2, if you want official documentation on the subject then try the following links (you should read these whether or not your read the rest of my article):

What is a compiler pass

A compiler pass is Symfony2's way of letting you interact with the service and parameter definitions defined in the global and bundle scope once they have been loaded but before they have been compiled into the optimised DIC. In a simplified form the phase of events looks like:

  1. Every yaml/xml/php configuration file specified by the global config file (usually config_ENV.yml) is loaded into a configuration array
  2. Every yaml/xml/php configuration file specified by bundles in the system is loaded into the same array
  3. The array is passed to any compiler pass registered with the system which can then manipulate it in any way deemed fit by the developer (add, remove, update any service definition)
  4. The array is then compiled into a class called the dependency injection container which is a huge object full of methods for accessing all of the services defined throughout the application

Why is the compiler pass useful

The primary way that I have found compiler passes to be useful is the ability to update arguments passed to services. This is commonly done when working with tagged services so that a service can be passed all other services tagged for it as a single argument (array) without the developer writing a large list in the arguments definition. Other uses include:

  • Creating new services that require information about other defined services before being defined
  • Swapping or adding arguments to a service that you did not write
  • Creating and modifying parameters in the container

Real world examples

Given the open source nature of it's code, Symfony2 becomes much easier to learn as there are so many examples available. We will look at two examples of a compiler pass being used, walk through the pass and how it works and then try and understand the merits of each pass.

The Symfony2 form registry

If you have ever made a custom for type in Symfony2, you may well have registered it as a form type so that you can use it in other forms without manually instantiating the class. You can do this because the form "registry" is constructed with your form type service injected (although I am showing code for the dependency injection extension, this service is passed to the actual form registry so for simplification I am talking about the two services as one). This is done in a compiler pass:

class FormPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasDefinition('form.extension')) {
            return;
        }
        $definition = $container->getDefinition('form.extension');
        // Builds an array with service IDs as keys and tag aliases as values
        $types = array();
        foreach ($container->findTaggedServiceIds('form.type') as $serviceId => $tag) {
            $alias = isset($tag[0]['alias'])
                ? $tag[0]['alias']
                : $serviceId;
            // Flip, because we want tag aliases (= type identifiers) as keys
            $types[$alias] = $serviceId;
        }
        $definition->replaceArgument(1, $types);

Walking through what happens in the above:

  1. The container is checked to see if it has the registry by using the registry's service id (if you declare services in yaml this is the top line of your definition, the name)
  2. If the definition exists then fetch it (A definition is an object populated with all of the information from the declaration)
  3. Loop over all of the service definitions tagged using the form.type tag (this returns the service id and the attributes from the tag, e.g alias, method)
  4. Get the alias attribute from the tag and add the service id to an array, identified (key) by it's alias
  5. Replace the second argument in the form.extension service definition with the array of service id's (note that this is an array of id's not actual services, this is why the registry has the container injected so it can fetch the services as required)

So as you can see, it is relatively simple to create a compiler pass to replace a definition's argument with something and to get tagged services.

Why is this better than hardcoding a list of services as the second argument for the form registry

The primary way that I see this as a better implementation than simply hard coding the arguments is maintainability. What I mean by that is two fold:

  1. Every time a new form type is added to the core form package, the service definition for the form registry does not have to be updated
  2. 3rd party modules with form types can register them with the registry without having to modify the core service definition (please do not modify core files)

There are other advantages too, such as being able to unit test the logic, but I don't think we require additional advantages to choose to implement it this way. The above two examples show that the alternative implementation is very limited, manual and makes external developers jobs much harder.

norzechowicz/aceeditor-bundle

For those of you who have not used ace editor it is a nifty, open source, javascript web editor that works really well and has many useful features. It is essentially a low feature, web based IDE. Norbert Orzechowicz has written a bundle that adds a form type allowing users to utilise ace editor in Symfony2 projects. One of the complications that Norbert had to overcome was adding a file to the list of form themes so that he can add additional markup to the rendering of the ace editor form type. He could have taken the easy way out and required users to add the form theme manually to app/config/config.yml. Instead we wrote a compiler pass:

class TwigFormPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        if (!$container->hasParameter('twig.form.resources')) {
            return;
        }
        $container->setParameter('twig.form.resources', array_merge(
            array('NorzechowiczAceEditorBundle:Form:div_layout.html.twig'),
            $container->getParameter('twig.form.resources')
        ));
    }
}

Walking through what happens above:

  1. As with the form registry, the container is checked for the existence of the required definition (this time it is a parameter not a service)
  2. If the parameter exists, the value is accessed and the new form theme template is added to the value before reassigning the new combination value to the parameter

So a parameter defined in config.yml has been manipulated to contain additional elements in it's array.

Why is this better than requiring the developer to register the form theme manually

Admittedly it would not be much work for a developer to make one modification to config.yml when they install this bundle, however in my opinion it is nice not to have to as it makes the installation easier. The biggest advantage I can see however is, when a developer writes a bundle that requires the norzechowicz/aceeditor-bundle. Now in their documentation for installation the developer will have to include installation instructions for norzechowicz/aceeditor-bundle too, as it is a pre-requisite to make their bundle work. This chain of dependencies could get longer and so the installation process (and documentation) gets more complicated. So with that in mind it seems like a good development decision not to require other developers to have to add code to install your module if your module can do it programatically using the tools Symfony2 provides. Hopefully the above has all made sense, look out for the next article in this series as it will be a tutorial on writing a compiler pass based on a use case I had for a recent project. Update: Part 2 of this series is now published, you can read it here

Luke Rotherfield

Symfony2 Developer in CT USA. Luke is a Symfony2 wizard and has written some sweet libraries of his own. Luke loves Jesus, his gorgeous wife and his two beautiful daughters :)