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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeBy 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(); | |
var_dump($container->getDefinitions());die; | |
$container->compile(); | |
$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 | |
services: | |
user_agent_subscriber: | |
class: AppBundle\EventListener\UserAgentSubscriber | |
arguments: ["@logger"] | |
tags: | |
- { 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!
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">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:
This is my custom compiler pass:
And this is my root bundle where I registered my new custom compiler:
The problem that my new configuration is not taken into account and it's not working properly.