Symfony Event Subscriber Tutorial


Previously we saw how we could create our own custom events, and use Symfony's event dispatcher to send (or dispatch) these events out, where after other interested parties would be notified of these events.

We saw how we could create our own Event Listener, along with a small amount of service configuration, which would give us one way of listening for / being notified about our custom event.

From my experience with teaching other Symfony developers about Events, the biggest point of confusion comes when the topic of Event Listeners vs Event Dispatchers is raised.

I mentioned in the first video in this series that Symfony's Event Listeners and Event Subscribers achieve the same goal in different ways. Ultimately both want to be notified when an event takes place, and then take some action accordingly.

Where Event Subscribers differ from Event Listeners is in their configuration.

With Event Listeners you have to provide some service configuration. Taking our previous example, we must provide something like this:

# app/config/services.yml

services:

    # Symfony 3.3 approach
    AppBundle\Event\Listener\FunEventListener:
        tags:
            - { name: kernel.event_listener, event: some.event.name }

    # Prior to Symfony 3.3
    crv.event.listener.fun_event_listener:
        class: AppBundle\Event\Listener\FunEventListener
        tags:
            - { name: kernel.event_listener, event: some.event.name }

However, from Symfony 3.3 onwards, with Event Subscribers you may be able to let autowiring and autoconfiguration take care of all your service configuration for you. In other words, you may not need to add any custom service definition at all:

# app/config/services.yml

services:

    # Symfony 3.3 approach with autowiring / autoconfiguration
    _defaults:
        autowire: true
        autoconfigure: true
        public: false

    # Prior to Symfony 3.3
    crv.event.subscriber.fun_event_subscriber:
        class: AppBundle\Event\Subscriber\FunEventSubscriber
        tags:
            - { name: kernel.event_subscriber }

You may now be wondering then: how can Symfony 3.3 figure out that FunEventSubscriber should become a service that is configured as an Event Subscriber?

Let's look at a basic implementation of an Event Listener:

<?php

// src/AppBundle/Event/Subscriber/FunEventSubscriber.php

namespace AppBundle\Event\Subscriber;

use AppBundle\Event\Events;
use AppBundle\Event\FunEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class FunEventSubscriber implements EventSubscriberInterface
{
    // return an array containing
    // the subscribed event(s)
    // and the method(s) to call when each event
    // is received
    public static function getSubscribedEvents()
    {
        return [
            Events::FUN_EVENT => [
                [
                    'logEvent', // method to call
                    // optional priority, default 0
                ],
                [
                    'processEvent',
                    10, // 10 is explicitly the priority here
                ],
            ],
            Events::ANOTHER_EVENT => [
                // ...
            ]
        ];
    }

    public function logEvent(FunEvent $event)
    {
    }

    public function processEvent(FunEvent $event)
    {
    }

    // etc
}

When using Symfony 3.3 onwards, and given that we have autoconfiguration enabled (which it is by default) then Symfony can deduce that our FunEventSubscriber is an Event Subscriber simply because we implements EventSubscriberInterface.

As a result, Symfony can configure this service for us, and crucially, add the expected service tag:

tags:
    - { name: kernel.event_subscriber }

All without us needing to do anything.

If you are using Symfony 3.2 or lower, or you have explicitly disabled service autoconfiguration, you will still need to manually tag your service:

# app/config/services.yml

services:

    # Symfony 3.3 approach without autowiring / autoconfiguration
    AppBundle\Event\Subscriber\FunEventSubscriber:
        tags:
            - { name: kernel.event_subscriber }

Also, if we remember back to the first video in this short series, we saw that when creating an Event Listener we had two options for determining which method would be called when an interesting event was received:

# app/config/services.yml

services:

    # Symfony 3.3 approach
    AppBundle\Event\Listener\FunEventListener:
        tags:
            - { name: kernel.event_listener, event: some.event.name }

    # Prior to Symfony 3.3
    crv.event.listener.fun_event_listener:
        class: AppBundle\Event\Listener\FunEventListener
        tags:
            - { name: kernel.event_listener, event: some.event.name, method: onCheeseAndTomatoToasty }

Taking the Symfony 3.3 config we should expect that when a FunEvent is dispatched, that FunEventListener would be notified, and it would call its onSomeEventName method with a FunEvent as its first argument.

