Front Controller Plugins in Zend Framework

Like Action Helpers, which I’ve discussed in a previous
article, Front Controller Plugins in Zend Framework are often
considered an esoteric, advanced subject. They are, however, remarkably
simple to implement, and provide an easy way to extend the functionality and
behavior of your entire web application.

What is a Front Controller Plugin?

In Zend Framework, plugins are used to listen for certain events in the
front controller. Events in the front controller bookend each of the major
actions that occur: routing, the dispatch loop, and dispatching an
individual action. The actual hooks defined are:

  • routeStartup(): prior to routing the request
  • routeShutdown(): after routing the request
  • dispatchLoopStartup(): prior to entering the dispatch loop
  • preDispatch(): prior to dispatching an individual action
  • postDispatch(): after dispatching an individual action
  • dispatchLoopShutdown(): after completing the dispatch loop

As you start thinking about the hooks listed above, a few questions may come
to mind, such as, “Why is there both a routeShutdown()
and dispatchLoopStartup() hook, when nothing occurs
between them?” The main reason is because of semantics: you may want to do
something to alter the results of routing after routing, or you may want to
modify the dispatcher prior to entering the dispatch loop, and these are
semantically different. Having different hooks helps keep these distinctions
clear.

Another question I’ve fielded is, “Why are there
dispatchLoopStartup/Shutdown() hooks and
pre/postDispatch() hooks?” In ZF, we actually have a dispatch
loop — which allows you to use the router to create multiple
requests for dispatch, or use logic in your controllers to request
additional actions. Thus, we have hooks on either end of the loop
(dispatchLoopStartup() and
dispatchLoopShutdown()), as well as within the loop
bookending the actual dispatch (preDispatch() and
postDispatch()).

An actual plugin is simply a class that extends
Zend_Controller_Plugin_Abstract. That class defines empty
methods for each of these hooks. A concrete plugin then simply overrides any
of these methods necessary for implementing its functionality. In all cases
except for dispatchLoopShutdown(), the hook methods take a
single $request argument of type
Zend_Controller_Request_Abstract (the base request class within
the ZF MVC):

public function preDispatch(Zend_Controller_Request_Abstract $request)
{
}

I will often speak of “early-running plugins” or “late-running plugins”. The
former refers to routeStartup(), routeShutdown(),
and dispatchLoopStartup() — hooks that run before the dispatch
loop begins, and thus would have application-wide effects. Late-running
plugins refer to postDispatch() and
dispatchLoopShutdown() — more typically the latter — plugins
that trigger after actions have been dispatched.

Registering Plugins with the Front Controller

Plugins themselves need to be instantiated and registered with the front
controller, which can be done with
Zend_Controller_Front::registerPlugin():

$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new FooPlugin());

This can be done at any time during the request. However, only hooks that
are triggered after the plugin is registered will be called.

You can optionally pass a stack index when registering plugins. This allows
you to specify an order in which plugins are triggered. If no stack index is
provided, then plugins are triggered in the order in which they are
registered. When a stack index is provided, then that index will be honored
for that plugin.

You can specify the stack index as the second parameter when registering the
plugin; the index should be numeric, and a lower number will indicate
earlier execution:

$front->registerPlugin(new FooPlugin(), 1);   // will trigger early
$front->registerPlugin(new FooPlugin(), 100); // will trigger late

Retrieving Plugins from the Front Controller

Occasionally, you may have need to gather state information from a plugin,
or configure it, after it has been registered with the front
controller. You can retrieve a plugin by passing the plugin’s class name to
Zend_Controller_Front::getPlugin():

$front     = Zend_Controller_Front::getInstance();
$fooPlugin = $front->getPlugin('FooPlugin');

How Plugins are used in Zend Framework

Okay, now that you know what a plugin is, and how to register one with the
front controller, the burning question is: what uses exist for plugins? To
answer this question, let’s first look at how plugins are used in existing
ZF components.

Zend_Layout

Zend_Layout can optionally be used with the MVC components, and
when it is, it registers a plugin with the front controller. This plugin
listens to the postDispatch() hook, and is registered with a
late stack index to ensure it runs after all other plugins have
executed, as well as to ensure no other actions exist to loop over.

The Layout plugin allows us to implement a Two Step View pattern in Zend
Framework; it captures the content in the response, and then passes it to
the layout object to process so that the content can be injected in the
layout view script.

