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:
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 FunEvent
s, 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.