Note here that there are two other arguments passed in that are often not needed, and therefore not specified. These are the event name itself, and the Event Dispatcher as the second and third arguments respectively.

And as we learned previously, we can - if needed - specify a custom method name (onCheeseAndTomatoToasty) to be called instead, which would still receive a FunEvent as its only argument.

Given that with Symfony 3.3 autoconfiguration we do not need to provide any service configuration, nor as we have also seen, with Symfony 3.2 or below we do not need to say which method (or methods) to call when a certain event is dispatched.

How then can Symfony determine which method or methods it should call when an interesting event is received?

Remember that our Event Subscriber had to implements EventSubscriberInterface.

In implementing EventSubscriberInterface we are contractually obligated to implement one method:

public static function getSubscribedEvents

This method must return an array.

This returned array will contain more arrays, which at first glance may very well appear confusing.

Here's the documentation:

The array keys are event names and the value can be:

  • The method name to call (priority defaults to 0)
  • An array composed of the method name to call and the priority
  • An array of arrays composed of the method names to call and respective priorities, or 0 if unset

For instance:

  • array('eventName' => 'methodName')
  • array('eventName' => array('methodName', $priority))
  • array('eventName' => array(array('methodName1', $priority), array('methodName2')))

We will cover some examples momentarily.

The important thing to note here is that with an Event Subscriber the event we are listening for, and the methods that we run as a result of receiving that event, are stored as configuration of our Event Subscriber.

This keeps the configuration and the implementation together.

This is an important difference to the way Event Listeners work, where we had configuration in a .yml file, and then our implementation in a .php file.

Knowing this, in Symfony 3.3 with autoconfiguration enabled this is another reason how things can "just work" without us needing to do too much.

Event Subscriber Examples

To start we will just use dump.

This means we don't need to inject anything.

<?php

// src/AppBundle/Event/Subscriber/FunEventSubscriber.php

namespace AppBundle\Event\Subscriber;

use AppBundle\Event\Events;
use AppBundle\Event\FunEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class FunEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            Events::FUN_EVENT => [
                [
                    'logEvent',
                ],
            ],
        ];
    }

    public function logEvent(FunEvent $event)
    {
        dump('log event');
    }
}

We hit our site, and boom, log event is dumped for us to the web debug toolbar.

We can get fancy and add two log statement in:

<?php

// src/AppBundle/Event/Subscriber/FunEventSubscriber.php

namespace AppBundle\Event\Subscriber;

use AppBundle\Event\Events;
use AppBundle\Event\FunEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class FunEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            Events::FUN_EVENT => [
                [
                    'logEvent',
                ],
                [
                    'logAnotherEvent',
                ],
            ],
        ];
    }

    public function logEvent(FunEvent $event)
    {
        dump('log event');
    }

        public function logAnotherEvent(FunEvent $event)
    {
        dump('log another event');
    }
}

Each method call is another array. It may feel a little verbose. This would be similar in JavaScript with [{a:1}, {b:2}]. Of course you can put everything inline in PHP too.

When running this now, we get:

  • log event
  • log another event

Dumped to our web debug toolbar dump output.

We can use priorities to determine their execution order:

    public static function getSubscribedEvents()
    {
        return [
            Events::FUN_EVENT => [
                [
                    'logEvent',
                ],
                [
                    'logAnotherEvent', 10
                ],
            ],
        ];
    }

When running this now, we get:

  • log another event
  • log event

In other words, the higher the priority, the sooner your method will be called.

You can use positive and negative numbers here. The default priority is zero.

Symfony Event Listener vs Subscriber

What follows is my own opinion.

I prefer Subscribers.

There's a good snippet from the docs on this:

  • Subscribers are easier to reuse because the knowledge of the events is kept in the class rather than in the service definition. This is the reason why Symfony uses subscribers internally;
  • Listeners are more flexible because bundles can enable or disable each of them conditionally depending on some configuration value.

I generally don't make bundles for sharing in this sense, so the benefits of using Listeners just isn't there for me.

Subscribers give me more flexibility, and as of Symfony 3.3 are just easier to use.

This is the theory, in the next video in this series we will look at a practical, more real world example.

Episodes