Modular Application Architecture - Events
When developing software, sometimes we need to allow our application to have plug-ins or modules developed by third parties. In this post we will start looking on how to implement a plugin-system by using "events".
This is the second post from a series of posts that will describe strategies to build modular and extensible applications. In this post we will start looking on how to implement a plugin-system by using "events".
One of the most common approaches to implement a plugin-based architecture is to use "events". The idea is that the application core "throws" events, modules listen to those events and react accordingly.
Modules (often called as plugins) can also "throw" events, making the whole mechanisms even more powerful since is possible to build a sort of communication between modules not coordinated directly by the core.
The event listeners can react to the events and eventually return some data to the core. The core application can use the received data to achieve some goal (as example displaying them to the user or using them to "throw" other events).
This strategy is used by popular frameworks as Symfony or by WordPress. WordPress calls this approach "hooks", while Symfony and Laravel calls them "events" and they are managed by an "event dispatcher".
Event Dispatcher
Here we will analyze in detail how the core application can throw events, how a plugin can listen to them and how is possible to use this mechanism to build a plugin system.
- The application core throws
events
. To make the system more flexible and performant events have aname
, so event listeners (plugins) can register on specific events without having to listen to each single event thrown by the core. - The events might have a
payload
. A payload contains information associated to the event, making it more meaningful. In this way plugins have more info about the context where the event was thrown as just the name could be not enough. - Event listeners can
return
some data back to who has thrown the event. The returned data should be in a format that can be understood by the who has thrown the event.
Event features
Is possible to add or remove some characteristics from the event system, this will give us in exchange some features and/or limitations. Even the "name", ""payload" and the "return" characteristics can be removed from the event system making it extremely minimal.
- removing "name": by removing the event name we will make our system more complex and less performant as listeners will have to listen to all the events and decide by themself to react to the event or not. Removing the event name is an uncommon choice and therefore not recommended.
- removing "payload": by removing the payload we make our event less useful as the only info available information for listeners to understand the context of the event is the name. The event listeners will have to find alternative ways to get the context information.
- removing "return": by removing the return we make our event listeners less able to interact with the core application. Effects to the system have to be implemented directly by the plugin using alternative ways. The core application is not able to "use" directly the effects of a plugin. Having return values is common, not having them makes easier to introduce asynchronous events.
- adding "stop event propagation": by giving to event listeners the ability to stop the event propagation (by returning a special value as example), it will be possible to influence the "event" effects thrown by the core on other plugins.
- adding "listener priorities": by adding listener priorities we allow plugins to co-operate better, especially when the "payload" and "stop event propagation" feature is available. It is possible to decide the order of execution of event listeners or to execute only one event listeners even if many are registered on the same event.
- adding "wildcard event listeners": when necessary to listen on multiple events, on all events or on events where the name is only partially known, can be useful to be able to listen on all the events. A common case for this are profiling plugins or debug features.
- adding "ability to throw event": having plugins able to throw events is a useful feature that allows to crete "plugins for plugins" making the whole system more flexible. A drawback of too much flexibility is that can go out of control really easily.
Asynchronous events
If we decide to have only "name" and a "payload" into our event system, it can be easily converted into an asynchronous event system. This because each event is independent from the others (this can be valid for web applications that have a synchronous flow, while with desktop applications or multithreaded/concurrent languages the situation is quite different).
Event listener registration
As said in the first article, a fundamental step is the plugin registration. Both, discovery and configuration strategies can be used.
Example implementations
Implementing and event system is a pretty common task, this means that out there there are already many implementations we can use instead re-inventing the wheel.
All of them have specific features and is just about choosing the best fit for us. As is possible to see here below, all of them offers you slightly different features and have slightly different behaviours, but in general they are very similar.
We are going to see how is possible to implement a side-bar having the content created dynamically by plugins. We will see how to do it using Symfony, Laravel and Wordpress.
Symfony
<?php
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\Event;
// the event and payload
class SidebarEvent extends Event
{
private $placement;
private $items = [];
public function __construct($placement)
{
$this->placement = $placement;
}
public function getPlacement()
{
return $this->placement;
}
public function addItem(Item $item)
{
$this->items[] = $item;
}
/**
* @return Item[]
*/
public function getItems() : array
{
return $this->items;
}
}
// listeners/plugins
$addContactListener = function (SidebarEvent $ev) {
$ev->addItem(new Item('Contact on the ' . $ev->getPlacement()));
};
$addHomeListener = function (SidebarEvent $ev, $eventName, EventDispatcher $dispatcher) {
$ev->addItem(new Item('Homepage' . $ev->getPlacement()));
};
// here is the application
$dispatcher = new EventDispatcher();
// listener registration
$dispatcher->addListener('site_bar', $addContactListener, -100); // priority
$dispatcher->addListener('site_bar', $addHomeListener);
// application throwing events
$dispatcher->dispatch('site_bar', $event = new SidebarEvent('left'));
$items = $event->getItems();
// example use the return values
echo "<li>";
foreach ($items as $item) {
echo "<ul>" . $item->getTitle() . "</li>";
}
echo "</li>";
What do we have here?
- the
$dispatcher
variable is the standard symfony event dispatcher; the symfony event dispatcher is synchronous and it allows to listeners to stop the event propagation or to dispatch/throw new events. - we are using a custom
SidebarEvent
event that has$placement
(that says us where is the sidebar) as payload and allowsdata
as return value; setting the return value will stop the event propagation. - our event listeners are
$addContactListener
and$addHomeListener
as simple callbacks. - the event listeners are registered on the
site_bar
andadmin_site_bar
events using theaddListener
method. - our application throws some events (using the
dispatch
method); in this case we are using a specific event class (SidebarEvent
) and as payload we have the page url. $items
will contain the values eventually set by an event listener when calling thesetItems
method;$items
will be an an array ofItem
as enforced by the type hinting.
Laravel
<?php
use App\Events\SidebarEvent;
// the event and payload
class SidebarEvent
{
private $placement;
public function __construct($placement)
{
$this->placement = $placement;
}
public function getPlacement()
{
return $this->placement;
}
}
// listeners/plugins
$addContactListener = function (array $payload) {
list($ev) = $payload;
return 'Contact on the ' . $ev->getPlacement();
};
$addHomeListener = function ($payload) {
list($ev) = $payload;
return 'Homepage on the ' . $ev->getPlacement();
};
// listener registration
Event::listen('site_bar', $addContactListener);
Event::listen('site_bar', $addHomeListener);
// application throwing events
$items = Event::fire('site_bar', [new SidebarEvent('left')]);
// example use the return values
echo "<li>";
foreach ($items as $item) {
echo "<ul>" . $item . "</li>";
}
echo "</li>";
What do we have here?
- the
Event
is the default laravel facade; the laravel event system is also synchronous, has a payload (that by default has to be an array), supports broadcasting, wildcard events and return values. - we are using a custom
SidebarEvent
event that has$placement
as payload. - our event listeners are
$addContactListener
and$addHomeListener
as simple callbacks. returningfalse
will stop the event propagation. - the event listeners are registered on the
site_bar
andadmin_site_bar
events using theEvent::listen
facade method. - our application throws some events (using the
Event::fire
facade method); in this case we are using a specific event class (SidebarEvent
) and as payload we have the page url. $returnPageView
will contain an array with all the not false return values from all the events. since the values inside$returnPageView
can't be checked by type hinting, a sanity check at application level should be done
WordPress
<?php
// listeners/plugins
$addContactListener = function ($placement) {
echo "<ul>Contact on the $placement</li>";
};
$addHomeListener = function ($placement) {
echo "<ul>Homepage on the $placement</li>";
};
// listener registration
add_action('site_bar', $addContactListener);
add_action('site_bar', $addHomeListener);
// application throwing events
echo "<li>";
$returnPageView = do_action('site_bar', 'left');
echo "</li>";
What do we have here?
- we are using the basic action event system from wordpress; supports priorities, payload and return values
- our event listeners are
$addContactListener
and$addHomeListener
as simple callbacks. - the event listeners are registered on the
site_bar
andadmin_site_bar
events using theadd_action
function. - our application throws some events (using the
do_action
facade method); in this case we are using a simple string as payload we have the page url. $returnPageView
will contain the return value from the last event listener; since the value inside$returnPageView
can't be checked, a sanity check at application level should be done
Conclusion
The "event" plugin mechanism is commonly used when the application want to be able to control what plugins are able to do. Plugins can interact with the application only in the "extension points" decided by the application.
Advantages:
- Very flexible.
- High degree of control offered to the application: the application decides extension points and data structures use.
- Can be asynchronous.
- Transparent: the application does not need to be aware of how many plugins are registered and how they are structured.
- Documentable: return and payload can be explicitly defined.
Disadvantages:
- Limited to pre-defined extension points: there is not an easy way to interact with parts of the application that does not offer appropriate extension points.
- Transparent: since the application is not aware of what plugins do and how many they are, can be difficult to keep application quality; "bad" plugins can't be excluded.
- In some dispatcher implementations is difficult to understand if listeners were triggered
- Hard to debug and almost useless stack traces in case of errors.
Notes: Depending on the implementation you decide to use, some of the said advantages/disadvantages may change.
Events are one of the most popular extension mechanism, in the next article we will see how and when the "middleware" extension pattern can be used.
Looking forward to your feedback.