ZF2 – Event Manager – Zend\EventManager

The ZF2 Event Manager is a component designed to implement simple subject/observer patterns, aspect-oriented designs and implementing event-driven architectures.

To start with, it is worth taking a look at the onBootstrap() method of the ZF2 skeleton application within the ‘Application’ module.

// Application/Module.php
<?php

namespace Application;

use Zend\Mvc\ModuleRouteListener;
use Zend\Mvc\MvcEvent;

class Module
{
    public function onBootstrap(MvcEvent $e)
    {
        $eventManager        = $e->getApplication()->getEventManager();
        $moduleRouteListener = new ModuleRouteListener();
        $moduleRouteListener->attach($eventManager);
    }
    ....
}

The first line, $e->getApplication()->getEventManager() retrieves the event manager instance. You can then add listeners to the event manager using the attach() method, and trigger events using the trigger() method.

For example, replacing the onBootstrap() method with the following:

public function onBootstrap(MvcEvent $e)
{
    $eventManager = $e->getApplication()->getEventManager();

    // Attach a listener
    $eventManager->attach('do', function ($ev) {
        $event = $ev->getName();
        $params = $ev->getParams();
        printf(
            'Handled event "%s", with parameters %s',
            $event,
            json_encode($params)
        );
    });

    // Trigger an event
    $params = array('foo' => 'bar', 'baz' => 'bat');
    $eventManager->trigger('do', null, $params);
    exit(); // This to prevent further execution of the application for now.

}

Results in ‘Handled event “do”, with parameters {“foo”:”bar”,”baz”:”bat”}’ being printed out.

Notice how the trigger() line is added after the attach() line. This is because if trigger() is executed first, there wouldn’t be any listeners currently defined and nothing would get printed out!

The trigger() method can take up to four parameters:

  • $event, which can either be a string or an EventInterface instance.
  • $target which must be an object or a string. This gives event listeners access to the calling object
  • $params which must be an array or an object.
  • $callback, a callback function, which can be used to execute additional functionality based on the result of the event listener.

A simple and impractical example of this can be seen below.

$eventManager->attach('do', function ($ev) {
    $result = "We're ok";
    return $result;
});

$params = array('foo' => 'bar', 'baz' => 'bat');
$eventManager->trigger('do', null, $params, function ($result) {
    if ($result === "We're ok") {
        // Do something additional
    }
});

The attach() method can take up to three parameters:

  • $event which is the named event to listen to.
  • $callback, a callback function.
  • $priority which is the priority order of the event listeners.

Higher the priority values execution earlier. For example, if you have two listeners attached to an event.

public function onBootstrap(MvcEvent $e)
{
    $eventManager = $e->getApplication()->getEventManager();

    $eventManager->attach('do', function ($ev) {
        echo "This comes second!<br/>";
    }, 100);

    $eventManager->attach('do', function ($ev) {
        echo "This comes first!<br/>";
    }, 200);

    $params = array('foo' => 'bar', 'baz' => 'bat');
    $eventManager->trigger('do', null, $params);
    exit();
}

This outputs:

This comes first!
This comes second!

Short Circuiting

One common use case for events is to trigger listeners until either one indicates no further processing should be done, or until a return value meets specific criteria. You can therefore use the stopPropagation() method within the callback of the attach() method.

public function onBootstrap(MvcEvent $e)
{
    $eventManager = $e->getApplication()->getEventManager();

    $eventManager->attach('do', function ($ev) {
        echo "This comes second!<br/>";
    }, 100);

    $eventManager->attach('do', function ($ev) {
        echo "This comes first and stop everything else!<br/>";
        $ev->stopPropagation();
    }, 200);

    $params = array('foo' => 'bar', 'baz' => 'bat');
    $eventManager->trigger('do', null, $params);
    exit();
}

This outputs:

This comes first and stop everything else!

Alternatively, a check could be made from the method triggering the event.

class Foo implements DispatchableInterface
{
    // assume composed event manager

    public function dispatch(Request $request, Response $response = null)
    {
        $argv = compact('request', 'response');
        $results = $this->getEventManager()->triggerUntil(__FUNCTION__, $this, $argv, function($v) {
            return ($v instanceof Response);
        });

        // Test if execution was halted, and return last result:
        if ($results->stopped()) {
            return $results->last();
        }

        // continue...
    }
}

In the above example, both trigger() and triggerUntil() will return a ResponseCollection instance; call its stopped() method to test if execution was stopped, and last() method to retrieve the return value from the last executed listener.

Wildcard Listeners

What if you want to attach a listener to multiple events or targets? The answer is to supply an array of events or targets, or a wildcard, *.

// Listen to multiple named events:
$events->attach(
    array('foo', 'bar', 'baz'), // events
    $listener
);

// Listen to all events via wildcard:
$events->attach(
    '*', // all events
    $listener
);

Aggregate Listeners

Another approach to listening to multiple events is via the use of listener aggregates, represented by Zend\EventManager\ListenerAggregateInterface.