Error Handling

As another example, the ErrorHandler plugin listens to
postDispatch(), too, also with a late stack index. It checks to
see if an application exception has been registered with the response, and,
if so, will request another action for the dispatch loop to iterate over —
the error action in the error controller, so as to report the exception.

Potential Uses for Plugins in Your Applications

Now that you’ve seen some concrete examples of how plugins are already used,
what potential uses can you find for them in your own applications? Some
examples that are often given include:

  • Application initialization
  • Caching
  • Routing initialization and customization
  • Authentication and ACLs
  • Output filter of final XHTMl

Example: Application Initialization Plugin

Let’s consider the first idea, application initialization. In most examples
of Zend Framework MVC apps, we show a bootstrap file that contains the
entire application initialization — loading configuration, loading all
plugins, initializing the view and database, etc. This works well, but it
can lead to a somewhat sloppy file, and also leaves the potential to leak
important information about your system should the file ever be displayed
without processing (ala the Facebook fiasco last year).

We can solve this by pushing most initialization into an early-running
plugin — specifically, a routeStartup() plugin. Here’s an example:

/**
 * Application initialization plugin
 * 
 * @uses Zend_Controller_Plugin_Abstract
 */
class My_Plugin_Initialization extends Zend_Controller_Plugin_Abstract
{
    /**
     * Constructor
     * 
     * @param  string $env Execution environment
     * @return void
     */
    public function __construct($env)
    {
        $this->setEnv($env);
    }

    /**
     * Route startup hook
     * 
     * @param  Zend_Controller_Request_Abstract $request
     * @return void
     */
    public function routeStartup(Zend_Controller_Request_Abstract $request)
    {
        $this->loadConfig()
             ->initView()
             ->initDb()
             ->setRoutes()
             ->setPlugins()
             ->setActionHelpers()
             ->setControllerDirectory();
    }

    // ...
}

Your bootstrap would then look like this:

require_once 'Zend/Loader.php';
Zend_Loader::registerAutoload();
$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new My_Plugin_Initialization('production'));
$front->dispatch();

I won’t go into the various methods called here, as they should be fairly
self-explanatory. The main thing to get from this, however, is that we’ve
now moved the complexity of the bootstrap into a class, and also provided a
way to group common tasks — helping make our application setup more
maintainable. We do this by simply leveraging the infrastructure provided by
the plugin system.

Example: Caching Plugin

As another example, consider a simple caching plugin. Oftentimes most pages
on a site are remarkably static. We can build a simple plugin that utilizes
Zend_Cache to seed and pull from the cache.

Our cache criteria will be as follows:

  • Cache configuration will be passed to the constructor
  • Only GET requests will be cached
  • Redirects will not be cached
  • Any given action can tell the plugin to skip caching

For this plugin, we will need to implement two different hooks. First,
routing needs to have finished, but the dispatch loop should not yet have
run when we test to see if we have a cache hit. Second, we want to cache
only when we’re certain that all actions have finished. So, we’ll implement
the dispatchLoopStartup() and
dispatchLoopShutdown() hooks to accomplish our task.

/**
 * Caching plugin
 * 
 * @uses Zend_Controller_Plugin_Abstract
 */
class My_Plugin_Caching extends Zend_Controller_Plugin_Abstract
{
    /**
     *  @var bool Whether or not to disable caching
     */
    public static $doNotCache = false;

    /**
     * @var Zend_Cache_Frontend
     */
    public $cache;

    /**
     * @var string Cache key
     */
    public $key;

    /**
     * Constructor: initialize cache
     * 
     * @param  array|Zend_Config $options 
     * @return void
     * @throws Exception
     */
    public function __construct($options)
    {
        if ($options instanceof Zend_Config) {
            $options = $options->toArray();
        }
        if (!is_array($options)) {
            throw new Exception('Invalid cache options; must be array or Zend_Config object');
        }

        if (array('frontend', 'backend', 'frontendOptions', 'backendOptions') != array_keys($options)) {
            throw new Exception('Invalid cache options provided');
        }

        $options['frontendOptions']['automatic_serialization'] = true;

        $this->cache = Zend_Cache::factory(
            $options['frontend'],
            $options['backend'],
            $options['frontendOptions'],
            $options['backendOptions']
        );
    }

