Symfony2: Event subsystems

Posted on by Matthias Noback

Recently I realized that some of the problems I encountered in the past could have been easily solved by what I'm about to explain in this post.

The problem: an event listener introduces a circular reference

The problem is: having a complicated graph of service definitions and their dependencies, which causes a ServiceCircularReferenceException, saying 'Circular reference detected for service "...", path: "... -> ... -> ...".' Somewhere in the path of services that form the circle you then find the event_dispatcher service. For example: event_dispatcher -> your_event_listener -> some_service -> event_dispatcher.

Your event listener is of course a dependency of the event_dispatcher service, but one of the dependencies of your event listener needs the event_dispatcher itself! So you accidentally introduced a cycle in the dependency graph and now you need to dissolve it.

The wrong solution: injecting the service container

Assuming there is no way to redesign these services in a better way, you finally decide to make the event listener "container-aware". You will fetch some_service directly from the service container, which gets injected as a constructor argument:

use Symfony\Component\DependencyInjection\ContainerInterface;

class YourEventListener
{
    private $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
    }

    public function onSomeEvent()
    {
        $someService = $this->container->get('some_service');

        ...
    }
}

This will break the cycle since you depend on something else entirely: event_dispatcher -> your_event_listener -> service_container. However, it also introduces a nasty dependency on the service container, making your class coupled to the framework, the service definitions themselves, and making it less explicit about its actual dependencies. Besides, I really think that a dependency issue at configuration level should not affect a class this much.

The solution

There is an entirely different and much better way to break the cycle, which is to avoid any dependency on the main event_dispatcher in your own services at all. Let me explain this to you.

In the past, whenever I introduced a new event in my code, I used the application's main event_dispatcher service to dispatch the event and I also registered any listeners for the new event to the same event_dispatcher, using the kernel.event_listener service tag like this:

services:
    my_service:
        arguments:
            - @event_dispatcher

    my_event_listener:
        class: ...
        tags:
            - { name: kernel.event_listener, event: my_event, method: onMyEvent }

However, the application's main event dispatcher depends on so many services (via the event listeners registered to it), that sooner or later the dependency mess will start to show cycles. Still, to work with events, we need an event dispatcher (or event emitter, event manager, etc.). Since our code is already written with the Symfony EventDispatcherInterface in mind, we just need to work around the main event_dispatcher service.

Event subsystems

We can avoid the event_dispatcher altogether by introducing event subsystems.

In the old situation we depended on the event_dispatcher. In the new situation, each set of custom events comes with its own event subsystem. This subsystem consists of a dedicated event dispatcher, and an easy way to register event listeners for the custom events.

Since Symfony 2.3 it's really easy to set up your own event dispatcher and to conveniently register event listeners and subscribers in the same way as before.

Example: a subsystem for domain events

A highly relevant example would be an event subsystem for domain events. You don't want those events to be dispatched by the same event dispatcher that Symfony2 uses for its core events.

To be able to use the domain event subsystem, we first need to define a new event dispatcher service:

services:
    domain_event_dispatcher:
        class: Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher
        arguments:
            - @service_container

Now we want to register event listeners for the domain event subsystem in the following, familiar way:

services:
    some_domain_event_listener
        class: ...
        tags:
            - {
                name: domain_event_listener,
                event: my_domain_event,
                method: onMyDomainEvent
            }

In order to make this work you should register a compiler pass in one of your bundles. The compiler pass itself is already provided by the framework:

use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\HttpKernel\DependencyInjection\RegisterListenersPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;

class YourBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        $container->addCompilerPass(
            new RegisterListenersPass(
                'domain_event_dispatcher',
                'domain_event_listener',
                'domain_event_subscriber'
            ),
            PassConfig::TYPE_BEFORE_REMOVING
        );
    }
}

(As of Symfony 2.5 the RegisterListenersPass can be found in the Symfony\Component\EventDispatcher\DependencyInjection namespace.)

As you can see, we only need to provide the name of the event dispatcher service and the tag names we want to use, and this will all work. From now on you can inject the specific domain_event_dispatcher service and use the corresponding tags whenever you want to dispatch events within this event subsystem. This will cause much less of a dependency mess. But it will also make it clear by reading the service definitions, which kind of event subsystem the code is about.

By the way, you can read more on domain events here.

One last note

All of the above can only be applied to your own stand-alone application, library or bundle code. When you are creating event listeners for kernel events or for events dispatched from within third-party bundles, go ahead and use the main event_dispatcher service!

PHP Symfony2 events
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Guest

I'm having the same problem, but the circular dependency is part of one subsystem so having another dispatcher wouldn't help me. I have solved this by using setter injection for the event dispatcher, so my service checks if the event dispatcher is there before trying to dispatch anything. Seems to work like a charm! I haven't seen any better solutions but I'd be interested to hear it should one exist.

Sebastiaan Stok

There is one downside to making your own event dispatcher, they will not show up in the log.
To do this you need decorate them as is done with the 'default' one.
https://github.com/symfony/...

@cordoval:disqus The containeraware is only so that Event listeners/subscribers can be loaded lazily.
For a none framework situation you'd properly don't need this.

Matthias Noback

Yes, thanks for your suggestion!

cordoval

you guys have good ideas check this out http://www.craftitonline.co...

cordoval

also a note that this will not work on master symfony Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass should be added because of the change

Matthias Noback

Yes, I had already mentioned this underneath the code sample ;)

cordoval

thanks i missed it or maybe you added it later, thanks!

cordoval

the event dispatcher must be containeraware? i would rather have it non containeraware

Matthias Noback

Well, the Symfony event dispatcher is container aware in order to allow lazy loading of event listeners. I'm not really oppossed to it in this situation, though I understand your concerns about container-aware and I also share them with you.

Christophe Coevoet

But this kind of make your blog post void. Given the event dispatcher is using lazy-loading, it does not have a direct reference to the listeners, so there is no circular dependency anymore

Matthias Noback

He he, turns out I didn't actually test this situation! Luckily this doesn't make the blog post completely void, because it's still really nice to be able to have your own event subsystem, which would separate concerns. But yeah, I understand how the problem of circular dependencies doesn't need to be solved anymore. Thanks for bringing it up.

jobou

Great post. It should be in the cookbook.

mickaƫl andrieu

I can't agree more as this is a common issue in big applications, big up for a new cookbook.
Thank you Matthias :)

Matthias Noback

Thank you both, as I already quickly mentioned on Twitter, I don't think this should be a cookbook article, but just a note saying you don't need to use the event_dispatcher service, but can roll your own.