An Introduction to Symfony Events


Pretty much any time you interact with a Symfony application, behind the scenes (or under the hood if you prefer), a whole bunch of events are created, dispatched, listened for, and potentially, responded too.

You may have heard about events but wonder what they are. Simply, an event is something that has already happened. It might have happened only a few microseconds ago, but it is a 'historical' thing.

When working with a Symfony framework application, an event will be a plain old PHP class that you create. They tend to be quite small, with only getters, and no setters. To set we tend to use the constructor.

There's only one thing we must do to make our own events work with the Symfony framework, and that is to extend Event.

In this case, Event is shorthand for Symfony\Component\EventDispatcher\Event, which means our event class will need to:

use Symfony\Component\EventDispatcher\Event;

Don't worry about any of this, as it becomes much clearer with examples.

The Symfony framework comes with a whole bunch of pre-configured events. But you shouldn't just take my word for it. You can see this for yourself:

symfony framework events list for a default controller action

And this is just a truncated section of output.

Even when you run a Symfony console command, there are still a bunch of events dispatched.

However, because events are so 'noisy', by default, information about events is not written to your log files:

# app/config/config_dev.yml

monolog:
    handlers:
        main:
            type: stream
            path: '%kernel.logs_dir%/%kernel.environment%.log'
            level: debug
            channels: ['!event']

Notice here how channels has !event?

In this instance, the ! denotes that any log messages received from the events channel should be excluded / ignored, and not written to the logs. If you'd like to know more about using Monolog with Symfony, I would recommend my 10 tips for a better Symfony debug log video.

As you might expect, with this !event entry in our list of channels, when a console command is run, no event information ends up in the logs.

Now, if we remove the !event entry, then re-run even a freshly generated Symfony console command, suddenly we see a bunch of event data:

[2017-09-09 09:17:59] event.DEBUG: Notified event "console.command" to listener "Symfony\Component\HttpKernel\EventListener\DebugHandlersListener::configure". {"event":"console.command","listener":"Symfony\\Component\\HttpKernel\\EventListener\\DebugHandlersListener::configure"} []
[2017-09-09 09:17:59] event.DEBUG: Notified event "console.command" to listener "Symfony\Component\HttpKernel\EventListener\DumpListener::configure". {"event":"console.command","listener":"Symfony\\Component\\HttpKernel\\EventListener\\DumpListener::configure"} []
[2017-09-09 09:17:59] event.DEBUG: Notified event "console.command" to listener "Symfony\Bridge\Monolog\Handler\ConsoleHandler::onCommand". {"event":"console.command","listener":"Symfony\\Bridge\\Monolog\\Handler\\ConsoleHandler::onCommand"} []
[2017-09-09 09:17:59] event.DEBUG: Notified event "console.terminate" to listener "Symfony\Bundle\SwiftmailerBundle\EventListener\EmailSenderListener::onTerminate". {"event":"console.terminate","listener":"Symfony\\Bundle\\SwiftmailerBundle\\EventListener\\EmailSenderListener::onTerminate"} []
[2017-09-09 09:17:59] event.DEBUG: Notified event "console.terminate" to listener "Symfony\Component\Console\EventListener\ErrorListener::onConsoleTerminate". {"event":"console.terminate","listener":"Symfony\\Component\\Console\\EventListener\\ErrorListener::onConsoleTerminate"} []
[2017-09-09 09:17:59] event.DEBUG: Notified event "console.terminate" to listener "Symfony\Bridge\Monolog\Handler\ConsoleHandler::onTerminate". {"event":"console.terminate","listener":"Symfony\\Bridge\\Monolog\\Handler\\ConsoleHandler::onTerminate"} []

Six event entries for a command that does nothing. No wonder that the event channel is suppressed by default.

Our First Event

Let's start by creating a simple event class:

<?php

// src/AppBundle/Event/FunEvent.php

namespace AppBundle\Event;

use Symfony\Component\EventDispatcher\Event;

class FunEvent extends Event
{
}

Yup, that's right, this class has no body.

As mentioned, the only thing our event has to do is to extends Event.

Typically you will want to pass some information (sometimes called 'context', but I do not like that word) into your event, and we will do this ourselves shortly.

Anyway, we have our event.

Now we need to do two extra things:

  • Dispatch the event
  • Listen for, or Subscribe to this event

To break this down:

Dispatch means to send. We will use Symfony's event dispatcher to send (or dispatch) our event.

Dispatched events don't do anything by themselves. They rely on us creating listeners, or subscribers, to get notified about this new event and take some action accordingly.

We will get on to the similarities and differences between event listeners and event subscribers shortly. They both achieve the same goal, but in a different way. Again, we will cover symfony event listeners vs subscribers with examples shortly.

