How to start using Symfony Events

profile picture

Jan Engineer

11 Jan 2018  ·  8 min read


Ever had a class or controller that contained more code than you’d like, becoming nearly untestable because of the gigantic list of dependencies? Let us tell you about event-driven development, which allows you to split your code into smaller pieces and isolate code blocks into separate classes.

UPDATE 12 Jan: after a reply on the Symfony Devs Slack from SensioLab’s own Javier Eguiluz, we’ve updated the section on EventListeners vs. EventSubscribers below.

In this post, I’ll try to give you a small introduction to using the Symfony Event dispatcher component and how to implement it into your Symfony project. In a later post, we’ll go deeper into events and how we use them at November Five.

But first, let me try to explain what event-driven development is. The very short version: instead of using one big class, you split up your code and let the smaller bits listen to a ‘event’. When this event is triggered, the dispatcher will execute a chain of classes with a well-defined responsibility, which all listen to that ‘event’.

The Symfony EventDispatcher component

The Symfony EventDispatcher component provides tools that allow your application components to communicate with each other by dispatching events and listening to them (this is based on the mediator pattern).

The component is such a powerful tool because it provides all the necessary steps to make sure the chain is executed flawlessly:

  • The Event class itself
  • A dispatcher
  • A Listener
  • Debug tools

Let’s go into more details on each of these elements.

1. Creating an event

Symfony’s Event class is the base class for classes containing event data. This class itself doesn’t contain any event data. Instead, it is used by events that don’t pass state information to an event handler when an event is raised.

<?php 

use Symfony\Component\EventDispatcher\Event;

class CustomEvent extends Event 
{
     // Class functionality goes here 
}

2. Dispatching the event

The event dispatcher is the central object of the event dispatcher system. Generally, a single dispatcher is created, which maintains a registry of listeners. When an event is dispatched via the dispatcher, it notifies all listeners registered with that event. There are a number of different types of dispatchers.

The basic type:

  • EventDispatcher: The basic implementation of the dispatcher.

And a few special types:

  • ContainerAwareEventDispatcher: Lazily loads listeners and subscribers from the dependency injection container.
  • ImmutableEventDispatcher: A read-only proxy for an event dispatcher.

3. Listening for an event

The most common way to listen to an event is to use the EventListener class. This class allows you to subscribe to a single event. By configuration, you can decide if the EventListener runs the provided code in the chain or not.

You can also manipulate the execution chain by adding a ‘priority’ method in the ‘Tag’ context for your listener definition.

Services:

november_five_user.listener.user_registered:
    class: NovemberFive\Listener\EventTestListener
    tags:
        - { name: kernel.event_listener, event: event.test, priority: 10 }

Next to the EventListener, there is also the EventSubscriber. This defines one or more methods that listen to one or various events. The main difference with the listeners is that subscribers tell the dispatcher what they’re listening to, while an EventListener listens only to what it’s configured for.

<?php

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\Event;

class TestEventSubscriber implements EventSubscriberInterface
{

    public static function getSubscribedEvents()
    {
            return [
                    "test.event" => [
                    ["taskA", 0],
                    ["taskB", -10],
                      ]
            ];
    }

    public function taskA(Event $event): void
    {
             // ...
    }

    public function taskB(Event $event): void
    {
            // ...
    }
}

For a given subscriber, different methods can listen to the same event. The order in which methods are executed is defined by the priority parameter of each method (the higher the priority the earlier the method is called).

Now let’s make it practical

Inspired by what you have read above? Let’s try to implement it in a real-life scenario. Let’s say you have a UserBundle in your project (basic implementation) that allows you to login / logout / register and request a new password when you’ve lost your current one.

At some point, your project manager visits you to ask if it’s possible to add some functionality to the registration process. When you take a closer look at the required scope, you learn that the client wants users who are registering from a certain email domain to be added automatically to the customer user group belonging to that domain.

At first you think, hey! I can just add that code to my controller! But then you start to realise that the client might ask for more features like this in the future and the controller will get bloated with extra functionalities and variables. To prevent this from happening, you should apply the “Mediator Pattern”. (The EventDispatcher is an implementation of this in the Symfony framework.)

So where do we start?

1. We start by creating a UserRegistered event class

This class contains the core event that we will pass around in the listeners pool.

<?php 

namespace NovemberFive\UserBundle\Event;

use Symfony\Component\EventDispatcher\Event;
use NovemberFive\UserBundle\Entity\UserInterface as User; 

class UserRegisteredEvent extends Event 
{
    /** @var User */
    protected $user;

    /** ... */
    public function __construct(User $user)
    {
       $this->user = $user;
    }

    /** ... */
    public function getUser(): ?User
    {
        return $this->user;
    }

    /** ... */
    public function setUser(User $user): self
    {
        $this->user = $user;

        return $this;
    }
}

2. Then we dispatch the event from our controller

<?php

namespace NovemberFive\UserBundle\Controller;

use NovemberFive\UserBundle\Entity\User;
use NovemberFive\UserBundle\Event\UserRegisteredEvent;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

/**
 * Class RegisterController
 * @package NovemberFive\UserBundle\Controller
 */
