Implementing a Cache System in PHP

In this programming tutorial, you will learn how to create a simple caching system using a PHP segregated interface.

If you’re a PHP developer looking for a guide that teaches you the concepts behind implementing segregated interfaces and how to utilize them, then you have come to the right place. This series of articles will show you how to define fine-grained contracts for your classes, so that they can perform only the tasks they’re responsible for.

In the last installment of this series, I went through the development of a basic registry system, which was capable of using different registry classes to store, retrieve and even dump data. The most engaging aspect of this sample system was that the swappable registry classes implemented a couple of segregated interfaces in order to execute the aforementioned operations.

As with other elements of object-oriented programming, it’s possible to use segregated interfaces in a great variety of scenarios and conditions and, therefore, enjoy the benefits that they provide right out of the box.

If you missed the first two parts in this series, or need a quick refresher, you can find them at:


Making the initial move: defining a segregated interface 

As I stated in the introduction, my goal in this article is to create an extendable caching system based on the contract defined by a segregated interface. To achieve this, the first step we need to take is to create the interface in question.

The following code creates an interface we will call “CacheableInterface”, which sets a contract that outlines the behavior of an abstract cache back-end. Check it out:  

(CacheableInterface.php)

<?php

interface CacheableInterface
{
    public function set($key, $data);
   
    public function get($key);
   
    public function delete($key);
   
    public function exists($key);   
}
 

As shown above, the “CacheableInterface” interface declares a set of methods that permit you to create several different cache back-ends with minor effort. You should pay particular attention to the contract established by the pertinent methods: effectively, any further implementer will have only the functionality required for setting, fetching, checking and even removing elements from the underlying cache mechanism.

This example clearly shows how the use of a segregated interface may help in the creation of classes that have a well-defined range of responsibilities.

Now that our granular contract exists, the next step is to create a couple of them, which will allow you to cache data using both the APC PHP extension and the file system as well.

{mospagebreak title=Building Multiple Cache Back-ends in PHP}

Building a couple of implementers of the previous “CacheableInterface” interface is a pretty straightforward process. As I expressed before, the first cache back-end that I want to show you is a simple wrapper for the APC PHP extension, and its source code is as follows:

(ApcCache.php)

<?php

class ApcCache implements CacheableInterface
{
    /**
     * Save data to the cache
     */
    public function set($key, $data)
    {
        if (!apc_store(strtolower($key), $data)) {
            throw new ApcCacheException('Error saving data with the key ' . $key . ' to the APC cache.');
        }
        return $this;
    }
   
    /**
     * Get the specified data from the cache
     */
    public function get($key)
    {
        if ($this->exists($key)) {
            if (!$data = apc_fetch(strtolower($key))) {
                throw new ApcCacheException('Error fetching data with the key ' . $key . ' from the APC cache.');
            }
            return $data;
        }
        return null;
    }
   
    /**
     * Delete the specified data from the cache
     */
    public function delete($key)
    {
        if ($this->exists($key)) {
            if (!apc_delete(strtolower($key))) {
                throw new ApcCacheException('Error deleting data with the key ' . $key . ' from the APC cache.');
            }
            return true;
        }
        return false;
    }
   
    /**
     * Check if the specified cache key exists
     */
    public function exists($key)
    {
        return (boolean) apc_exists(strtolower($key));
    }
   
    /**
     * Get cache information
     */
    public function getInfo()
    {
        ob_start();
        print_r(apc_cache_info());
        return ob_get_clean();
    }
    
    /**
     * Clear the cache
     */
    public function clear($cacheType = 'user')
    {
        return apc_clear_cache($cacheType);
    }       
}



(ApcCacheException.php)

<?php

class ApcCacheException extends Exception{}

