Pragmatism in the real world

An introduction to ZendEventManager

Zend Framework 2’s EventManager is a key component of the framework which is used for the core MVC system. The EventManager allows a class to publish events that other objects can listen for and then act when the event occurs. The convention within Zend Framework 2 is that any class that triggers events composing its own EventManager.

Terminology

For the purposes of this article, we will use these definitions:

  • An EventManager is an object that holds a collection of listeners for one or more named events, and which triggers events.
  • An event is an action that is triggered by an EventManager.
  • A listener is a callback that can react to an event.

When the EventManager‘s trigger() method is called, all the listeners attached to the event are called in turn and are passed an Event object which contains a target and additional parameters as required.

Triggering events

A typical use-case for EventManager is to trigger events within a mapper class:

use ZendEventManagerEventManager;
use ZendEventManagerEvent;

class PhotoMapper
{
    public $events;

    public function events() 
    {
        if (!$this->events) {
            $this->events = new EventManager(__CLASS__); 
        }

        return $this->events;
    }

    public function findById($id)
    {
        $this->events()->trigger(__FUNCTION__ . '.pre', $this,
            array('id' => $id));

        // retrieve from database and create a $photo entity object

        $this->events()->trigger(__FUNCTION__ . '.post', $this, 
            array('photo' => $photo));

        return $photo;
    }

    //  class continues...

Firstly, note that, by convention, the EventManager instance is called $events and we use a method called events() and that will instantiate the EventManager for us when needed. This is a very common code pattern in ZF2 classes; so common in fact that the ZfcBase module contains a class called ZfcBaseEventManagerEventProvider that you can extend from to provide you with this functionality.

Within our mapper’s findById()method, we trigger two events: before and after the actual work is done. When triggering an event, we supply the event’s name, the target class and an array of data that the listeners may be interested in.

By convention, we use the name of the method as our event’s name. This makes it predictable for users of the class. Also, by convention, if there two events, then we append ‘.pre’ and ‘.post’ to the event name. the target class is usually the class that triggered the event, and so we pass in $this. Finally, we can provide some additional data to the listeners. In this particular method, we supply the id before we retrieve (as that’s all we have!) After retrieval, we have the entity object itself, so we supply that to the listener callbacks.

Listening for an event

A listener is just any PHP callback function which takes a single argument, $event, which is an instance of an Event. The listener is attached using event manager’s attach() method. For example, we could attach a logger to our mapper’s findById.pre event like this:

$photoMapper->events()->attach('findById.pre', function(Event $event) {
    $message = "Trying to retrieve photo: " . $event->getParam('id');
    MyLogger::log($message);
});

The $event parameter that is passed to the listener method has three user methods:

  • getName() – Useful when the same listener is attached to multiple events
  • getTarget() – Usually the class that trigger the event
  • getParams() – Retrieve the parameters sent when the event was triggered.

Priority
When attaching a listener to an event, you can specify the priority in the third parameter. The larger the number, the earlier the listener is called. i.e. for a listener that you want to be executed first, set the priority to say 1000. For a listener that you want to be executed last, set the priority the -1000. The default priority is 1 and listeners with the same priority are executed in order or priority.

Short circuiting

The return value from trigger() is a ResponseCollection which is a collection of all the returned results from every listener that has been called in reverse order. i.e. the result from the last listener is first in the collection.

As this result is passed back to trigger, we can use it to stop processing of the listeners if something interesting happens. This is known as short-circuiting and we do this by attaching a callback as the last parameter to trigger().

A good example of this is a caching solution on our findById() method. We change it so that it looks like this:

// $sharedEvents is a ZendEventManagerSharedEventManager instance either injected
// or acquired using $sharedEvents = StaticEventManager::getInstance(); or from
// $sharedEvents = $events->getSharedManager();

$sharedEvents->attach('Gallery\Model\PhotoMapper', 'findById.pre', function(Event $event) {
    $message = "Trying to retrieve photo: " . $event->getParam('id');
    MyLogger::log($message);
});

Note that the only change to the attach call is the addition of a new first parameter which is the name of the class where the event manager is to be found. In this case, it’s GalleryModelPhotoMapper. The other three parameters are the same: event name, callback and priority.

In terms of execution of listeners with the same priority, those attached directly are executed before those attached via the SharedEventManager.

Summary

This article covers how to use the EventManager in your application. The biggest advantage to using EventManager in your application is that you can decouple classes that really shouldn’t be coupled together which makes your code easier to write and maintain. In fact, this is so useful, that ZF2’s MVC system makes heavy use of EventManager; The module manager, routing, dispatching and the view layer are all implemented using listeners attached to events.