December 8th 2014
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):
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:
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:
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.
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:
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.
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:
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.
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:
So a parameter defined in config.yml has been manipulated to contain additional elements in it's array.
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
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 :)