Sunday, August 25, 2013

Zend Framework 2 event manager

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.
namespace Tweet\Service;

class TweetService
{
    /**
     * @param string $content
     */
    public function sendTweet($content)
    {
        // Send a tweet through Twitter API...
    }
}
But this crazy marketing guy said us that it could be fun to send an email to the tweet’s author too:
namespace Tweet\Service;

class TweetService
{
    /**
     * @param string $content
     */
    public function sendTweet($content)
    {
        // Send a tweet through Twitter's API...

        // Send a mail
        $mailService->send(...);
    }
}
But it’s not enough for him, he also wants SMS:
namespace Tweet\Service;

class TweetService
{
    /**
     * @param string $content
     */
    public function sendTweet($content)
    {
        // Send a tweet through Twitter's API...

        // Send an e-mail...
        $mailService->send(...);

        // Send a SMS...
        $smsService->send(...);
    }
}
Some problems quickly arise:
  • 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:
namespace Tweet\Service;

use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerInterface;

class TweetService implements EventManagerAwareInterface
{
    /**
     * @var EventManagerInterface
     */
    protected $eventManager;

    /**
     * @param string $content
     */
    public function sendTweet($content)
    {
        // Send the tweet with Twitter's API...

        // Trigger an event
        $this->getEventManager()->trigger('sendTweet', null, array('content' => $content));
    }

    /**
     * @param  EventManagerInterface $eventManager
     * @return void
     */
    public function setEventManager(EventManagerInterface $eventManager)
    {
        $this->eventManager = $eventManager;
    }

    /**
     * @return EventManagerInterface
     */
    public function getEventManager()
    {
        return $this->eventManager;
    }
}
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:
namespace Tweet;

use Zend\Mvc\MvcEvent;

class Module
{
    public function onBootstrap(MvcEvent $event)
    {
        $eventManager = $event->getApplication()->getEventManager();
        $eventManager->attach('sendTweet', function($e) {
            var_dump($e);
        }, 100);
    }
}
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:
namespace Tweet;

use Zend\Mvc\MvcEvent;

class Module
{
    public function onBootstrap(MvcEvent $event)
    {
        $eventManager       = $event->getApplication()->getEventManager();
        $sharedEventManager = $eventManager->getSharedManager();

        $sharedEventManager->attach('Tweet\Service\TweetService', 'sendTweet', function($e) {
            var_dump($e);
        }, 100);
    }
}
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.
Thus, we need to slightly modify our setEventManager method in TweetService:
public function setEventManager(EventManagerInterface $eventManager)
{
    $eventManager->addIdentifiers(array(
        get_called_class()
    ));

    $this->eventManager = $eventManager;
}
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:
  1. 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!).
  2. 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!
  3. 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:
public function setEventManager(EventManagerInterface $eventManager)
{
    $eventManager->addIdentifiers(array(
        'Application\Service\ServiceInterface',
        get_called_class()
    ));

    $this->eventManager = $eventManager;
}
Now, update the Module.php class:
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:
namespace Tweet;

use Zend\Mvc\MvcEvent;

class Module
{
    public function onBootstrap(MvcEvent $event)
    {
        $eventManager = $event->getApplication()->getEventManager();
        $eventManager->attach(MvcEvent::EVENT_ROUTE, function($e) {
            var_dump($e);
        }, 100);
    }
}

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:
namespace Tweet\Listener;

use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;

class SendListener implements ListenerAggregateInterface
{
    /**
     * @var \Zend\Stdlib\CallbackHandler[]
     */
    protected $listeners = array();

    /**
     * {@inheritDoc}
     */
    public function attach(EventManagerInterface $events)
    {
        $sharedEvents      = $events->getSharedManager();
        $this->listeners[] = $sharedEvents->attach('Tweet\Service\TweetService', 'sendTweet', array($this, 'onSendTweet'), 100);
    }

    public function detach(EventManagerInterface $events)
    {
        foreach ($this->listeners as $index => $listener) {
            if ($events->detach($listener)) {
                unset($this->listeners[$index]);
            }
        }
    }

    public function onSendTweet($e)
    {
        var_dump($e);
    }
}
This way, the onBootstrap method is simplified, and allow to move logic to specific classes instead of polluting the Module.php class:
public function onBootstrap(EventInterface $e)
{
    $eventManager = $e->getTarget()->getEventManager();
    $eventManager->attach(new SendListener());
}

No comments:

Post a Comment