18
Event dispatching in different frameworks
I first encountered events when I was working with Joomla!. I had to create some plugins, and quickly found out most of these plugins were using events to inject certain behavior. At the time I found it a difficult concept to grasp. Moving on from Joomla! to WordPress, these things kept coming up, and even when I started using Symfony they were everywhere. I’ve come to embrace them, and I never want to go without again.
Imagine a worker in a factory; she makes stuff, and she is good at it. At some point her manager wants her to keep an inventory record of what she’s made, every time she finishes one of her products. She doesn't like this, because she feels this isn’t her responsibility, but rather that of the inventory department. Her manager agrees, and they decide she should tell inventory what she made when she is done. Inventory then records what she made, at what date and time, and any other relevant information.
This works out great, but after a while she is asked to also tell her manager when she finishes one of her products, so he can keep better track of her performance. Yet instead of agreeing, she realizes this will take her away from her work, so she provides an alternative; “What if I just tell everyone at once when I’ve finished my product; so anyone who wants to, can respond and act on it?”. Apart from her idea of just yelling “Another one!” every time she’s finished, the manager agrees. She could keep working, and wasn't asked to do anything else in the future. The manager would keep a record for his statistics, and inventory responded by updating their records.
This is pretty much what event dispatching is. Send out a message, optionally (but preferably) with some context, to anyone who wants to respond or act on it.
Now that we have a bit of an idea as to what event dispatching is, let's zoom in a bit further and meet the players of working with events.
An event can, in theory, be anything you’d like. As long as it has a unique name or ID. This name is what lets everything know what event to respond to. As we’ll see further on, events can in some cases be a simple value or even no value at all; but in most modern frameworks it is a simple class
object containing the context. Because a class
already has a name, this name widely adopted as the event name.
We’ll explore events more later with some examples in different frameworks.
Since code cannot shout, it needs its equivalent of a megaphone to make sure all who want can hear. This megaphone is called a Dispatcher, because it dispatches events to anyone interested (listeners).
The dispatcher is the middle-man in the communication between events and listeners. Because of this, the dispatcher is the one who receives the event from the triggering code (sometimes called an Emitter); either a controller or a service. And because the dispatcher sends out the event, it also needs a list of listeners that want to act on the event, so it can trigger all of them. This list is sometimes called a Listener Provider.
Note: You can probably forget the terms: Emitter & Listener Provider. These are merely words for: your existing code that calls the dispatcher & the configuration that maps the listeners to the event. The latter will be different and be named differently in every framework.
A listener contains code to be executed when the event is dispatched. A listener only listens to one event. An event can however have an unlimited amount of listeners. So multiple listeners can listen to the same event, and do completely different things.
A listener is always a callable
, either an anonymous function, a method on a specific class, or even an invokable class. This callable will receive the event object or value that was provided as its argument; or context. Sometimes the name of the event is enough information for the listener to act upon; but most of the time it will need the event object or value.
Maybe you have heard of subscribers in relation to events. It might look and sound like subscribers and listeners are pretty much the same, but they’re not. Like was stated earlier: a listener only listens to one event. A subscriber on the other hand, is a class that registers multiple listeners to various events. Subscribers are mostly used to group listeners that are needed to add a single feature or functionality.
So, as a quick recap: events are (usually) objects with a unique ID, that are sent by a dispatcher to listener callables that want to respond to that event.
Fact: Did you know that there is a list of recommendations for PHP on how to work with certain concepts? These are called PHP Standard Recommendations; or PSRs. One of these recommendations is PSR-14: Event Dispatcher. This recommendation explains in technical terms how event dispatching could work.
There are two options when it comes to dispatching an event; you can either send a selection of value parameters to the listener, or you can send an event object. Let’s take a closer look at these options.
If there is one framework that has value events elevated to an art form, it’s WordPress. This framework is absolutely littered with events that can pretty much change everything you would like. It does this by dispatching an event-name, accompanied by a variable set of parameters. When the event is dispatched, the listener actually receives all these parameters as its arguments. The first argument however is the value that can be modified. This type of event is a called a hook
(more on those later on). The rest of the arguments are available to the listener as context. These can
be absolutely anything.
PHP wil pass a scalar value (string
, bool
, int
or float
) or even an array
as a copy to the receiving function or callable. This means you cannot change the variable that is provided. Therefore, the listeners return
the new value back to the dispatcher.
Note: While it is possible to change a value of a variable via a function if it is passed by reference; this is tricky business as are changing the value inside the variable. And that is not always what you want; e.g. when you use that same variable later on and expect it to still be the original value.
The value approach is useful for simple values like booleans or strings. WordPress even has little functions like __return_true()
that will always return true
. These are handy to quickly change a setting via an event. A downside to this approach is that, as your context arguments grow, the listener callable should also receive them as arguments. I’ve been in situations where I (only) needed the 11th(!) parameter as context to change the first argument.
Example of value events in WordPress:
// Somewhere in a plugin the `get_title` event is dispatched.
$post = get_post();
$title = 'Original title';
$title = apply_filters('get_title', $title, $post);
// Somewhere in a functions.php for example:
// Adding the post ID before the title.
add_filter('get_title', function (string $title, ?WP_Post $post): string {
return ($post ? $post->ID . ': ' : '') . $title;
}, 10, 2);
Almost every other framework or Event Dispatcher implementation works with event objects. This object is created inside the calling code and then dispatched. Because an object is a class, you can add any methods to it. Methods for reading values, or adding them if necessary. And unlike scalar or array values, objects are passed to a function or callable by reference. This means the listener is not working on a copy, but on the exact same instance of that object. So a lister doesn't return
anything, but can interact with the object via the provided methods.
One big advantage of using objects over values, is you can actually change multiple values in the same object. The listeners also only receive the event object as a parameter, so you are free to add more context to that event without having to change the callable or function definition.
As opposed to WordPress' implementation, the code for an object based event dispatcher can be a bit more verbose. Although, to be fair, WordPress hides their dispatcher in a global variable that is accessed by the aforementioned functions.
Example of object events in thephpleage/event:
This particular event dispatcher is an implementation of PSR-14.
// Somewhere you subscribe to the event.
$dispatcher->subscribeTo(PostCreatedEvent::class, function (PostCreatedEvent $event): void {
$post = $event->getPost();
$newTitle = sprintf('%d: %s', $post->getId(), $post->getTitle());
$post->setTitle($newTitle);
});
// Somewhere in the source code.
$post = $this->createPost('Original title');
$event = $dispatcher->dispatch(new PostCreatedEvent($post));
$title = $event->getPost()->getTitle(); // Will have the new title.
Note: the dispatcher actually returns
the event instance after dispatching it to all the listeners.
So while value events have their place and use, it’s recommended to always go for event objects because they are more flexible and easily extendable in the future.
Side note: While it isn’t common practice in WordPress plugins or themes to use event objects, I think it really should be, as it makes for a more enjoyable developer experience. Just create an event object inside your plugin and dispatch that when it makes sense.
Now that you (most likely) have a basic understanding on how events are dispatched, it’s time to dig a little deeper. What exactly happens when you "dispatch an event"?
When the dispatcher sends the event to the associated listeners, it cannot send it to all listeners at once. Like the rest of code in PHP, the events have to be called sequentially, meaning they are called one after the other. And not only that, they share and pass along the same event object or value.
It’s like a sign-up sheet; the sender gives the sheet to the first person. This person adds their name to it and passes the sheet to a second person; they also sign up, passing it along until every one is done. After that, the complete list is returned to the sender. Not everybody has to sign up, but they will pass the sheet along.
The same holds true for events. The event is created by the code and then send, through the dispatcher, to the first listener. Yet, unlike Middleware, the listeners do not call the next listener; instead the dispatcher will pass the event to every listener. This process of passing along the event is called Event propagation.
Because event dispatching is like this game of hot potato, the order in which the listeners are called can be important. Therefore, all event dispatcher implementations have a concept of priority. In almost every implementation this priority is set by adding a weight to the listener. The dispatcher then sorts the listeners on this weight before it begins sending the event. Laravel had this ability up until version 5.4, but chose to remove support.
Examples:
// WordPress will have a priority number. The lower the number, the earlier it will be called.
add_filter('the_event_name', '__return_true', 10);
// Symfony works the other way around. The higher the number, the more priority it will have.
$dispatcher->addListener(SomeEvent::class, [$listener, 'onBeforeSave'], 30); // 0 is the default.
// `thephpleage/event` package also prefers a higher number for earlier calls.
$dispatcher->subscribeTo(SomeEvent::class, [$listener, 'onBeforeSave'], PHP_INT_MAX); // Must be the first call.
Sometimes a listener is so important, it should prevent any further listeners from being called for this event. It has to essentially short-circuit at that point. That’s why most frameworks have a concept of stoppable events. This means the event (object) has a function
that can be called to signal the dispatcher to stop any propagation of the event. The dispatcher has to listen to this call. It is not possible to cancel this action.
Examples:
Not all frameworks have the same approach. And for now WordPress has no support for stoppable events.
// Symfony has a base `Event` class that contains a `stopPropagation()` method.
// The same holds true for the `thephpleage/event `package`, but there you extend `StubStoppableEvent`
// or implement `StoppableEventInterface` (PSR-14).
public function listener(SomeEvent $event): void {
// Do stuff here.
$event->stopPropagation();
}
// Laravel will stop propagation if the listener returns `false`.
public function listener(SomeEvent $event) {
// Do stuff here.
return false;
}
There are two types of events. You can create hooks or actions. WordPress actually doesn't refer to "events" anywhere in their documentation. They are all-in on these two types by providing apply_filter()
(hook) and do_action()
(action). So what’s the difference? It all depends on whether your code needs the result.
A hook is an event that is also used by the dispatching code after it has been dispatched, and before it performs some action. For example: you have a function that sets a title on an object. This title has a sensible default, but you want your users to be able to overwrite it. You can dispatch an event that has a setTitle()
method to change it. The code will then use the value from the event.
Yet it can also be used to retrieve some data the function will work on. For example: you have an importer that imports every hour. This importer can work with any class that implements a certain interface. You dispatch an event to determine what classes it should use to import. The event has an addDataSource()
method that a listener can use to add a data source. This means you can have multiple data sources, that only respond when they want to. So even if your importer runs every hour, the data source doesn't have to be included every hour.
Example:
// Laravel style of event dispatching.
$event = DetermineDataSourcesEvent::dispatch();
foreach($event->getImporters() as $importer) {
$this->importFrom($importer);
}
Note: because a hook is used by the code before an action is triggered, it is common to name them in present tense: e.g.
DetermineDataSourcesEvent
orBeforeSaveEvent
.
An action is an event that is dispatched only to signal listeners to respond as a by-product. The code itself doesn't use the event after it has been dispatched. Actions are therefore usually dispatched after an action is performed. The event is created with the context of the action. For example, when a blog post was created, it will usually attach that post as context to the event, so the listeners can do something with it.
Note: because an action is dispatched after an action, it is common to name them in past tense: e.g.
BlogPostCreatedEvent
orAfterSaveEvent
.
Example:
// Symfony style of dispatching an event.
use Symfony\Component\EventDispatcher\EventDispatcher;
$post = $this->createBlogPost(); // Some code that creates the blog post.
$dispatcher = new EventDispatcher();
$dispatcher->dispatch(new BlogPostCreatedEvent($post));
To summarize; hooks can be viewed as before
-events, and actions as after
-events.
Perhaps you have heard of webhooks
and are wondering if those are just another term for hooks
. They are not. Though webhooks are events, they are events that are triggered via an HTTP request. Meaning the "listeners" for these events are (other) websites URLs. Therefore, the listener does not have to be a PHP script. It can be any webbased application. It just needs to understand the request.
Events are a powerful tool to decouple your code, and make it more readable. It offers you a way to extend your existing code, without having to change the business logic underneath. Most frameworks have a form of event dispatching and all have their pro's and con's.
18