As we all know, server-side caching is a useful technique for optimizing the performance of web apps. Whenever some expensive operation is called for – a db query, a remote web service call, etc – caching the results of the operation means that the next time you need them, you can get them relatively cheaply from the cache.
Zend_Cache: A basic usage example
Zend Framework contains a Zend_Cache
component that supports a variety of frontends and backends. Typical usage for filesystem-based caching is something like the following:
// create the cache object
$frontend = array(
'lifetime' => 86400, // seconds
'automatic_serialization' => true,
);
$backend = array(
'cache_dir' => '/path/to/your/cache',
);
$cache = Zend_Cache::factory('Core', 'File', $frontend, $backend);
// now use the cache object
$cacheId = 'someCacheId';
$data = $cache->load($cacheId);
if (false == $data){
$data = someExpensiveOperation();
$cache->save($data, $cacheId);
}
In practice, I tend to put the cache creation portion into a factory/container class of some kind, while the usage/consumption is often in a repository or service class. But for clarity, it is shown all in line, as if it were in a controller.
The first time you run through this code, the data will not have been saved in cache (a cache miss, as they say), so you will be forced to incur the cost of the someExpensiveOperation()
call. But the next time you run through it – assuming the cache has not expired beyond its lifetime – then you will register a cache hit and bypass the expensive operation.
All cool.
Yeah, but…
But there is one thing that has always bugged me about this typical flow: generating the $cacheId
. For example, if someExpensiveOperation()
is a db call to fetch an article, then the $cacheId
will probably employ the id or slug of the article:
// $slug is the slug of the article, something like 'my-cool-article'
$cacheId = 'article_byslug_' . str_replace('-', '_', $slug); // Zend_Cache does not like cache id's with hyphens
Generating a cache id strikes me as part of the internal details of the “caching process”. As such, it seems to me like the knowledge of how to do that should be embedded inside the caching object itself. It’s his business to know how to load data from cache, save data to cache, and remove data from cache. Connecting an id to that data should be part of his job. So, why should I have to construct cache id’s for him?
Even further, why am I seeing cache id’s at all? Couldn’t the cache object simply offer me an interface with methods like:
public function loadArticlebySlug($slug);
public function saveArticle($article);
public function removeArticleBySlug($slug);
Isn’t there some easy way to make this happen?
Custom cache frontends to the rescue!
It turns out that Zend_Cache::factory()
actually does allow me to create custom frontends that implement whatever interface I want. I can define my interface:
interface Project_Cache_Frontend_ArticleInterface
{
public function loadArticlebySlug($slug);
public function saveArticle($article);
public function removeArticleBySlug($slug);
}
Then implement in a class extending Zend_Cache_Core
:
class Project_Cache_Frontend_Article extends Zend_Cache_Core implements Project_Cache_Frontend_ArticleInterface
{
public function loadArticleBySlug($slug)
{
return $this->load($this->getCacheIdBySlug($slug));
}
public function saveArticle($article)
{
return $this->save($article, $this->getCacheIdBySlug($article->slug));
}
public function removeArticleBySlug($slug)
{
return $this->remove($this->getCacheIdBySlug($slug));
}
protected function getCacheIdBySlug($slug)
{
return 'article_byslug_' . str_replace('-', '_', $slug);
}
}
Note the protected method getCacheIdBySlug($slug)
. All the knowledge of how to create cache id’s is wrapped up in the cache object. It just exposes a functional interface that describes what you want to do, not how you want it done.
Now, tell the Zend_Cache::factory()
method to use my custom frontend:
// frontend and backend options as before
$frontend = array(
'lifetime' => 86400, // seconds
'automatic_serialization' => true,
);
$backend = array(
'cache_dir' => '/path/to/your/cache',
);
$cache = Zend_Cache::factory('Project_Cache_Frontend_Article', 'File', $frontend, $backend, true, false, true);
Note that we are specifying the complete name of our implementing cache class, as well as three boolean values.
The first boolean value – true
in this case – tells the factory that we are using a custom frontend.
The second boolean value – false
in this case – tells the factory that we are not using a custom backend.
The third boolean value – true
in this case – tells the factory to use autoloading to instantiate the frontend and backend objects.
Fully constructed by the factory with the custom frontend, cache usage is cleaner:
$article = $cache->loadArticleBySlug($slug);
if (false === $article){
$article = someExpensiveOperationToGetArticleBySlug($slug);
$cache->saveArticle($article);
}
No cache id’s, no mixing of concerns.
Maybe it’s a whole lot of work just to tuck away some cache id generation. But I confess that it feels better to me.