Zend Front Controller Plugin

来源:互联网 发布:wish for mac 编辑:程序博客网 时间:2024/05/22 10:44

What is a Front Controller Plugin?

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

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

As you start thinking about the hooks listed above, a few questionsmay 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 maywant to do
something to alter the results of routing after routing, or you maywant to
modify the dispatcher prior to entering the dispatch loop, andthese are
semantically different. Having different hooks helps keep thesedistinctions
clear.

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

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

 

public function preDispatch(Zend_Controller_Request_Abstract $request){}

 

I will often speak of “early-running plugins” or “late-runningplugins”. 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 typicallythe latter — plugins
that trigger after actions have been dispatched.

Registering Plugins with the Front Controller

Plugins themselves need to be instantiated and registered with thefront
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, onlyhooks that
are triggered after theplugin 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 stackindex is
provided, then plugins are triggered in the order in which theyare
registered. When a stack index is provided, then that index will behonored
for that plugin.

You can specify the stack index as the second parameter whenregistering the
plugin; the index should be numeric, and a lower number willindicate
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 aplugin,
or configure it, after ithas been registered with the front
controller. You can retrieve a plugin by passing the plugin’s classname 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 onewith the
front controller, the burning question is: what uses exist forplugins? To
answer this question, let’s first look at how plugins are used inexisting
ZF components.

Zend_Layout

Zend_Layout can optionally beused with the MVC components, and
when it is, it registers a plugin with the front controller. Thisplugin
listens to the postDispatch() hook,and is registered with a
late stack index to ensure it runs after all other pluginshave
executed, as well as to ensure no other actions exist to loopover.

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

Error Handling

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

Potential Uses for Plugins in Your Applications

Now that you’ve seen some concrete examples of how plugins arealready 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 mostexamples
of Zend Framework MVC apps, we show a bootstrap file that containsthe
entire application initialization — loading configuration, loadingall
plugins, initializing the view and database, etc. This works well,but it
can lead to a somewhat sloppy file, and also leaves the potentialto leak
important information about your system should the file ever bedisplayed
without processing (ala the Facebook fiasco last year).

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

 

class My_Plugin_Initialization extends Zend_Controller_Plugin_Abstract{        public function __construct($env)    {        $this->setEnv($env);    }        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 befairly
self-explanatory. The main thing to get from this, however, is thatwe’ve
now moved the complexity of the bootstrap into a class, and alsoprovided a
way to group common tasks — helping make our application setupmore
maintainable. We do this by simply leveraging the infrastructureprovided by
the plugin system.

Example: Caching Plugin

As another example, consider a simple caching plugin. Oftentimesmost pages
on a site are remarkably static. We can build a simple plugin thatutilizes
Zend_Cache to seed and pullfrom 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 notyet have
run when we test to see if we have a cache hit. Second, we want tocache
only when we’re certain that all actions have finished. So, we’llimplement
the dispatchLoopStartup() and
dispatchLoopShutdown() hooks toaccomplish our task.

 

class My_Plugin_Caching extends Zend_Controller_Plugin_Abstract{        public static $doNotCache = false;        public $cache;        public $key;        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']        );    }        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;        }    }        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. Ifso, it
sends the response from the cache. In dispatchLoopShutdown(),
we check to see if we’ve indicated that the plugin should notcache, if it’s
a redirect, or if for some reason we have no cache key; if any ofthese
conditions are met, we return early. Otherwise, we cache theresponse
object.

How do you suppress caching for an action? You may have noticed thepublic
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, andthus mean no
cache hit is ever found on subsequent requests to the samelocation.

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 Imay need to
make: I could easily expand on this to allow specifying specificroutes,
modules, controllers, or actions that should never be cached;specifying
alternate cache keys for custom routes (as you may need logic toinclude URI
parameters as part of the caching logic to ensure that individualresource
pages are cached separately), etc. These would all depend onrouting having
finished.

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

Forwarding to Additional Actions

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

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

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

 

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

 

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

 

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 pullsoff that
stack on each iteration of the dispatch loop (unless another actionis
already waiting to dispatch), allowing you to pass several actionsat once
for the application to loop over.

Other Considerations

What sorts of things should you not dowith plugins? My rule of
thumb is that if the functionality requires any sort ofintrospection of or
interaction with the action controller, you should use an actionhelper
instead. Additionally, if the functionality will be enabled basedon the
module, controller, or action — i.e., if only a subset of yourapplication
depends on the functionality or will be affected by it — actionhelpers are
again a better choice.

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

Conclusion

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