Dispatching an Event from a Controller

We have created our first event.

We've seen an event class needs to extends Event, but aside from that it's a plain old PHP class.

Next, we need to instantiate that class, and then we can pass it as an argument to the event dispatcher, which will take over proceedings.

To illustrate this point, we are going to dispatch our event from a Symfony controller action.

This process works identically when using a Symfony service, as we shall see shortly.

<?php

// src/AppBundle/Controller/DefaultController.php

namespace AppBundle\Controller;

# we need the `use` statement for our event
use AppBundle\Event\FunEvent;

# we also need to `use` something implementing EventDispatcherInterface
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

# all the other standard `use` statements here
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/", name="homepage")
     */
    public function indexAction(Request $request, EventDispatcherInterface $eventDispatcher)
    {
        // Symfony 3.3 onwards allows us to easily inject our dependencies
        // directly into controller actions

        // prior to Symfony 3.3, we could grab the event dispatcher from the container
        // $eventDispatcher = $this->get('event_dispatcher');

        $eventDispatcher->dispatch(
            'some.event.name',
            new FunEvent()
        );

        return $this->render('default/index.html.twig', [
            'base_dir' => realpath($this->getParameter('kernel.project_dir')).DIRECTORY_SEPARATOR,
        ]);
    }
}

At this point you should be able to browse to your site, e.g. http://127.0.0.1:8000/ and... well, nothing happens. Sure, you see the homepage, but if you check the logs, you won't see anything about some.event.name, or our FunEvent.

Well, that's no fun :(

Listening For Fun Events

We're two thirds of the way there.

The final step is to create either and event listener, or an event subscriber, that will be notified about our some.event.name taking place.

What's really cool about this is that whatever setup we use, this listener / subscriber will be passed the FunEvent object. Whilst this isn't super useful to us in our basic example, it does open up a whole range of possibilities in real world scenarios.

Let's add an event listener:

<?php

// src/AppBundle/Event/Listener/FunEventListener.php

namespace AppBundle\Event\Listener;

use AppBundle\Event\FunEvent;

class FunEventListener
{
    public function onSomeEventName(FunEvent $event)
    {
        dump($event);
    }
}

It's a plain old PHP class. We don't need to extends or implements anything.

The name of the class can be anything. I'm interested in listening for FunEvents, so for that reason I call this class the FunEventListener. You can name it whatever.

The method name - onSomeEventName - is important.

By default, Symfony will try to call a method with a camel-cased representation of your event name.

Remember, when we dispatched our event, we named it some.event.name:

$eventDispatcher->dispatch(
    'some.event.name',
    new FunEvent()
);

Therefore, by default, Symfony will try to call onSomeEventName.

You can change this. You can either change the event name, or specify a custom method to be called - more on this in a moment.

One other important thing to note:

onSomeEventName / your method will be called with your event as its only argument.

This is incredibly useful, and is where most of the power lies in using events.

Service Configuration

This wouldn't be a Symfony tutorial without a bit of service configuration :)

Ok, so Symfony is good, and with Symfony 3.3's dependency injection improvements, it's getting better. But it's not able to read your mind. Not yet, anyway.

Therefore we need to tell Symfony about our new Event Listener setup.

We're going to need to provide a service definition, and most importantly, tag our service to ensure it is correctly registered for listening for some.event.name events.

Depending on what version of Symfony you are using, depends on how you might approach 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 }

Of course, feel free to change the service name.

By this point you should be able to visit the web page for your controller action and see your FunEvent object being dumped onto the web debug toolbar.

This proves our setup works, even if it doesn't do anything particularly interesting at this point.

Call A Different Method

If you want to change the method name, that's easy too:

<?php

// src/AppBundle/Event/Listener/FunEventListener.php

namespace AppBundle\Event\Listener;

use AppBundle\Event\FunEvent;

class FunEventListener
{
    public function onCheeseAndTomatoToasty(FunEvent $event)
    {
        dump($event);
    }
}

Just add the method info:

# app/config/services.yml

services:

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

    # 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 }

Cool and the gang.

The last thing I would suggest here is to move your event listener configuration into a separate file. Particularly with Symfony 3.3 onwards, there are benefits to doing this:

# app/config/services/event_listener.yml

services:

    # config as usual

And update your config.yml file to include this new file:

# app/config/config.yml

imports:
    - # other stuff here
    - { resource: services/event_listener.yml }

You need not do this, but it works well on bigger projects in my experience.

Summary

In this video we covered how to create our first custom event inside the Symfony framework.

We learned how to dispatch an event using the Symfony event dispatcher service.

We also learned how configure a custom Symfony event listener service definition, and to how this allows us to react to dispatched events.

Finally, we covered how to customise the method that is called when our event listener is triggered.

Episodes