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($cacheEnabledTMP_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.

If you would like to comment on this article, please ping me on twitter.
If your response won't fit into 140 characters, write a blog post and then ping me!