When writing a class that implements this interface, two methods must be defined.

  • attach(EventManagerInterface $events).
  • detach(EventManagerInterface $events).

Basically, you pass an EventManager instance to one and/or the other, and then it’s up to the implementing class to determine what to do.

Example:

use Zend\EventManager\EventInterface;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\ListenerAggregateInterface;
use Zend\Log\Logger;

class LogEvents implements ListenerAggregateInterface
{
    protected $listeners = array();

    public function attach(EventManagerInterface $events)
    {
        $this->listeners[] = $events->attach('do', array($this, 'logSomething'));
        $this->listeners[] = $events->attach('doSomething', array($this, 'logSomething'));
    }

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

    public function logSomething(EventInterface $e)
    {
        echo "In the log() method";
    }
}

You can attach the listener class using either the attach() or attachAggregate(). For example in an IndexController (and assuming you have registered the class and the listener in the module’s configuration):

$use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $eventManager = $this->getEventManager();
        $serviceManager = $this->getServiceLocator();
        $logEvents = $serviceManager->get('logEvents');

        $eventManager->attachAggregate($logEvents);

        $eventManager->trigger('doSomething', $this); // Outputs: In the log() method
        exit(); // This to prevent further execution of the application for now.
    }
}

Equally, you can detach an aggregate listener by using the detach() method.

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $eventManager = $this->getEventManager();
        $serviceManager = $this->getServiceLocator();
        $logEvents = $serviceManager->get('logEvents');

        $eventManager->attachAggregate($logEvents);
        $logEvents->detach($eventManager);

        $eventManager->trigger('doSomething', $this); // No output
        exit();
    }
}

It is usually good practice to define your own EventManager class instead of using the event manager from the onBootstrap() method. The event manager instance is really specifically for MVC events only. When adding you own event manager, you should implement the EventManageAwareInterface interface.

The EventManagerAwareInterface contains one method, setEventManager(), which will need to defined in your class.

A simple event manager class has been defined below with a ‘Debug’ module.

// Debug/src/Debug/Service/Logger.php
<?php
namespace Debug\Service;

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

class Logger implements EventManagerAwareInterface
{
    protected $events;

    public function setEventManager(EventManagerInterface $events)
    {
        $events->setIdentifiers(array(
            __CLASS__,
            get_called_class(),
        ));
        $this->events = $events;
        return $this;
    }

    public function getEventManager()
    {
        if (null === $this->events) {
            $this->setEventManager(new EventManager());
        }
        return $this->events;
    }
}

The setIdentifiers() method allows passing a string, or an array of strings, defining the name or names of the context or targets the given instance will be interested in.

In the above example, the identifiers are both the same: Debug\Service\Logger.

Events can then be added and triggered within the class, such as within the logMessage() method in the below example.

// Debug/src/Debug/Service/Logger.php
<?php
namespace Debug\Service;

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

class Logger implements EventManagerAwareInterface
{
    protected $events;

    public function setEventManager(EventManagerInterface $events)
    {
        $events->setIdentifiers(array(
            __CLASS__,
            get_called_class(),
        ));
        $this->events = $events;
        return $this;
    }

    public function getEventManager()
    {
        if (null === $this->events) {
            $this->setEventManager(new EventManager());
        }
        return $this->events;
    }

    public function logMessage($content)
    {
        $this->getEventManager()->trigger(__FUNCTION__, null, array('content' => $content));
    }
}

Then within the IndexController.php file, we should be able to attach a listener to the ‘logMessage’ event and see our output. In the example below, we should see a string starting with ‘Logged a message:’.

namespace Debug\Controller;

use Zend\Mvc\Controller\AbstractActionController;

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $eventManager = $this->getEventManager();
        $serviceManager = $this->getServiceLocator();

        $eventManager->attach('logMessage', function ($e) {
            printf("Logged a message: %s",
                $e->getParam('content')
            );
        });

        $logger = $serviceManager->get('logger');
        $logger->logMessage("Some message!");

        exit();

    }
}

But it doesn’t work, right? Run the following two lines within the indexAction() method.

$eventManager = $this->getEventManager();
print_r($eventManager->getIdentifiers());

The following output should appear.

Array ( [0] => Zend\Mvc\Controller\AbstractController [1] => Debug\Controller\IndexController [2] => Debug [3] => Zend\Stdlib\DispatchableInterface [4] => Zend\EventManager\EventManagerAwareInterface [5] => Zend\EventManager\EventsCapableInterface [6] => Zend\Mvc\InjectApplicationEventInterface [7] => Zend\ServiceManager\ServiceLocatorAwareInterface [8] => Zend\Mvc\Controller\AbstractActionController )

Interestingly, it doesn’t contain our identifier, Debug\Service\Logger. This is where the Shared Event Manager comes into play. A shared event manager is a manager which is unique across the application, and that is injected into each event manager. More information on this can be found in the Shared Event Manager article.

Note: This article is based on ZF version 2.4.