class RegisterController extends Controller
{
    /**
     * @Template()
     *
     * @param Request $request
     * @return array
     */
    public function registerAction(Request $request)
    {
        // Form stuff
        // ...

        // Dispatching the event
        $userEvent = new UserRegisteredEvent($user);
        $user = $this->get('event_dispatcher')->dispatch('user.registered', $userEvent)->getUser();

        // Store the user to the database
        $objectManager = $this->get('doctrine.orm.default_entity_manager');
        $objectManager->persist($user);
        $objectManager->flush($user);

        // Returning the user
        return [
            'user' => $user,
        ];
    }
}

3. Then we create a listener

In this step, you can choose whether you want to create an EventListener or an EventSubscriber.

  • EventListener: The most common way to listen to an event is to register an event listener with the dispatcher. This listener can listen to one or more events and is notified each time those events are dispatched.
  • EventSubscriber: This is very similar to the the EventListener, except that the class itself can tell the dispatcher which events it should listen to.

UPDATED: While you can choose according to personal preference between these two types, it’s important to note that modern Symfony is moving away from Listeners. In Symfony v3.4+ and Flex, it’s recommended to stick to subscribers, which are then autoconfigured. This means you won’t need to add any YAML config to make it work anymore.

That being said, we’ll still explain how to use both types in this post. If you want to create an EventListener, you can look at the following example. You’d need a UserRegisteredListener:

<?php

namespace NovemberFive\UserBundle\Listener;

use NovemberFive\UserBundle\Event\UserRegisteredEvent;
use Symfony\Component\EventDispatcher\Event;

/**
 * Class UserRegisteredListener
 * @package NovemberFive\UserBundle\Listener
 */
class UserRegisteredListener
{
    /**
     * @param UserRegisteredEvent|Event $event
     * @return void
     */
    public function onUserRegistered(Event $event): void
    {
        $domainName = substr(strrchr($event->getUser()->getEmail(), "@"), 1);
        if ('novemberfive.co' === $domainName) {
            $event->getUser()->addRole('ROLE_CUSTOMER_NOVEMBERFIVE');
        }
    }
}

And a services.yml:

services:
    ...
    november_five_user.listener.user_registered:
        class: NovemberFive\UserBundle\Listener\UserRegisteredListener
        tags:
            - { name: kernel.event_listener, event: user.registered }

If you’d prefer to use an EventSubscriber, you need a UserRegisteredSubscriber:

<?php

namespace NovemberFive\UserBundle\Subscriber;

use NovemberFive\UserBundle\Event\UserRegisteredEvent;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Class UserRegisteredSubscriber
 * @package NovemberFive\UserBundle\Subscriber
 */
class UserRegisteredSubscriber implements EventSubscriberInterface
{
    use LoggerAwareTrait;

    /**
     * UserRegisteredSubscriber constructor.
     * @param LoggerInterface $logger
     */
    public function __construct(LoggerInterface $logger)
    {
        $this->setLogger($logger);
    }

    /**
     * @return array
     */
    public static function getSubscribedEvents()
    {
        return [
            "user.registered" => [
                ["appendClientGroup", 0],
                ["writeLog", -10],
            ]
        ];
    }

    /**
     * @param UserRegisteredEvent $event
     */
    public function appendClientGroup(UserRegisteredEvent $event): void
    {
        $domainName = substr(strrchr($event->getUser()->getEmail(), "@"), 1);
        if ('novemberfive.co' === $domainName) {
            $event->getUser()->addRole('ROLE_CUSTOMER_NOVEMBERFIVE');
        }
    }

    /**
     * @param UserRegisteredEvent $event
     */
    public function writeLog(UserRegisteredEvent $event): void
    {
        $this->logger->info(sprintf('Created a new user: %s', $event->getUser()->getEmail()));
    }
}

And a services.yml:

november_five_user.subscriber.user_registered:
    class: NovemberFive\UserBundle\Subscriber\UserRegisteredSubscriber
    arguments:
        - "@logger"
    tags:
        - { name: kernel.event_subscriber }

Note that in this example we are implementing the EventSubscriberInterface. If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes getSubscribedEvents and registers the subscriber as a listener for all returned events. The returned array works as following:

[   
    “<EventName>” => [ 
        [<Function within the class>, <Priority (High -> Low)>
    ]
]

Debugging your events

Great – you now know how to work with Symfony events! But do you know how to debug them?

Luckily, the Symfony console command provides you with some handy tools to debug your event flows:

php bin/console debug:event-dispatcher (full list)
php bin/console debug:event-dispatcher <event> (single event)
For an EventListener, this would look like this
> app/console debug:event-dispatcher user.registered

Registered Listeners for "user.registered" Event
=========================================

  Order     Callable                                                                               Priority  
  #1        NovemberFive\UserBundle\Listener\UserRegisteredListener::onUserRegistered()                0          

While for an EventSubscriber, you can use this example:

> app/console debug:event-dispatcher user.registered

Registered Listeners for "user.registered" Event
=========================================

  Order     Callable                                                                               Priority  
  #1        NovemberFive\UserBundle\Subscriber\UserRegisteredSubscriber::appendClientGroup()        0         
  #2        NovemberFive\UserBundle\Subscriber\UserRegisteredSubscriber::writeLog()                -10       

Conclusion

After reading this post, you should be able to dispatch a basic event and create a listener for it. After playing with it for a bit, I’m sure you’ll agree with me that the Symfony EventDispatcher component can be a lifesaver in certain situations (events vs. regular services).

What are those certain situations for us at November Five? That’s a question for our next blog post ;) In our next installment, we’ll dive into concrete situations where event driven development can improve your code.

Are you dispatching events yet?