Tuesday, July 8, 2008

Action Helpers in Zend Framework

Action Helpers in Zend Framework are often considered a fairly arcane subject, something for experts only. However, they are meant to be an easy way to extend the capabilities of Action Controllers, negating the need to create your own base controller with custom functionality. The aim of this tutorial is to show you how to quickly and easily create and use Action Helpers to your advantage.

Basics

Many tutorials on Zend Framework would have you believe you should create a base class extending Zend_Controller_Action to provide base functionality for your controllers:

/**
* Your concrete controllers would now extend My_Controller_Action
*/
abstract class My_Controller_Action extends Zend_Controller_Action
{
// create your utility methods here...
}


However, this is not only not necessary, but typically not a great move for extensibility. You may find later that a given controller only needs a subset of the methods in your base controller -- or that you're constantly adding methods that only a few of your controllers need, creating bloat.

The better solution is to use action helpers. Action helpers are intended to provide run-time, use-at-will capabilities to action controllers. In other words, you can use them if you need them, but they aren't loaded by default.

Action helpers are handled by a broker. Zend_Controller_Action_HelperBroker maintains a static registry of registered helpers, and also serves as a factory for loading helpers on demand. By default, the $_helper property of Zend_Controller_Action contains an instance of the broker.

When an action controller is instantiated, a new broker instance is registered with it, and the action controller is in turn registered with the broker. When you retrieve or access helpers, they then have access to the controller -- allowing integration with it. So, for example, you can set public properties or call public methods on the action controller via your helper -- and vice versa.

In general, you retrieve your helper by using the last segment of the class name. So, for example, if your helper is named 'Foo_Helper_Bar', you'd refer to it as 'bar'. You then have two options for retrieving it: as a property of the broker, or via the getHelper() method:

$bar = $this->_helper->bar;
$bar = $this->_helper->getHelper('bar');


However, this is just the tip of the iceberg.

The direct() Method

Action helpers can use the Strategy Pattern. If you define the method direct() in your helper, you can call your helper as if it were a method of the broker.

An illustration is worth a thousand words. Let's look at the Url helper, which returns a URL based on the input received:

$url = $this->_helper->url('bar', 'foo'); // "/foo/bar"


Implementing the direct() method in your action helpers is an easy way to add virtual functionality to your actions.

Event Hooks

As if that wasn't enough, Action Helpers also have several event hooks to help automate functionality. The three hooks provided are:

  • init(): called when the action controller is intialized (but only if an instance of the helper already exists in the broker)
  • preDispatch(): called after plugin preDispatch() routines, but prior to the action controller preDispatch() routines -- but only if an instance of the helper already exists in the broker.
  • postDispatch(): called after the action controller postDispatch() routines, but prior to plugin postDispatch() routines -- but only if an instance of the helper already exists in the broker.

Note the caveat on each: only if an instance of the helper exists in the broker already. Typically, you'll load helpers on-demand -- i.e., only when you need them. However, there are some cases where you may want to add automatic functionality similar to plugins -- but with the ability to introspect the current controller. This is where the action helper hooks come in handy.

As an example, the ViewRenderer, which is enabled by default in the ZF MVC, is an action helper. It uses the hooks as follows:

  • init(): Initializes the view object, sets appropriate script, helper, and filter paths for the current controller, and registers the current view object as the controller's "view" member.
  • postDispatch(): determines if it should render anything, and, if so, renders a view script based on the current (or requested) action to the appropriate response segment.

As another example, you could add a preDispatch() hook to an action helper that checks a public member of your action controller to determine which actions require authentication -- and redirect to a login form when a match is made. This works better than using a standard plugin, as it allows you to keep the information about authentication requirements with the controller -- where it belongs.

Registering Helpers with the Broker

If you want to make use of hooks, you will need to register your helpers early -- typically in the bootstrap or an early running plugin. To do this, you register them with the broker:

Zend_Controller_Action_HelperBroker::addHelper(
new Foo_Helper_Bar()
);


However, another reason to register with the broker is to ensure that your custom helpers are found. To this end, you can simply tell the helper broker the class prefix of your helpers, so it knows where to find them:

// By class prefix:
Zend_Controller_Action_HelperBroker::addPrefix('Foo_Helper');

// Alternately, providing the path to classes with that prefix, if they
// are not on the include_path:
Zend_Controller_Action_HelperBroker::addPath($path, 'Foo_Helper');


Adding a path or prefix only tells the broker where to look for helpers -- it doesn't instantiate them. If you want a helper loaded before the dispatch cycle, so that event hooks can be utilized, you will still need to either add an instance of the helper to the broker, or attempt to retrieve it (which will create an instance). Which brings us to our next topic.

Retrieving Helpers from the Broker Statically

Sometimes you may find that you want to utilize an action helper outside an action controller -- perhaps to configure it, or because it offers functionality you need. The static method getStaticHelper() is used to perform this.

As an example, I often find I need to configure the ViewRenderer -- for instance, to set some default view helper paths. I can do so as follows:

