Extending Symfony's Event Service

One important concept in Symfony 2 (the framework OroBAP/OroCRM is built-in/on-top-of) is the service container. Programmers create classes and class hierarchies for myriad reasons — one way of looking at a service container is it allows a programmer to say

Hey, this class? It's a service. It provides an important single unit of functionality to programmers using this system.

Service containers are an abstraction on top of the programming language itself.

Service containers make a lot of things easy and consistant. One of those things is dependency injection. One way of looking at dependency injection is

Hey, that class? I want to change how it works. Use this other class instead.

Symfony's configuration system allows a developer to swap out service classes without changing the underlying PHP code.

With that in mind, I wanted to inject a different event dispatcher into my system for debugging purposes. However, when I went digging into the system, I ran into a few problems.

The first step of injecting a different class for a service container is finding the name/identifier of the service. I knew, (through some debug_backtrace debugging in an event listener), that there were two event dispatchers in Symfony — one for dev, one for prod

//dev dispather
Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher

//prod dispatcher
Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher

This makes sense, as the web profiler available in dev mode reports on dispatched events, and you wouldn't want a production system wasting resources doing that.

So, understanding how Symfony works, I assumed there was some extra dev configuration that injected a different dispatcher into the service for dev mode. The next step was to search the default Symfony configuration for the ContainerAwareEventDispatcher class. I found it in

#File: vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml
<!-- ... -->
<parameters>
    <parameter key="event_dispatcher.class">Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher</parameter>
</parameters>

Being in an older bundle (that's part of the core system), it's not surprising the service is configured in XML (newer bundles seem to use yaml files). This configuration file has a paramater named event_dispatcher.class. This isn't the service definition, it's just a configuration paramater. However, if we jump down further in the file

#File: vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml

<services>
    <service id="event_dispatcher" class="%event_dispatcher.class%">
        <argument type="service" id="service_container" />
    </service>
</services>

We see the configuration for a service named event_dispatcher that uses the event_dispatcher.class paramater to define the service class

class="%event_dispatcher.class%"

This is what I was looking for.

Or so I thought.

Normally, my assumptions above would be correct. However, the event dispatcher is special. Looking through my ack results, I also saw the following configuration.

#File: vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml
<parameters>
    <parameter key="debug.event_dispatcher.class">Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher</parameter>
    <!-- ... -->
</paramaters>

<!-- ... -->

<service id="debug.event_dispatcher" class="%debug.event_dispatcher.class%">
    <tag name="monolog.logger" channel="event" />
    <argument type="service" id="event_dispatcher" />
    <argument type="service" id="debug.stopwatch" />
    <argument type="service" id="logger" on-invalid="null" />
    <call method="setProfiler"><argument type="service" id="profiler" on-invalid="null" /></call>
</service>    

Earlier I assumed Symfony injected the TraceableEventDispatcher class into the system for the dev environment. However, the above configuration makes it look like the event service is named debug.event_dispatcher. This doesn't match event_dispatcher.

At this point my brain shuts down because I'm trying to hold two impossible-to-resolve ideas in my head. There's a name for this condition — it's called being a programmer.

Pre-Bootstrapped System Behavior

The answer, of course, was my assumption about the Symfony event dispatcher was incorrect. The event dispatcher is part of the HTTP kernel object, and the system needs to (or the cire developers have chosen to) instantiate this object before the system is fully bootstrapped.

The event dispatchers are hard coded in the generated container classes. For the dev container that's

#File: app/cache/dev/appDevDebugProjectContainer.php
protected function getHttpKernelService()
{
    return $this->services['http_kernel'] = 
        new \Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel(
            $this->get('debug.event_dispatcher'), 
            $this, 
            $this->get('debug.controller_resolver')
        );
}    

and for the prod container that's

protected function getHttpKernelService()
{
    return $this->services['http_kernel'] = 
        new \Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel(
            $this->get('event_dispatcher'), 
            $this, 
            new \Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver($this, $this->get('controller_name_converter'), $this->get('monolog.logger.request', ContainerInterface::NULL_ON_INVALID_REFERENCE)));
}

The first argument to the HTTP Kernel's (ContainerAwareHttpKernel) constructor is the event dispatcher object, and you can see the different hard coded service values.

$this->get('debug.event_dispatcher')
$this->get('event_dispatcher')

So, unlike the rest of the system, the event dispatcher (and controller resolver) don't fully conform to what a Symfony developer would consider best practices.

This means I'd need to inject two service classes. One for the event_dispatcher service, and another for the debug.event_dispatcher service. It also means they'd probably need to be different classes in order to ensure each obeyed the implicit interfaces for the originals.

I don't bring this up to condem the Symfony or Oro core teams. I bring it up to identify a pattern that exists in almost every programatic computer system out there. These systems are built, in large part, by programmers to solve programmer's problems. However, the code that bootstraps these systems needs to solve problems the old, pre-system way.

In a sufficiently complex system the bootstrap code ends up resembling a weird hybrid that's not quite raw programming language, but not fully conformant with the rules of the system. This is as true for Symfony as it is for the *nix bash shell.

This both because a partially bootstraped system doesn't have accesses to the same resource, and also because the developers are used to working with the patterns they've developed for the system itself and start incorporating different versions of them in the bootstrap code. It's systems all the way down.

If you're planning on sticking to the programmers path, learning to accept this will help you quickly identify the parts of a system that behave like this, and quickly track down bugs related to these differenence.