Notes on Zend_Cache
Recently I needed to speed up a legacy project that makes a lot of database calls to generate each page. After profiling, I discovered that 90% of the database calls returned data that rarely changed, so decided to cache these calls. One of the nice things about Zend_Framework is that its use-at-will philosophy means that you can use any given component with minimal dependencies on the rest of the framework code.
In my case, I wanted to use Zend_Cache, so I needed Zend/Cache/*, Zend/Cache.php, Zend/Loader/*, Zend/Loader.php and Zend/Exception.php and didn’t bother with any other part of the framework.
The application I’m speeding up is completely procedural with lots of include files and no virtually no classes anywhere other than in the lib/ directory! I wanted to minimise the disruption to the current code and so it seemed that a simple static class that provided a set of proxy functions to an underlying Zend_Cache object would be easiest. I also provided a mechanism to turn off the cache using a simple boolean that could be set when initialising the class.
The class is called TheCache:
class TheCache
{
/**
* @var boolean
*/
static protected $_enabled = false;
/**
* @var Zend_Cache_Core
*/
static protected $_cache;
static function init($enabled, $dir, $lifetime=7200) {
self::$_enabled = $enabled;
if(self::$_enabled) {
require_once 'Zend/Cache.php';
$frontendOptions = array(
'lifetime' => $lifetime,
'automatic_serialization' => true,
);
$backendOptions = array(
'cache_dir' => $dir,
'file_name_prefix' => 'thecache',
'hashed_directory_level' => 2,
);
self::$_cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
}
}
static function getInstance() {
if(self::$_enabled == false) {
return false;
}
return self::$_cache;
}
static function load($keyName) {
if(self::$_enabled == false) {
return false;
}
return self::$_cache->load($keyName);
}
static function save($keyName, $dataToStore) {
if(self::$_enabled == false) {
return true;
}
return self::$_cache->save($dataToStore, $keyName);
}
static function clean()
{
if(self::$_enabled == false) {
return;
}
self::$_cache->clean();
}
}
The init() function is used to set up the cache to use files stored in the supplied directory. The only configuration is to choose whether the cache is enabled and the lifetime of all objects stored in it. Note that this is where we get specific to the problem in hand. In this specific case, I didn’t need different lifetimes for each item stored in the cache, which nicely simplified everything.
Let’s look at how it is used. First we initialise the cache in an include file that happens to be included for every request:
$cacheEnabled = (bool)getenv('THE_CACHE_ENABLED') ? getenv('THE_CACHE_ENABLED') : false;
TheCache::init($cacheEnabled, TMP_DIR.'/the-cache/');
I use an environment variable set using SetEnv within my Apache virtual hosts to determine if the cache should be enabled or not, so we use getenv() to retrieve the value and then call TheCache::init(). The use of THE_CACHE_ENABLED allows me to disable the cache on my development machine, and have it enabled on my live server without having any code changes. Obviously, TMP_DIR is defined previously.
Now that we have initialised the cache, we can use it. Within this application, we generally use the ADODB database classes to generate arrays of data from the database along these lines:
$sql = 'SELECT x,y FROM z WHERE a=b';
$rs = $db->Execute($sql);
$data = $rs->GetArray();
To cache this information, I changed the code to look like this:
$keyName = 'data-z-a-b'; // unique name describing this data set
$data = TheCache::load($keyName)
if($data === false) {
$sql = 'SELECT x,y FROM z WHERE a=b';
$rs = $db->Execute($sql);
$data = $rs->GetArray();
TheCache::save($keyName, $data);
}
Firstly we invent a unique name for the dataset and assign to $keyName and then load the data from the cache object using the load() function. If the data is cached, then we are done. If not, we perform the SQL query to get the data and then store it into the cache using save(). Rinse and repeat for each operation whose results you want to cache.
And that’s all there is to it.
"After profiling, I discovered that 90% of the database calls returned data that rarely changed"
It would be nice if in the future you show us how you do this.
Thank you, greate article, as always :)
Is there any reason to not declare init()-method as static?
svenwin,
Carelessness on my part when formatting for posting here! I've updated.
Thanks,
Rob…
If you use AdoDB, why don't you just use the built-in caching possibilities?
Jeroen,
Although I only showed the database related items here, we also cached some non-db related processes.
Regards,
Rob…
iongion,
I have a Zend Studio 5.5 license which has a profiler built in which is very useful at times :)
Rob…
As for me I do not like Zend Studio etc. I love Eclipse!
Snowcore
from what i discovered, the new zend studio is eclipse with a zend php plugin.
@iongion: I think you're right about that, I believe it is a version of the PDT with more features.
I also have a post on my site (http://www.chrisabernethy.com/zend-framework-route-benchmarks/) that talks about profiling using eclipse, PDT, xdebug and kcachegrind.
Hi Rob, I was wondering, is it possible to use Zend_Cache to convert my dynamic generated pages into static HTML pages? For example,
http://www.example.com/update/show/id/3
would map to the following page:
http://www.example.com/update/show/3.html
At this time, I'm interested in page caching.
Thanks in advance,
-Conrad
Conrad,
I would think you could do something with a custom route and Zend_Cache.
Regards,
Rob…
Hi Rob,
Thanks for Zend Framework in Action book, I´ve had it since october 2007 (MEAP) and it´s helping me a lot.
I hope you can help me with this problem:
I am working on an Intranet, I was using the Zend_Cache
$cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
to cache a queries (lists) that are showed always you enter on each section. But, when you insert or update some item related to a query its cache must be removed.
I am using the tags. When I create a new cache I tag it with the names of all tables involved in that query.
While this tables not change the cache is available.
But when a change happen:
In My_Model_Db_Table class i put this:
$cache->clean(Zend_Cache::CLEANING_MODE_MATCHING_TAG, array($this->_name));
and works perfectly but it is too too slow.
I do not know why but it takes 33 seg more.
The inserts and updates are usual and it takes a long time.
Have you got any idea? Am I doing any wrong?
Thanks in advance.
Cristina.
Cristina,
I recommend asking on the ZF mailing lists.
Regards,
Rob…
I have found this link
http://www.nabble.com/Optimize-Zend_Cache-clearing-to16809942.html#a16809942
which relate the same problem although it does not clarify it.
Thanks.
Cristina.
Hey Its really a very very helpful post. I think with the same coding technique we can use some other zend components also.
Well its really a very good technique to use framework components in procedural php development.
Thanks again for wonderful post.
I think a little error has slipped into your save-function.
In case the cache has not been initated and a key/value is passed, the function will always return true, although the value is never saved.
Dominic,
That's intentional. When the cache is disabled, it doesn't actually matter what is returned, but I don't want any error conditions to occur in the calling code. A true result is more likely to achieve that goal.
Regards,
Rob…
Rob, of course the intended behaviour is totally up to you.
Since this is just a wrapper around Zend_Cache, I was expecting your save-function to behave like the original one:
//Zend_Cache_Core::save(…)
//…
if (!$this->_options['caching']) {
return true;
}
Which is exactly what yours does, so I don't have a point. :)