In the above code snippet, the “ApcCache” class acts like a proxy for the most relevant methods included in the APC extension, which allows you to store, fetch and delete data from shared memory. The class is pretty similar to the one that I developed in this previous article (http://www.devshed.com/c/a/PHP/Swapping-Cache-BackEnd-at-Runtime-in-PHP/), so if you had the chance to read the tutorial, understanding how this cache back-end works should be a breeze for you.

Now, take a look at the following class, which also implements the “CacheableInterface” interface, only this one caches data using the file system:

(FileCache.php)

<?php

class FileCache implements CacheableInterface
{
    protected $_cacheDir = 'cache';
   
    /**
     * Constructor
     */
    public function __construct($cacheDir = '')
    {
        if ($cacheDir !== '') {
            if (!is_dir($cacheDir)) {
                throw new FileCacheException('The specified cache directory is invalid.');
            }
            $this->_cacheDir = $cacheDir;
        }
    }
   
    /**
     * Save data to the specified cache file
     */
    public function set($key, $data)
    {
        $cacheFile = $this->getCacheFile($key);
        if (!file_put_contents($cacheFile, serialize($data))) {
            throw new FileCacheException('Error saving data with the key ' . $key . ' to the cache file.');
        }
        return $this;
    }
   
    /**
     * Get data from the specified cache file
     */
    public function get($key)
    {
        if ($this->exists($key)) {
            $cacheFile = $this->getCacheFile($key);
            if (!$data = unserialize(file_get_contents($cacheFile))) {
                throw new FileCacheException('Error reading data with the key ' . $key . ' from the cache file.');
            }
            return $data;
        }
        return null;
    }
   
    /**
     * Delete the specified cache file
     */
    public function delete($key)
    {
        if ($this->exists($key)) {
            $cacheFile = $this->getCacheFile($key);
            if (!unlink($cacheFile)) {
                throw new FileCacheException('Error deleting the file cache with key ' . $key);
            }
            return true;
        }
        return false;
    }
   
    /**
     * Check if the specified cache file exists
     */
    public function exists($key)
    {
        $cacheFile = $this->getCacheFile($key);
        return file_exists($cacheFile);
    }
   
    /**
     * Get the specified cache file
     */
    protected function getCacheFile($key)
    {
        return $this->_cacheDir . DIRECTORY_SEPARATOR . strtolower($key) . '.cache';
    }     
}



(FileCacheException.php)

<?php

class FileCacheException extends Exception{}

This file-based back-end caches data in a specified file, located under the default “cache” directory (although this option can be easily changed). In addition, the class is capable of retrieving and deleting cache files as well, in accordance to the contract set by the “CacheableInterface” interface.

At this point, you saw how easy is to spawn a couple of cache back-ends based on the mentioned interface. Given that, the last thing that needs to be done to get this sample cache system up and running is create an adapter capable of consuming the previous cache classes via dependency injection.

The following class implements this adapter in a simple way:

(Cache.php)

<?php

class Cache
{
    protected $_cacheBackend;
   
    /**
     * Constructor
     */
    public function __construct(CacheableInterface $cacheBackend)
    {
        $this->_cacheBackend = $cacheBackend;
    }
   
    /**
     * Save data to the cache
     */
     public function set($key, $data)
     {
         $this->_cacheBackend->set($key, $data);
         return $this;
     }
    
     /**
     * Get data from the cache
     */
     public function get($key)
     {
         return $this->_cacheBackend->get($key);
     }
    
     /**
     * Delete the specified cache data
     */
     public function delete()
     {
         return $this->_cacheBackend->delete($key);
     }         
}


So far, so good. Having built a basic adapter that uses the “Plug-in” pattern to work with the previous cache back-ends (or eventually any other, as long as it implements the “CacheableInterface” interface), the last step that must be taken is set up an example that shows how to utilize this cache system.


{mospagebreak title=Cache Data in RAM with PHP}

Putting all of the pieces together: seeing the previous cache classes in action

If you’re anything like me and want to see how to put the earlier caching system to work, below I created a couple of scripts that will hopefully be of help in this case. Here’s the first one, which uses the “ApcCache” backend to cache in RAM some information about a fictional user:

<?php

// example using the ApcCache class

// include the autoloader
require_once 'Autoloader.php';
Autoloader::getInstance();

// create an instance of the Cache class
$cache = new Cache(new ApcCache);

// save some data in the cache
$cache->set('fname', 'Julie')
      ->set('lname', 'Wilson')
      ->set('email', 'julie@domain.com');

// get the data from the cache
echo ' First Name: ' . $cache->get('fname') .
     ' Last Name: ' . $cache->get('lname') .
     ' Email: ' . $cache->get('email');

To keep things shorter and uncluttered, I omitted the definition of the corresponding autoloader; the above script is self-explanatory, so you shouldn’t have a hard time understanding how it works.

Finally, here’s the second script, which caches the data about our friend Julie Wilson, but this time using the “FileCache” class. Check it out:

<?php

// example using the FileCache class

// include the autoloader
require_once 'Autoloader.php';
Autoloader::getInstance();

// create an instance of the Cache class
$cache = new Cache(new FileCache);

// save some data in the cache
$cache->set('fname', 'Julie')
      ->set('lname', 'Wilson')
      ->set('email', 'julie@domain.com');

// get the data from the cache
echo ' First Name: ' . $cache->get('fname') .
     ' Last Name: ' . $cache->get('lname') .
     ' Email: ' . $cache->get('email');

Mission accomplished. Even when this couple of examples are somewhat basic, they come in handy for demonstrating how the usage of a single segregated interface may be an invaluable resource in the development of an extendable caching system.

And speaking of extending things, there’s plenty of room to enhance the functionality of this sample system. So, if you have some time off and want to do something productive with it, go ahead and add to the system a whole new cache backend. All that you’ll have to do is create an implementer of the “CacheableInterface” interface. It’s that simple, indeed.  

Final thoughts

Over this third episode of the series, I recreated yet another scenario where the use of segregated interfaces can be of great help in the development of truly “pluggable” systems. In this particular case, I appealed to a single interface to create from scratch a simple caching library, but logically it’s feasible to extend this concept to others fields with the same ease.

Considering that I have already at disposal two cache back-ends ready to be put in action, it’d be really a shame not to reuse them. To prevent this from happening, in the coming tutorial I’m going to develop a customizable user model, which not only will make use of the pertinent back-ends, but it’ll utilize a MySQL abstraction class to access the underlying storage mechanism.

Don’t miss the next part! 

Google+ Comments

Google+ Comments