    /**
     * Start caching
     *
     * Determine if we have a cache hit. If so, return the response; else,
     * start caching.
     * 
     * @param  Zend_Controller_Request_Abstract $request 
     * @return void
     */
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        if (!$request->isGet()) {
            self::$doNotCache = true;
            return;
        }

        $path = $request->getPathInfo();

        $this->key = md5($path);
        if (false !== ($response = $this->getCache())) {
            $response->sendResponse();
            exit;
        }
    }

    /**
     * Store cache
     * 
     * @return void
     */
    public function dispatchLoopShutdown()
    {
        if (self::$doNotCache
            || $this->getResponse()->isRedirect()
            || (null === $this->key)
        ) {
            return;
        }

        $this->cache->save($this->getResponse(), $this->key);
    }
}

During dispatchLoopStartup(), the plugin does several things.
First, it checks to see if certain initial conditions are met — for
instance, that we have a GET request. It then sets up a cache key, based on
the current request, and checks to see if we have a cache hit. If so, it
sends the response from the cache. In dispatchLoopShutdown(),
we check to see if we’ve indicated that the plugin should not cache, if it’s
a redirect, or if for some reason we have no cache key; if any of these
conditions are met, we return early. Otherwise, we cache the response
object.

How do you suppress caching for an action? You may have noticed the public
static member, $doNotCache. In an action, simply set this to a
true value:

My_Plugin_Caching::$doNotCache = true;

This will suppress storing a cache for the current request, and thus mean no
cache hit is ever found on subsequent requests to the same location.

Savvy readers may be wondering why I used dispatchLoopStartup()
instead of routeStartup(), particularly as I’m looking only at
the request object. The rationale is for future considerations I may need to
make: I could easily expand on this to allow specifying specific routes,
modules, controllers, or actions that should never be cached; specifying
alternate cache keys for custom routes (as you may need logic to include URI
parameters as part of the caching logic to ensure that individual resource
pages are cached separately), etc. These would all depend on routing having
finished.

However, the main purposes of this example stand: using multiple hooks to
achieve an overall goal — caching — as well as methods for interacting
with a plugin.

Forwarding to Additional Actions

One topic that is asked quite often is how to forward to another action, or
determine if the current request is already forwardng to another action.

The request object contains that information in a special flag,
isDispatched. When that flag is false, then the current request
has not yet been dispatched (typically true when checking prior to the
dispatch loop, or after a call to _forward() in an action); in
other words, it’s a new request. If the flag is true, then that indicates
that the current request has already been dispatched.

Thus, to dispatch another action, simply update the state of the
request, and set the flag to false. As an example, to forward to
SearchController::formAction(), you might have code like the
following in your hook:

$request->setModuleName('default')
        ->setControllerName('search'))
        ->setActionName('form')
        ->setDispatched(false);
}

To check and see if a request has been dispatched, do the following:

if ($requst->isDispatched()) {
    // request has already been handled
} else {
    // new request, not yet dispatched
}

Note: you may want to check out the ActionStack plugin and helper, added in
1.5.0, which allows you to add actions to a stack; the plugin pulls off that
stack on each iteration of the dispatch loop (unless another action is
already waiting to dispatch), allowing you to pass several actions at once
for the application to loop over.

Other Considerations

What sorts of things should you not do with plugins? My rule of
thumb is that if the functionality requires any sort of introspection of or
interaction with the action controller, you should use an action helper
instead. Additionally, if the functionality will be enabled based on the
module, controller, or action — i.e., if only a subset of your application
depends on the functionality or will be affected by it — action helpers are
again a better choice.

However, if the functionality deals with the site as a whole — such as the
initialization and caching plugin examples presented, plugins are the
appropriate approach.

Conclusion

Hopefully this tutorial has shown you that plugins are not an esoteric
topic, but instead something rather trivial to implement. Plugins provide an
excellent way to add functionality at some key flex points in Zend Framework
MVC applications, and can provide application wide coherency and
configuration.

About Matthew Weier O'Phinney

Matthew is a Principal Engineer at Zend Technologies. He is currently project lead for both Zend Framework and Apigility; a Zend Certified Engineer; and a member of the Zend Education Advisory Board, the group responsible for authoring the Zend Certification Exam. He contributes to a number of open source projects, blogs on PHP-related topics, and presents talks and tutorials related to PHP development and the projects to which he contributes. You can read more of his thoughts on his blog, mwop.net/blog.