$viewRenderer = Zend_Controller_Action_HelperBroker::getStaticHelper('ViewRenderer');

$viewRenderer->initView(); // make sure the view object is initialized
$viewRenderer->view->addHelperPath($path); // set a helper path


It's a lot to type, but you typically won't need such functionality often. However, the functionality it provides is useful -- since only one instance of a particular helper can exist in the broker at any given time, you can be assured that you're configuring it globally.

Creating Your Own Helper

Action helpers should extend the Zend_Controller_Action_Helper_Abstract class. That class contains the following utility methods:

  • setActionController(), for setting the current action controller
  • getActionController(), for retrieving the current action controller
  • getFrontController(), for retrieving the current front controller instance
  • getRequest(), for retrieving the current request object (uses the action controller first, then looks in the front controller)
  • getResponse(), for retrieving the current response object (uses the action controller first, then looks in the front controller)
  • getName(), to return the name of the helper

In addition, you can also define any of the event hook methods as listed previously.

You can have your action helper do anything you want at this point. If it will have a common action, you may want to expose it via the direct() method, as detailed earlier. Otherwise, anything goes.

Example: Form Retrieval Helper

Now that we've learned about action helpers, let's create one.

For our example, let's say you have a suite of controllers that each use one or more forms; furthermore, let's say that a given form may be used in multiple controllers. We're going to create a helper that allows you to fetch a form by class name.

We'll assume that form classes are stored in the 'forms' subdirectory of the current module. We'll also assume that they are namespaced with the current module (unless we're in the default module), plus the prefix 'Form_'; e.g., if we had a 'news' module, forms would be prefixed with 'News_Form_'. Finally we'll use the name passed to the helper to determine which form class to load, using the prefix. We'll primarily use the direct() method to interact with the helper, as we only really have one thing we want the helper to do -- load forms.

/**
* Action Helper for loading forms
*
* @uses Zend_Controller_Action_Helper_Abstract
*/
class My_Helper_FormLoader extends Zend_Controller_Action_Helper_Abstract
{
/**
* @var Zend_Loader_PluginLoader
*/
public $pluginLoader;

/**
* Constructor: initialize plugin loader
*
* @return void
*/
public function __construct()
{
$this->pluginLoader = new Zend_Loader_PluginLoader();
}

/**
* Load a form with the provided options
*
* @param string $name
* @param array|Zend_Config $options
* @return Zend_Form
*/
public function loadForm($name, $options = null)
{
$module = $this->getRequest()->getModuleName();
$front = $this->getFrontController();
$default = $front->getDispatcher()
->getDefaultModule();
if (empty($module)) {
$module = $default;
}
$moduleDirectory = $front->getControllerDirectory($module);
$formsDirectory = dirname($moduleDirectory) . '/forms';

$prefix = (('default' == $module) ? '' : ucfirst($module) . '_')
. 'Form_';
$this->pluginLoader->addPrefixPath($prefix, $formsDirectory);

$name = ucfirst((string) $name);
$formClass = $this->pluginLoader->load($name);
return new $formClass($options);
}

/**
* Strategy pattern: call helper as broker method
*
* @param string $name
* @param array|Zend_Config $options
* @return Zend_Form
*/
public function direct($name, $options = null)
{
return $this->loadForm($name, $options);
}
}


Place the above in a file named 'FormLoader.php' in the "My/Helper/" directory of your library (or any directory on your include_path).

Okay, now, how would we use it? Let's assume we're in the LoginController in the default module, and want to load the 'login' form. We'd name it 'Form_Login', and place the class file in 'forms/Login.php' in our application directory:

application/
controllers/
LoginController.php
forms/
Login.php - Contains class 'Form_Login'


In our bootstrap file or an early-running plugin, we'd make sure that we tell the broker where to find our helpers:

Zend_Controller_Action_HelperBroker::addPrefix('My_Helper');


And, finally, in our controller, we can now grab our form using the helper:

$loginForm = $this->_helper->formLoader('login');


Seem like a lot of work to simply load a form? Consider this: as long as you follow the rules outlined by our FormLoader helper, you can now use this in all your action controllers. So, you may have a UserController, and need to grab the registration form:

$regForm = $this->_helper->formLoader('registration');


Additionally, once you've registered a particular helper prefix (e.g., 'My_Helper'), you can drop other helpers in that same location, and they'll automatically be found by the broker -- you won't have additional setup past that original call to add the prefix to the broker.

The point here is that action helpers help you DRY up your code -- you push the bits and pieces you think you'll use again and again in your controllers to your action helpers. After a while, you'll have a library of controller-related functionality that you can draw on for other projects -- without needing your own, custom base class for action controllers, which ultimately leaves your library more extensible and flexible.

2 comments:

pinkgothic said...

Dude(tte). Don't you know it's rude to steal entire articles without creditting the original author? o_o;

http://devzone.zend.com/article/3350-Action-Helpers-in-Zend-Framework

Tuna said...

Thank you for your great explanation and exemplification about Zend Action Helpers. Very simple and beautiful.