An introduction to Zend\EventManager

23rd April 2012

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 Zend\EventManager\EventManager;
use Zend\EventManager\Event;

class PhotoMapper
{
    public $events;

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

        return $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 ZfcBase\EventManager\EventProvider 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:


    public function findById($id)
    {
        $results $this->events()->trigger(__FUNCTION__ '.pre'$this, 
            array('id' => $id),
            function ($result) {
                return ($result instanceof Photo) ? true false;
            }
        );

        if ($results->stopped()) {
            // we ended early. Result returned from last listener was an entity
            $photo $results->last();
            return $photo;
        }        

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

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

        // save $photo to cache here

        return $photo;
    }

We have added a callback to our trigger() call that returns true if the result returned by a listener is an instance of Photo. This means that we can attach a listener that tries to read a cache for the id. If it finds one, then it returns the cached object. The trigger callback notices and returns true and the ResponseCollection's stopped flag is set to true.

We then test for stopped() and if it has been set, $results->last() contains the Photo entity that we need.

SharedEventManager

It's common that you may want to set up listeners before the objects with the event manager you need has been instantiated. Good examples are listeners that log or cache. ZF2 comes with the concept of the SharedEventManager. Essentially you attach listeners to the SharedEventManager and then when the particular event manager's trigger() is called, it will also call all the listeners attached to the shared event collection.

There are two ways to access the SharedEventManager: DIC injection into the object where you need it or via a static class called (rather unimaginatively) StaticEventManager. The exact details of how the injection will work are up in the air at the moment as it's expected that some additional defaults will be provided to make it easier in beta 4 and the use of the StaticEventManager will become deprecated.

In either case, the way to attach a listener to the SharedEventManager is the same:


// $events is a Zend\EventManager\SharedEventManager instance either injected
// or acquired using $events = StaticEventManager::getInstance();

$events->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 Gallery\Model\PhotoMapper. 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.

Access view variables in another view model

3rd April 2012

Unlike Zend Framework 1, the view layer in Zend Framework 2 separates the variables assigned to each view model. This means that when you are in the layout view script, you don't automatically have access to variables that were assigned the the action's view model and vice versa.

Accessing action variables in the layout

Consider this controller code:


class IndexController extends ActionController
{
    public function indexAction()
    {
        return array('myvar' => 'test');
    }
}

If you are in the layout.phtml, then to retrieve this value you do:

layout.phtml:

<?php
$children $this->viewModel()->getCurrent()->getChildren();
$child $children[0];
?>
<!-- some HTML -->
<?php echo $this->escape($child->myvar);?>

If you really want to make sure you collect the correct child view model, then you could iterate over $children and look for the child that has the correct captureTo name set. For the action's view model, this defaults to content:

layout.phtml:

<?php
$children $this->viewModel()->getCurrent()->getChildren();
foreach($children as $child) {
    if ($child->captureTo() == 'content') {
        break;
    }
}
?>
<!-- some HTML -->
<?php echo $this->escape($child->myvar);?>

Accessing layout variables in the action view

If you have assigned a variable to the layout's view model in, say, an event listener within Module.php:

Module.php:


