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:
var $components = array('RegisterCallbacks');var $components = array('RegisterCallbacks' => array('priority' => array('ImportantPluginOne, 'ImportantPluginTwo')));Configure::listobjects('plugin')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-configuration-and-callbacks-for-plugins
* '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-cakephp-12/
*/
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));
}
}
}
}
?>
4 Responses to An Easy Plugin Callback Component for CakePHP 1.2
Rachman Chavik
November 23rd, 2009 at 2:30 am
Hi Jamie,
I’ve been looking for ways to do something like this as well.
I considered your post, looked through AD7six mini controller, and some other stuff.
But I will probably take a shortcut and violate MVC a bit more
Here’s my current attempt: http://github.com/rchavik/cholesterol/blob/master/views/helpers/widget.php
– rchavik
Jamie
November 25th, 2009 at 8:22 am
Interesting approach, Rachman. I like the MVC approach as much as the next guy, but I’m not about “violating” it to get stuff done either. With coding time always at a premium for my projects, sometimes the result matters more than the process (ugh, I know, I can’t believe I’m saying that either!).
Rachman Chavik
November 26th, 2009 at 4:03 am
Jamie,
I’m not very familiar with MVC theories or other project’s implementation, but I feel that there should be some sort of mechanism to allow Views to retrieve data from Models in cakephp.
From wikipedia: http://en.wikipedia.org/wiki/Model-view-controller, it mentioned the following:
A view queries the model in order to generate an appropriate user interface (for example, the
view lists the shopping cart’s contents). Note that the view gets its own data from the model.
Also, have a look at http://java.sun.com/blueprints/patterns/MVC-detailed.html
My implementation of the WidgetHelper relies on the assumption that WidgetComponent has been created. That component acts as a data puller, and sends the data back to the ‘element’ via the widget_data variable.
So, in any view you’ll be able to do:
$widget->element(
‘News’ /* Model to use */,
‘news/latest’ /* element views/elements/news/latest.ctp */
);
which then will call the NewsWidgetComponent::latest() function, which in turn calls News model to retrieve data. You can then render the returned widget_data as you wish in element latest.ctp.
Jamie
November 27th, 2009 at 2:58 pm
Rachman – I’m in full agreement that views should be allowed to retrieve data from models. I’ve done that more than a few times. We shouldn’t be letting views modify model data (i.e. inserting, updating or deleting records), but I don’t see anything wrong with giving views a mechanism to access data on demand.
That said, I try to load data in the controllers whenever I can, just to keep things a bit more organized for myself. And that’s why I created this plugin callback system – so I can load plugin-specific variables on a site-wide basis while still keeping everything organized.
Thanks for the feedback and the discussion!