Using core CakePHP functionality to implement positive cache expiry
I'm currently sat on the plane on the way back from CakeFest. It was a really enjoyable event in a great city and I think the organisers should be very proud of the event. A big thank you to them for all their hard work. I'm also very grateful for all the positive feedback I received following my talk entitled CakePHP at a Massive Scale on a Budget .
I wasn't very happy with the final piece of the talk where I was hoping to demonstrate positive cache expiry using core CakePHP functionality. As I've got eight hours to kill I thought I'd explain now.
For those who didn't catch the talk I'll quickly explain what I mean by positive cache expiry. Often web developers simply cache data until it expires or simply delete cached items when things are updated. This is fine in most circumstances but can cause problems when your site needs to instantly update. A news site, for example. We have to cache listings of the latest news, features, reviews and blog articles on the homepage. When new articles are added we want them to instantly appear in the listing on the homepage.
I came up with what needed to be a very complicated solution to this when writing CyclingNews but you can add it to your CakePHP application fairly simply.
We have a table in our website called articles. Here is the SQL for it.
CREATE TABLE articles (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
handle varchar(255) DEFAULT NULL,
strapline varchar(255) DEFAULT NULL,
article_type_id int(10) unsigned NOT NULL,
publish_date datetime NOT NULL,
body mediumtext NOT NULL,
created datetime DEFAULT NULL,
modified datetime DEFAULT NULL,
PRIMARY KEY ( id ),
UNIQUE KEY handle ( handle ),
KEY article_type_id ( article_type_id ),
);
The field article_type_id
signifies whether the article is a
news, feature, review or blog article.
Fat models make our positive cache expiry much easier. Because we use the
Article::getLatest()
method to get the latest
articles throughout the site we can easily setup an automatic cache update
system with it.
class Article extends AppModel {
var $name = 'Article';
var $displayField = 'name';
var $cachedMethods = array(array( 'getLatest', 'article_type_id' ));
function getLatest($article_type_id = null, $force = false)
{
if (!$force) {
$data = Cache::read('articles_latest_' . $article_type_id);
if ($data !== false) {
return $data;
}
}
$params = array( 'conditions' => array( 'Article.published_state_id' => 4 ),
'order' => array( 'Article.publish_date' => 'DESC' ),
'limit' => 10 );
if ($article_type_id) {
$params['conditions']['Article.article_type_id'] = $article_type_id;
}
$data = $this->find('all', $params);
Cache::write('articles_latest_' . $article_type_id, $data);
return $data;
}
}
We're using a separate controller to keep the website logic away from CMS logic. It's not really necessary for this example because we are only displaying articles on the homepage but for more complicated sites and in combination with something like LazyModel, it allows us to generate pages slightly faster.
class WebsitesController extends AppController {
var $name = 'Websites';
var $helpers = array('Javascript');
var $uses = array();
function beforeFilter()
{
$this->layout = 'website';
}
function home() {
$this->loadModel('Article');
$this->set('latestNews', $this->Article->getLatest(1));
$this->set('latestFeatures', $this->Article->getLatest(2));
$this->set('latestReviews', $this->Article->getLatest(3));
$this->set('latestReports', $this->Article->getLatest(4));
$this->set('latestPreviews', $this->Article->getLatest(5));
$this->set('latestBlogs', $this->Article->getLatest(6));
}
}
To enable us to do the automatic cache updating we've added the following
AppModel::afterSave()
method to app_model.php.
class AppModel extends Model {
var $cachedMethods = array();
function afterSave($created)
{
$this->cacheUpdate = true;
$data = $this->read(null, $this->data[$this->name]['id']);
Cache::write('model' . $this->name . '_' . $data[$this->name]['handle'], $data);
foreach ($this->cachedMethods as $method) {
$args = array();
if (count($method) > 1) {
for ($i = 1; $i < count($method); $i++) {
$args[] = $data[$this->name][$method[$i]];
}
}
$args[] = true; // last argument is always true to force update
call_user_func_array(array($this, $method[0]), $args);
}
return true;
}
}
You update a news article. News articles have an article_type_id of 1. When
AppModel::afterSave()
is called it looks in the
$cachedMethods
array for methods that need to be
updated. The Article model has the following in
$cachedMethods:
var $cachedMethods = array(array( 'getLatest', 'article_type_id' ));
This tells the cache updating method to run the method
Article::getLatest()
and pass the
article_type_id
of the article that is being saved
as the first parameter. This will put the latest news listing in the cache
when a news article is updated. e.g. The following will be run:
$this->getLatest(1, true);
If you look back at the Article::getLatest() method you'll see the optional second parameter allows us to bypass the cache and fetch the latest data. We'll now have the latest data in the cache.
I hope that better explains what I was trying to get across at the end of my talk.