An Easy Plugin Callback Component for CakePHP 1.2

Most people who do serious, large scale development with CakePHP would agree that keeping the bulk of a project’s code segregated in plugins is the best way to keep things organized. Sometimes, however, we want to use a plugin’s functionality outside of the scope of that plugin’s controllers. For example, we might have a products plugin with a ‘feature product’ capability, which we might want to display on our site’s homepage. So what are the options? We could just stick some code in the AppController (which I covered in a previous post – my old Widget Component), or you could use the requestAction() function to call a method from another controller. But, both of those options have their faults: putting all of that code in the AppController can quickly add up, while requestAction() eats up a lot of resources since it initiates an entirely new request.
So, we need a solution that keeps the plugin code in the plugin directory and doesn’t use a ton of resources. And that’s where plugin controller callbacks come in. The Cake core really should have built-in callback functionality for plugin controllers, so developers can, for example, set a certain plugin action to fire after every beforeFilter(), during every beforeRender(), etc. But until it’s a part of the core, the community needs to fend for itself. There are a few implementations of plugin callbacks/hooks floating around in the community, but none of them suited my purposes exactly. So, I figured what the hell, I’ll try doing my own! And just like that, my RegisterCallbacksComponent was born. I used a bakery article called “PluginHandler to load configuration and callbacks for plugins” by Gediminas Morkevicius (sky_l3ppard) as a starting point, since I liked the approach he used. I boiled it down, however, to make it simpler and just for callbacks (he handles other auto-loading aspects as well, such as routes).
The component is at the bottom of the post, after the instructions. You can also download it from its repository in my brand-spankin’-new Github account.
The basic idea is that each plugin has a “Callback” class – CamelizedPluginNameCallback (for example, PhotoGalleriesCallback if you’re working with a PhotoGalleries plugin). Within that class are your callback functions which are executed by the component.
Now, I’ve only done some cursory testing at this point, but the component is pretty straightforward and everything seems to be working. To use:

  • Attach the component to your AppController: var $components = array('RegisterCallbacks');
  • If you want certain plugin callbacks to fire before others, you can specify a partial or complete order using the ‘priority’ setting: var $components = array('RegisterCallbacks' => array('priority' => array('ImportantPluginOne, 'ImportantPluginTwo')));
  • Any plugins not specified in the ‘priority’ array are loaded as per Configure::listobjects('plugin')
  • For each plugin you want to include in the callback system, create a plugin_name_callback.php file in the plugin’s root directory (app/plugins/plugin_name). Within that file define a CamelizedPluginNameCallback class.
  • The CamelizedPluginNameCallback class can contain any or all of the following functions:
    • initialize(&$controller): Called before the controller’s beforeFilter method.
    • beforeFilter(&$controller): Called after the controller’s beforeFilter() method but before the controller executes the current action handler. Uses ‘beforeFilter’ instead of ’startup’ to make the action name more consistent with the controller name.
    • beforeRender(&$controller): Called after the controller’s beforeRender method but before the controller renders views and layout.
    • shutdown(&$controller): Called before output is sent to the browser.
    • beforeRedirect(&$controller, $url, $status = null, $exit = true): Called when the controller’s redirect method is called but before any further action.

And that’s basically it. Now you can keep your application-wide plugin code where it belongs – with the plugins. Here’s the full component code, current as of the time of writing. For up-to-date code, use my Github repository.

<?php
/**
* RegisterCallbacksComponent class file
*
* Executes callback functions for each plugin before each controller callback action.
* Files should be located in: /plugins/[plugin_name]/[plugin_name]_callback.php
*
* Inspiration, architecture, and some code from:
* http://bakery.cakephp.org/articles/view/pluginhandler-to-load-configurat...
* 'PluginHandler to load configuration and callbacks for plugin' - Gediminas Morkevicius (sky_l3ppard)
*
* @filesource
* @author Jamie Nay
* @copyright Jamie Nay
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
* @link http://jamienay.com/2009/11/an-easy-plugin-callback-component-for-cakeph...
*/
class RegisterCallbacksComponent extends Object {
/**
* Controller object
*
* @var object
* @access private
*/
var $__controller = null;

/**
* Component settings
*
* @access public
* @var array
*/
public $settings = array();

/**
* Default values for settings.
* - priority (optional): the order in which callbacks should be executed. If
* priority is left empty, or if some plugins are left out of the list, the
* plugins are just added in the order in which they're loaded via Configure.
*
* @access private
* @var array
*/
private $__defaults = array(
'priority' => array()
);

/**
* Registered plugins - plugins that have a PluginNameCallback class.
*
* @access private
* @var array
*/
private $__registered = array();

/**
* Configuration method.
*
* In addition to configuring the settings (see $__defaults above for settings explanation),
* this function also loops through the installed plugins and 'registers' those that have a
* PluginNameCallback class.
*
* @param object $controller Controller object
* @param array $settings Component settings
* @access public
* @return void
*/
public function initialize(&amp;$controller, $settings = array()) {
$this->__controller = &amp;$controller;
$this->settings = array_merge($this->__defaults, $settings);

if (empty($this->settings['priority'])) {
$this->settings['priority'] = Configure::listobjects('plugin');
} else {
foreach (Configure::listobjects('plugin') as $plugin) {
if (!in_array($plugin, $this->settings['priority'])) {
array_push($this->settings['priority'], $plugin);
}
}
}

foreach ($this->settings['priority'] as $plugin) {
$file = Inflector::underscore($plugin).'_callback';
$className = $plugin.'Callback';
if (App::import('File', $className, true, array(APP . 'plugins' . DS . Inflector::underscore($plugin)), $file.'.php')) {
if (class_exists($className)) {
$class = new $className();
ClassRegistry::addObject($className, $class);
$this->__registered[] = $className;
}
}
}

/**
* Called before the controller's beforeFilter method.
*/
$this->executeCallbacks('initialize');
}

/**
* Executes beforeFilter() methods in the callback classes.
* Called after the controller's beforeFilter() method but before
* the controller executes the current action handler.
* Uses 'beforeFilter' instead of 'startup' to make the action name
* more consistent with the controller name.
*
* @param object $controller Controller object
* @access public
* @return void
*/
public function startup(&amp;$controller) {
$this->executeCallbacks('beforeFilter');
}

/**
* Executes beforeRender() methods in the callback classes.
* Called after the controller's beforeRender method but before the
* controller renders views and layout.
*
* @param object $controller Controller object
* @access public
* @return void
*/
public function beforeRender(&amp;$controller) {
$this->executeCallbacks('beforeRender');
}

/**
* Executes shutdown() methods in the callback classes.
* Called before output is sent to the browser.
*
* @param object $controller Controller object
* @access public
* @return void
*/
public function shutdown(&amp;$controller) {
$this->executeCallbacks('shutdown');
}

/**
* Executes beforeRedirect() methods in the callback classes.
* Called when the controller's redirect method is called but
* before any further action.
*
* @param object $controller Controller object
* @param string $url
* @param string $status
* @param boolean $exit
* @access public
* @return void
*/
public function beforeRedirect(&amp;$controller, $url, $status = null, $exit = true) {
$this->executeCallbacks('beforeRedirect', array($url, $status, $exit));
}

/**
* Executes the requested method in each Callback class, in order
* of priority, if that method exists in the class. Also sends any
* arguments, with the $this->__controller always being the first
* argument.
*
* @param string $method Method name
* @param array $args Optional arguments
* @access public
* @return void
*/
public function executeCallbacks($method, $args = array()) {
foreach ($this->__registered as $callback) {
$class = ClassRegistry::init($callback);
if ($class &amp;&amp; in_array($method, get_class_methods($class))) {
call_user_func_array(array($class, $method), array_merge(array($this->__controller), $args));
}
}
}

}
?>