    public function onBootstrap($e)
    {
        $application $e->getParam('application');
        $viewModel $application->getMvcEvent()->getViewModel();
        $viewModel->some_config_var '12345';
    }

This is how you access some_config_var in the action view:

view/index/index.html:

<?php echo $this->escape($this->layout()->some_config_var); ?>

Another, more long winded way is to use the getRoot() method on the viewModel view helper:

view/index/index.html:

<?php
$layoutViewModel $this->viewModel()->getRoot(); 
?>
<!-- Some HTML -->
<?php echo $this->escape($layoutViewModel->some_config_var); ?>

Setting configuration variables into the view

It therefore follows that if you need to set a variable that could be accessed from any view script, it's easiest to set it into the layout's view model and then access it via the layout() view script. This is handy for view layer config variables that you want to store in your config files, such as the Google search API key.

Application/config/module.config.php:

<?php
return array(
    'layout' => array(
        'google_search_api_key' => '1234567890',
    ),

Application/Module.php:


    public function onBootstrap($e)
    {
        $application $e->getParam('application');
        $config $e->getParam('config');

        $viewModel $application->getMvcEvent()->getViewModel();
        $viewModel->config $config->layout;
    }

view/search/index.html:

<?php echo $this->layout()->config->google_search_api_key?>

Returning JSON using the Accept header in ZF2

30th March 2012

Following yesterday's article on returning JSON from a ZF2 controller action, Lukas suggested that I should also demonstrate how to use the Accept header to get JSON. So this is how you do it!

Set up the JsonStrategy

We set up the JsonStrategy as we did in returning JSON from a ZF2 controller action.

Return a ViewModel from the controller

As we're letting the JsonStrategy intercede for us, we don't need to do anything special in our controller at all. In this case, we simply return a normal ViewModel for use by either the JsonRenderer or PhpRenderer as required:

module/Application/src/Application/Controller/IndexController.php:

<?php

namespace ApplicationController;

use ZendMvcControllerActionController,
    ZendViewModelViewModel;

class IndexController extends ActionController
{
    public function anotherAction()
    {
        $matches[] = array('distance' => 10'playground' => array('a'=>1));
        $matches[] = array('distance' => 20'playground' => array('a'=>2));
        $matches[] = array('distance' => 30'playground' => array('a'=>3));

        $result = new ViewModel(array(
            'success'=>true,
            'results' => $matches,
        ));
        return $result;
    }
}

with our HTML view script:

module/Application/view/index/another.phtml:

<?php if ($success): ?>

<h2>Results</h2>

<ul>
<?php foreach ($results as $row): ?>
    <li>Distance: <?php echo $this->escape($row['distance']);?>m</li>
<?php endforeach; ?>
</ul>

<?php endif; ?>

So if you set up a route and browse to it, you'll see a nicely rendered page.

Retrieving the data as JSON

To retrieve the data via JSON, we need a client where we can set the Accept header. We'll use curl for this test. When doing anything with APIs and testing, we head over to LornaJane's blog for the Curl Cheat Sheet and use this command line:

curl -H "Accept: application/json" http://zf2test.dev/json/another

and you should see the output of:

{
  "content":{
    "success":true,
    "results": [
      {"distance":10,"playground":{"a":1}},
      {"distance":20,"playground":{"a":2}},
      {"distance":30,"playground":{"a":3}}
    ]
  }
}

(Formatted for readability - you get the result back on a single line from curl.)

This way you can use the same controllers for your HTML views and for returning JSON to those clients that can use it.

Returning JSON from a ZF2 controller action

29th March 2012

The new view layer in Zend Framework 2 can be set up to return JSON rather than rendered HTML relatively easily. There are two steps to this:

Set up the JsonStrategy

Firstly we need to set up the view's JsonStrategy to check to a situation when returning JSON is required and then to render out JSON for us. The JsonStrategy will cause the JsonRenderer to be run in two situations:

  1. The view model returned by the controller action is a JsonModel
  2. The HTTP Accept header sent in the Request include "application/json"

The enable the JsonStrategy, we simply attach it to the view's event manager with a reasonably high priority. This can be done in our Application's Module class. Firstly we create an onBootstrap() callback on the bootstrap event and then we implement onBootstrap() to attaché the JsonStrategy:

module/Application/Module.php:


class Module implements AutoloaderProvider
{
    public function init(Manager $moduleManager)
    {
        $events StaticEventManager::getInstance();
        $events->attach('bootstrap''bootstrap',
            array($this'onBootstrap'));
    }

    public function onBootstrap(Event $e)
    {
        $application  $e->getParam('application'); 
        /* @var $application \Zend\Mvc\Application */
        $locator      $application->getLocator();
        $view         $locator->get('Zend\View\View');
        $jsonStrategy $locator->get('Zend\View\Strategy\JsonStrategy');
        $view->events()->attach($jsonStrategy100);        

    }

    // more methods such as getConfig() and getAutoloaderConfig()
}

As you can see, in init() we grab the StaticEventManager to attach our onBootstrap() method to the bootstrap event. Then, within onBootstrap(), we grab the view and the JsonStrategy from the locator (via application) and attach the JsonStrategy to the view's events() event manager.

Return a JsonModel from the controller action

To send JSON to the client when the Accept header isn't application/json, we use a JsonModel in a controller action like this:

module/Application/src/Application/Controller/IndexController.php:


namespace Application\Controller;

use Zend\Mvc\Controller\ActionController,
    Zend\View\Model\ViewModel,
    Zend\View\Model\JsonModel;

class IndexController extends ActionController
{
    public function indexAction()
    {
        $result = new JsonModel(array(
        'some_parameter' => 'some value',
            'success'=>true,
        ));

        return $result;
    }
}

The output will now be JSON. Obviously, if you're sending JSON back based on the Accept header, then you can return a normal ViewModel.

A list of ZF2 events

16th March 2012

Both the Module Manager and the MVC system use the Event Manger extensively in order to provide "hook points" for you to add your own code into the application flow. This is a list of the events triggered by each class during a standard request with the Skeleton Application:

Module Manager

  • Zend\Module\Manager: loadModules.pre
  • For every module:
    • Zend\Module\Manager: loadModule.resolve
    • Zend\Module\Manager: loadModule
  • Zend\Module\Manager: loadModules.post

Bootstrap

  • Zend\Mvc\Bootstrap: bootstrap

Application

Successful:

  • Zend\Mvc\Application: route
  • Zend\Mvc\Application: dispatch
    • Zend\Mvc\Controller\ActionController: dispatch (if controller extends this class)
  • Zend\Mvc\Application: render
  • Zend\View\View: renderer
  • Zend\View\View: response
  • Zend\Mvc\Application: finish
  • On an error in route or dispatch:

  • Zend\Mvc\Application: dispatch.error
  • Zend\Mvc\Application: render
  • Zend\View\View: renderer
  • Zend\View\View: response
  • Zend\Mvc\Application: finish

Note that routing and dispatching is also implemented using these registered events, so you can implement "pre" and "post" hooks by changing the priority of the listener that you register.