Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Compiler Passes

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

By the time we got to this step in the Kernel, our configuration files have been loaded, but this gives us just one service definition:

... lines 1 - 551
protected function initializeContainer()
... lines 554 - 556
if (!$cache->isFresh()) {
$container = $this->buildContainer();
$this->dumpContainer($cache, $container, $class, $this->getContainerBaseClass());
$fresh = false;
... lines 565 - 573
... lines 575 - 811

So every other service must be added inside compile(). And that's true!

Calling compile() executes a group of functions called compiler passes. In fact, there's one called MergeExtensionConfigurationPass, and it's responsible for the Extension system we just looked at:

... lines 1 - 21
class MergeExtensionConfigurationPass implements CompilerPassInterface
... lines 24 - 26
public function process(ContainerBuilder $container)
... lines 29 - 38
foreach ($container->getExtensions() as $name => $extension) {
... lines 40 - 49
$extension->load($config, $tmpContainer);
... lines 51 - 52
... lines 54 - 57

It loops over the extension objects and calls load() on each one. This is where most of the services come from.

But there's a bunch of other compiler passes, and most do small things. They're usually registered inside your bundle class - FrameworkBundle is a great example:

... lines 1 - 45
class FrameworkBundle extends Bundle
... lines 48 - 64
public function build(ContainerBuilder $container)
... lines 67 - 72
$container->addCompilerPass(new RoutingResolverPass());
$container->addCompilerPass(new ProfilerPass());
... lines 75 - 76
$container->addCompilerPass(new RegisterListenersPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->addCompilerPass(new TemplatingPass());
... lines 79 - 97

The build() method of every bundle is called early, and is used almost entirely just to add compiler passes. So what's the point of compiler passes? Why not just do any container modifications in the extension class?

The special thing about a compiler pass is that when it's called, the entire container has been built. So it's perfect when you need to tweak the container, but only once all of the service definitions are loaded.

Compiler Pass and Tags

Let's see an example. In our services.yml, we have one service, and it's an event subscriber. To tell Symfony this is an event subscriber, we had to add the tag: kernel.event_subscriber:

... lines 1 - 5
class: AppBundle\EventListener\UserAgentSubscriber
arguments: ["@logger"]
- { name: kernel.event_subscriber }

So how does that work?

It's a compiler pass! And you can see it registered in FrameworkBundle, it's RegisterListenerPass:

... lines 1 - 19
class RegisterListenersPass implements CompilerPassInterface
... lines 22 - 43
public function __construct($dispatcherService = 'event_dispatcher', $listenerTag = 'kernel.event_listener', $subscriberTag = 'kernel.event_subscriber')
$this->dispatcherService = $dispatcherService;
$this->listenerTag = $listenerTag;
$this->subscriberTag = $subscriberTag;
... lines 50 - 105

The subscriberTag property is: kernel.event_subscriber. Near the bottom, it calls $container->findTaggedServiceIds() and passes it that:

... lines 1 - 87
foreach ($container->findTaggedServiceIds($this->subscriberTag) as $id => $attributes) {
$def = $container->getDefinition($id);
... lines 90 - 94
$class = $def->getClass();
... lines 96 - 102
$definition->addMethodCall('addSubscriberService', array($id, $class));
... lines 105 - 107

It's saying: give me all services tagged with kernel.event_subscriber. The $definition variable at the bottom is the Definition object for the event_dispatcher. And we use it to add a method call for addSubscriberService and pass it the service id and the class.

Let's go see this in the cached container. Refresh to get it back, then search for user_agent_subscriber:

... lines 1 - 1126
protected function getDebug_EventDispatcherService()
$this->services['debug.event_dispatcher'] = $instance = new \Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher(new \Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher($this), $this->get('debug.stopwatch'), $this->get('monolog.logger.event', ContainerInterface::NULL_ON_INVALID_REFERENCE));
... lines 1130 - 1136
$instance->addSubscriberService('user_agent_subscriber', 'AppBundle\\EventListener\\UserAgentSubscriber');
... lines 1138 - 1160
return $instance;
... lines 1163 - 4244

There it is! It's calling the addSubscriberService method and passing the service id and class.

This is one of the most common jobs for a compiler pass. For example, there's another tag called form.type and this FormPass looks for all services tagged with that and does some container tweaking.

And there's a bunch more: like the compiler pass that checks for circular references. If service A depends on service B, which depends on service C, which depends on service A, you'll get a really clear exception. Then there are other passes which make micro-optimizations to speed the container up even more.

Creating a Compiler Pass

Most of the time, you won't need to create a compiler pass - you just need to understand how they work. But, we're diving deep, so let's make one! In AppBundle create a new DependencyInjection directory and inside of there a Compiler directory. I don't have to put it here, but this follows the core standard.

In here, create a new class called EarlyLoggingMessagePass. Remember how we logged a message as soon as the logger was created? We're going to do that again.

Compiler classes are pretty easy - just implement CompilerPassInterface and add the one method: process():

... lines 1 - 2
namespace AppBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class EarlyLoggingMessagePass implements CompilerPassInterface
... lines 10 - 16
public function process(ContainerBuilder $container)
... lines 19 - 20

Now we should feel really comfortable: that's a ContainerBuilder object and we know all about him. It also has every service already defined inside. So we can say: $definition = $container->findDefinition('logger'). Now just add $definition->addMethodCall() and pass it debug for the method, and an array with a single argument: Logger CREATED:

... lines 1 - 16
public function process(ContainerBuilder $container)
$definition = $container->findDefinition('logger');
$definition->addMethodCall('debug', array('Logger CREATED!'));
... lines 22 - 23

And that's a functional compiler pass.

You can register this by overriding the build() method in AppBundle and adding it there. But that's too easy.

Instead, go to AppKernel and override buildContainer(). Call the parent method, then add $container->addCompilerPass() and pass it a new EarlyLoggingMessagePass. And don't forget to return the $container:

... lines 1 - 5
class AppKernel extends Kernel
... lines 8 - 39
protected function buildContainer()
$containerBuilder = parent::buildContainer();
$containerBuilder->addCompilerPass(new \AppBundle\DependencyInjection\Compiler\EarlyLoggingMessagePass());
return $containerBuilder;

Ok, let's try it! Refresh! Click into the profiler then go to the logs tab. Under debug, there's the message! First on the list.

Phew! So you're now a master. The Container is all about Definition objects, which are populated from Yaml and XML files and then updated later in the dependency injection extension classes. If you're following this, go dive into the FrameworkBundle and see where the real core services come from And congrats, because now, you're a dependency-injection-asaurus!

Ok guys, seeya next time!

Leave a comment!

Login or Register to join the conversation

Override extension configuration via compiler pass; I'm trying to override an existant api platform configuration api_platform.mapping.paths via a some kinda of custom <a href="https://symfony.com/doc/current/service_container/compiler_passes.html&quot;&gt;Compiler Pass</a>

The goal is to merge my new configuration with the existing one

Here is the configuration part that I would like to update it at the height of the complication of my container:

# api/config/packages/api_platform.yaml

        # The list of paths with files or directories where the bundle will look for additional resource files.
        paths: []

This is my custom compiler pass:

     * Class MergeApiMappingPathsConfigurationPass.
    class MergeApiMappingPathsConfigurationPass implements CompilerPassInterface
         * {@inheritdoc}
        public function process(ContainerBuilder $container)
            $configs = $container->getExtensionConfig('api_platform');
            $configs[0]['mapping']['paths'] = array_merge($configs[0]['mapping']['paths'], [__DIR__.'/../../Resources/resources/']);
            $container->prependExtensionConfig('api_platform', $configs[0]);

And this is my root bundle where I registered my new custom compiler:

class DemoBundle extends Bundle
        public function build(ContainerBuilder $container)
            $container->addCompilerPass(new MergeApiMappingPathsConfigurationPass());

The problem that my new configuration is not taken into account and it's not working properly.


Hey ahmedbhs!

I think what you want is this: https://symfony.com/doc/cur...

The problem with a compiler pass is that they are *really* meant to modify *services* that API Platform created, but not its config directly. It's not working because API Platform has already used its configuration by this point - so you're modifying it too late.

So, try the above solution and let me know if you have any luck :).


1 Reply
Roman R. Avatar
Roman R. Avatar Roman R. | posted 4 years ago | edited

Tell please how add compiller passes in symfony 4
I trying add this code in Kernel.php but it didn't work

`public function build(ContainerBuilder $container)

    parent::build($container); // TODO: Change the autogenerated stub

    $container->addCompilerPass(new UrlGeneratorPass());

I also try implement CompillePassInterface in Kernel but this didnt work too


Hey Roman R.

You may find useful this new episode where Ryan implements a CompilerPass on Symfony4

Or, you can read the docs here: https://symfony.com/doc/cur...



Dependency-injection-asaurus! xD


Hey Serge,

Haha, glad you like it! Should we reserve rights for this word? :D



You definitely should!
Then sell the rights to Universal Pictures so they include it in the new Jurassic World movie.


Lol! OK, calling our lawyers :D


1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "2.6.x-dev", // 2.6.x-dev
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.6
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.2.0
        "symfony/assetic-bundle": "~2.3", // v2.5.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.7
        "symfony/monolog-bundle": "~2.4", // v2.6.1
        "sensio/distribution-bundle": "~3.0", // v3.0.9
        "sensio/framework-extra-bundle": "~3.0", // v3.0.3
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "hautelook/alice-bundle": "~0.2" // 0.2
    "require-dev": {
        "sensio/generator-bundle": "~2.3" // v2.4.0