Wednesday, February 3, 2010

PHP Cache Class

While Kohana has an internal caching function that works quite well, I wanted a bit more robust handling. Namely, I wanted to be able to set a cache file that never expired - something Kohana 3's cache function doesn't handle. Also, I found it awkward to have to pass the lifespan of the cache into read function. I found some sample code where you can play with the touch() function to set an m time in the future. When the m and c times are equal, we have an indefinite cache file

There are only a few situations where you'd want an indefinite cache, and it leaves handling expiration up to the function doing the caching. In my current case, this is reading ATOM feeds from Picasa. The feed headers allow the feed to be queried to see if the feed has been updated.

  • Attempt to get cached feed to get the etag field.
  • Retrieve the feed using the If-None-Match: header
  • Evaluate response:
    • If response is HTTP 304 - content is not modified, use cached feed. In this case the entire feed is not sent back in the response.
    • If response is HTTP 200 - content is modified, take feed and update cache

Code

Much of this is based on Kohana's caching function with some file locking tossed in.
This is an early version - not tested much, beware!
/**
 * Cache helper
 *
 * @package    Cache
 * @author     Levi Baker
 */
class Cache
{
    /**
     * @var directory permissions for the cache directories.
     */
    private static $cache_dir_permissions = 0700;

    /**
     * Cache a resource
     * @param string $name - resource name
     * @param mixed $data - data to be serialized and cached.
     * @param int $lifetime - ttl of the data in seconds.
     */
    public static function write($name, $data, $lifetime = null)
    {
        $file = self::name($name);
        $dir = self::dir($name);
    
        try
        {
            // Ensure that the directory has been created.
            if ( ! is_dir($dir))
            {
                // Create the cache directory
                mkdir($dir, self::$cache_dir_permissions, TRUE);
    
                // Set permissions (must be manually set to fix umask issues)
                chmod($dir, self::$cache_dir_permissions);
            }
    
            // Open the file.
            $f = fopen($dir.$file, 'w');
            
            // Get an exlusive lock on the file while writing
            flock($f, LOCK_EX);
            
            // Write to cach
            if (fwrite($f, serialize($data)) === false)
                throw new Exception('Could not write to cache file.');
            
            fclose($f);    // close and release the lock.
            
            
            // Set lifetime
            if ( ! touch($dir.$file, time() + (int) $lifetime))
                throw new Exception('Could not touch cache file');
                
            return true;
        }
        catch (Exception $e)
        {
            throw $e;
        }
    }
    
    /**
     * Read from a cache resource
     * @param string $name - resource name
     * @return mixed - unserialized resource, or null if not found.
     */
    public static function read($name)
    {
        try
        {
            $path = self::dir($name).self::name($name);

            if (is_file($path))
            {
                // Check if a lifetime was set.
                if (filemtime($path) > filectime($path))
                {
                    // cache file is expired.
                    if (time() > filemtime($path))
                    {
                        unlink ($path);
                        return null;
                    }
                }
                
                // We need a file pointer to do a lock
                $f = fopen($path,'r');

                if (!$f)
                    return null;
                
                // Get a shared lock
                flock($f, LOCK_SH);
                
                $data = file_get_contents($path);
                fclose($f);
                
                return unserialize($data);
            }
            
            // Cache not found.
            return null;
        }
        catch (Exception $e)
        {
            throw $e;
        }
    }
    
    /**
     * Delete a cached resource
     * @param string $name - resource name
     */
    public static function delete($name)
    {
        $path = self::dir($name).self::name($name);

        if (is_file($path))
        {
            return unlink ($path);
        }
        return false;
    }
    
    /**
     * Name - returns the name of the cache file
     * @param string $name
     */
    private static function name($name)
    {
        return sha1($name).'.txt';
    }
    
    /**
     * Directory - returns the full path of the cache file
     * @param string $name
     */
    private static function dir($name)
    {
        $file = self::name($name);
        return Kohana::$cache_dir.DIRECTORY_SEPARATOR.$file[0].$file[1].DIRECTORY_SEPARATOR;
    }
}

While this is setup to run with Kohana 3, it could quite easily be made standalone by modifying the function dir($name) to set the cache directory elsewhere.

Using this class is pretty simple:

// Write to a cache with no expiration
cache::write('resource.name', $my_cool_data);

// Write to a cache with a lifespan of 5 minutes
cache::write('resource.expires', $temporary_data, 300);

// Read data
$data = cache::read('resource.name');

// Check that the resource didn't expire though...
if ($data !== null)
{
    // Do things!
}

Notes

This uses serialize on the data coming in, so be sure that the data being saved is serializable. Some php classes have issues with serialization. Look into the __sleep() and __wakeup() magic functions to extend and enable serialization on objects.

No comments:

Post a Comment