In one of my side projects, which I’m currently refactoring to use CakePHP, I needed to have certain dynamic (i.e. database-retrieved) elements on every page: the three latest entries from the articles section, certain user details, etc. I could add a whole lot of models to the $uses array of my individual models or AppModel, but that results in a lot of overhead and it just doesn’t make sense, since, if I’m just displaying the latest three articles in a site-wide sidebar, my other models aren’t really “using” Article. So I searched for a better, more fluid solution.
The simplest fix is the requestAction method, but that results in a lot of overhead since it results in an almost-complete page load with every use (basically, it just loads another URL on the fly). I searched around the popular CakePHP blogs for a few hours looking for a good solution, but, after not finding anything I liked, I decided to make my own. I actually got the idea for this component from the comments on debuggable.com‘s post ‘requestAction considered harmful‘ -Rafael Bandeira suggested a Widget view helper, which I thought was a great idea but decided to go with a controller component instead.
And that’s where the Widget component comes in. Basically, this component, which I attach to AppController, allows you to retrieve any information from any model, without directly interacting with the model itself and without using $uses, App::import, ClassRegistry, etc. The basic idea is that you write your own findX methods in your models, which you then call via the Widget component.
So, we’re going to work through implementing a Widget component, and we’ll use it to get the latest three published entries from the Articles table and display them across the site. Here’s what we’ll do:
Most CakePHP developers who’ve graduated from the ‘novice’ school have done this, but for those who haven’t: adding custom find methods is a great way to extend your application. And, it’s easy! I’m using the method found in Matt Curry‘s excellent (and free!) Super Awesome Advanced CakePHP Tips e-book.
We just need to overwrite the default Model::find() method with a new find() in our AppModel. It looks something like this:
// For custom find('xxx') methods. The function looks for a __findFindType class method.
function find($type, $options = array()) {
$method = null;
if (is_string($type)) {
$method = sprintf('__find%s', Inflector::camelize($type));
}
if ($method && method_exists($this, $method)) {
return $this->{$method}($options);
} else {
$args = func_get_args();
return call_user_func_array(array('parent', 'find'), $args);
}
}
Basically, whenever we call find(), we’re going to look in the model doing the finding for a __findXXX method that matches find(‘XXX’). If the method doesn’t exist then we fall back on the default find() method.
Now we’ll write a method in the Article model – /app/models/article.php – class to find the latest articles:
function __findLatest($limit = 3) {
return $this->find('all', array('limit' => 3, 'published' => true, 'order' => 'Article.begin_publishing DESC'));
}
Pretty simple. To use it, we just call: $this->Article->find(‘latest’);
Finally, the good stuff! The Widget component is simple too; it only has one method (of course, expand as you see fit). Put this in /app/controllers/components/widget.php:
<?php
class WidgetComponent extends Object {
function retrieve($modelName, $findType, $options = NULL) {
$model = ClassRegistry::init($modelName);
return $model->find($findType, $options);
}
}
?>
As you can see, we’re using a method called ‘retrieve’ to find our data. Its first argument is the name of the model from which you want to grab data, while the second argument denotes the name of the findXXX method. The third argument is any optional data you want to pass along. You’ll see an example usage in the next section. Speaking of which…
So now that we’ve written our custom find method and Widget Component, the only thing left to do is add the functionality to AppController. First, add it to the $components array in /app/app_controller.php:
var $components = array('Widget');
Next, write a function in AppController to set your common (site-wide) widgets. I like to call it “_setCommonWidgets()”:
function _setCommonWidgets()
{
$this->set('latestArticles', $this->Widget->retrieve('Article', 'latest'));
}
And there’s the Widget Component in action. We call the retrieve() method, pass along the model name we want – Article – and the find type, ‘latest’. We set the resulting array as view variable, so it’ll be available in any view as $latestArticles. But wait!
We forgot one step: we need to invoke the _setCommonWidgets() function. Easy stuff: just call it in AppController::beforeRender():
// Set any common 'widget' variables. $this->_setCommonWidgets();
And that’s it. Now you can use _setCommonWidgets to give your views access to any data you’d like, without resorting to requestAction, $uses, manual calls to App::import or ClassRegistry, etc.
I’d love feedback on this feature. Improvements especially are welcome.
5 Responses to A Quick, Dirty and Useful Widget Component for CakePHP
Jamie Nay » Persisting Pagination Limits in CakePHP
July 12th, 2009 at 7:46 pm
[...] the view is rendered, so in, say, beforeFilter() – I actually set mine in conjunction with my quick and dirty widget component (/end shameless plug [...]
Daniel
March 13th, 2010 at 9:28 am
I just wanted to say thank you, this works perfectly
Daniel
March 14th, 2010 at 2:13 am
Hi, quick follow up question – is there anyway that these can be cached?
Jamie
March 14th, 2010 at 11:29 am
Hi Daniel – always nice to hear when someone finds your code useful.
To use this with caching, I’d just use the automatic query caching method I wrote about a while ago:
http://jamienay.com/2009/11/adding-automatic-caching-to-model-find-in-cakephp-1-2/
Once you put that in, you can just pass ‘cache’ as one of the Widget::retrieve() arguments, as outlined in the caching article.
Mark
July 14th, 2010 at 4:48 am
Thank you. Thank you! THANK YOU!