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.