Michaël Gallego

Understanding the Zend Framework 2 event manager

The event manager is without any doubt one of the least known Zend Framework 2 component. Nevertheless, it is a very
powerful component which offers a lot of flexibility when used correctly. This short article aims to help you to use it.

Why using the event manager?

The event manager allows event driven programming. It allows to flexibly connect different parts of the code to each
other, without having a wrong, inverted dependency.

The event manager is extensively used internally (that’s why we said that ZF 2 is an event-driven framework). Thus,
contrary to Zend Framework 1, most of MVC elements (routing, dispatching, view…) are not called one after another.
Instead, framework triggers events (“dispatch”, “route”…). Other objects listen those events and, in turn,
do something in response of those events. We can extract some wording:

Objects trigger events. Those events are named (“route”, “dispatch”, “sendTweet”…) and often contain
additional parameters (for instance, an event called “sendTweet” could contain the tweet’s content).

Other objects listen (they are called listeners) those events to do something. In other ways, we attach objects to events.

The TweetService example

The nice thing about the event manager is that not only the framework can use them! Let’s take a simple example: we
have written a TweetService class whose goal is to send a tweet thanks to the Twitter API.

Our TwitterService now depends on the application. This specific application requires that a mail and SMS are sent,
but maybe another one will just send the tweet, while another one will add a message to a queue. This is exactly
what we want to avoid when we create a “generic” module whose aim is to be a “drop-in” module.

Furthermore, the TweetService depends on two other services too: an email service and SMS service. This means:
every time we want to use the TwitterService module, we also must download the EmailService and SMSService modules,
even if we don’t use them.

How to use it

Trigger an event

A very elegant solution to this problem is directly bundled into Zend Framework 2 : the event manager. Thus, we are
going to simply trigger an event to say “hey mate, a tweet has been sent” instead of hard-coding what to do. Our code
is then modified this way:

As we can see, we now only trigger an event called “sendTweet”. It’s up to the mail and/or SMS service to listen to
this event to send an email/SMS.

You can also notice that this class implements the EventManagerAwareInterface interface. When we create an object
that implements this interface through the service manager, Zend Framework 2 will automatically inject a new event
manager for us by calling the setEventManager method.

Add listeners

Most of the time, listeners are added in the Module.php file, more precisely in the onBootstrap method. This is the
easiest and recommended way. Intuitively, this is what most people do when they are first introduced to the event manager:

This code is pretty simple: we retrieve the application’s event manager, and add a callback (through a closure)
that is called when the event sendTweet is triggered.

Unfortunately… this does not work. I know I know, you may think this is not intuitive, but there are reasons for
that. Let’s introduce the shared event manager !

The shared event manager

Let’s go back to the TweetService. Earlier, I said that when a class implements the EventManagerAwareInterface,
ZF 2 automatically inject a new event manager. I need to emphasize on the word new!

As a consequence, when we trigger the event sendTweet in the TweetService class, because the event manager is
different (once again, it is a new event manager), the TweetService’s event manager has absolutely no knowledge
about listeners that could have been added to other event managers. And this is absolutely what we were doing in the
previous code snippet, as we were attaching a listener to the application’s event manager (which is different from the TwitterService’s one).

You may ask yourself why ZF 2 does not inject the same event manager everywhere. This way, the problem would be
solved. But if you think about it more, this could rise even more problems. For instance, let’s imagine an event
called send. Multiple objects may trigger an event called send, but for completely different purposes
(send could be an event’s name used to send an email, a SMS, a HTTP request or whatever!). This would mean that
listeners could receive event for things they would not be interested at all.

That’s why each object has its own event manager, with its own events.

To solve our previous example, we need to use a so-called shared event manager. A shared event manager is a
manage which is unique across the application, and that is injected into each event manager (yeah I know, it’s
not easy to grasp !). Let’s modify our Module.php code in order to attach the event into the shared event manager instead:

First, we retrieve the shared event manager from the application’s event manager. Then, we attach a listener. The
subtlety is the first parameter whose value is here Tweet\Service\TweetService. Indeed, currently, without this
parameter, the event manager of our TweetService has no way to “get” the listeners of the event it triggers.

Now, everything is linked properly. We are adding here a new identifier whose value is get_called_class()
(in this case, it is equals to Tweet\Service\TweetService). When this service will trigger the event sendTweet,
here is what will happen internally:

The TweetService’s event manager will check if objects are listening to sendTweet, which is not the case
here because the listeners have been added to the shared event manager (remember the Module.php code!).

The event manager will then retrieve the shared event manager (remember it is unique !). Then, for each of its
identifiers (in our case, it only has one, which is get_called_class), it will check if an event was added to the
event sendTweet with the given identifier. In other words, it will check if there is a registered listener for the
event sendTweet with the identifier Tweet\Service\TweetService (=== get_called_class()). This is exactly what we did
in the onBootstrap method!

Finally, for each listener, the callback will be executed (in our example, we simply called a var_dump($e)).

This identifier mechanism is really powerful. For instance, let’s imagine we want to trigger the event sendTweet
in several services. We could add another, more generic, identifier:

namespace Tweet;
use Zend\Mvc\MvcEvent;
class Module
{
public function onBootstrap(MvcEvent $event)
{
$eventManager = $event->getApplication()->getEventManager();
$sharedEventManager = $eventManager->getSharedManager();
// This listener will be called ONLY if the sendTweet event is triggered
// by an event manager that has the Tweet\Service\TweetService identifier !
$sharedEventManager->attach('Tweet\Service\TweetService', 'sendTweet', function($e) {
var_dump($e);
}, 100);
// This listener will be called for all events sendTweet from all event
// manager that has the identifier Application\Service\ServiceInterface,
// so potentially a lot
$sharedEventManager->attach('Application\Service\ServiceInterface', 'sendTweet', function($e) {
var_dump($e);
}, 100);
}
}

The special case of MVC events

I said earlier that we should use the shared event manager. But there is one specific case: the event manager we
retrieve from the onBootstrap method is the MVC event manager. This means that this event manager knows the
events triggered by the framework. This means that if you want to add listeners to the events of the Zend\Mvc\MvcEvent
class, you can do it without using the shared event manager:

Let’s clean that…

In the previous example, we used a closure to define what the listeners must do. This is quick and easy, but if
you have a lot of listeners, this can quickly be a mess. Furthermore, if you want to attach the same listener
to multiple events, it will irremediably lead to a code duplication.

Hopefully, we can do it better by creating new classes that implement Zend\EventManager\ListenerAggregateInterface
interface (starting from ZF 2.2, you can instead extend the abstract class Zend\EventManager\AbstractListenerAggregate).
This interface asks you to write two methods: attach and detach. Here is the class that adds a listener to the sendTweet event:

Conclusion

Events are extremely powerful and are often a nice solution to some problems. However, please don’t abuse them: it
makes the code harder to read and complicate the workflow (you need to know all the listeners that are registered
for all the events, good luck if the code is not documented!).

From my personal experience, in your application-specific code, you’d rather hardcode the actions in your services,
while you’d send events in generic modules.