Simple file-based PHP cache class

Filed under: PHP | 9 Comments »

I needed a simple file-based cache for a PHP project the other day and figured I would share. The production code is slightly different, made into a CodeIgniter library*, but this version is now live up on the Mobile Drudgereport.

class JG_Cache {

    function __construct($dir)
    {
        $this->dir = $dir;
    }

    private function _name($key)
    {
        return sprintf("%s/%s", $this->dir, sha1($key));
    }

    public function get($key, $expiration = 3600)
    {

        if ( !is_dir($this->dir) OR !is_writable($this->dir))
        {
            return FALSE;
        }

        $cache_path = $this->_name($key);

        if (!@file_exists($cache_path))
        {
            return FALSE;
        }

        if (filemtime($cache_path) < (time() - $expiration))
        {
            $this->clear($key);
            return FALSE;
        }

        if (!$fp = @fopen($cache_path, 'rb'))
        {
            return FALSE;
        }

        flock($fp, LOCK_SH);

        $cache = '';

        if (filesize($cache_path) > 0)
        {
            $cache = unserialize(fread($fp, filesize($cache_path)));
        }
        else
        {
            $cache = NULL;
        }

        flock($fp, LOCK_UN);
        fclose($fp);

        return $cache;
    }

    public function set($key, $data)
    {

        if ( !is_dir($this->dir) OR !is_writable($this->dir))
        {
            return FALSE;
        }

        $cache_path = $this->_name($key);

        if ( ! $fp = fopen($cache_path, 'wb'))
        {
            return FALSE;
        }

        if (flock($fp, LOCK_EX))
        {
            fwrite($fp, serialize($data));
            flock($fp, LOCK_UN);
        }
        else
        {
            return FALSE;
        }
        fclose($fp);
        @chmod($cache_path, 0777);
        return TRUE;
    }

    public function clear($key)
    {
        $cache_path = $this->_name($key);

        if (file_exists($cache_path))
        {
            unlink($cache_path);
            return TRUE;
        }

        return FALSE;
    }
}

Usage is dead simple, initiate the class with a path to your cache location and then use JG_Cache::get() and JG_Cache::set() to play with your data. Each cache has a key, which you set. If you’re caching something with user data, make sure to put their unique ID in it. This is Memcached style, really basic. Cache data defaults to last an hour, but that’s flexible in the get() method (the second argument is the number of seconds you want the data to stick around).

$cache = new JG_Cache('/path/to/cache');

$data = $cache->get('key');

if ($data === FALSE)
{
    $data = 'This will be cached';
    $cache->set('key', $data);
}

//Do something with $data

While we’re using a string here, the class uses serialize() and unserialize(), so you can cache whatever PHP data types you want. Objects are especially handy, at least for what I needed to use it for. The cache files themselves are stored as a hash of the key name so you don’t have to worry about file naming rules.

It’s designed to fail silently so your code should always be able to access the data without the cache. The beauty of it is there are only a few extra lines to implement the caching and the rest of your code can remain the same.

* CodeIgniter provides a caching class, but its usefulness is marginal because it can only cache entire [rendered] page output which means you can’t have personalized pages. I needed something more granular, to be able to cache a remote XML feed for example.

Latest posts

9 Responses to “Simple file-based PHP cache class”

  1. FranL says:

    Hi!

    Thank you for sharing your cache class. I would like to use it in a script licensed under the GNU GPL.
    What license applies to your code?

    Thanks.

  2. Sam Dalton says:

    Excellent! Exactly what I needed, thanks!

  3. Sergey says:

    Thank you for the sharing!
    I’ve downloaded the class and now using it in my current project.
    Overall it seems great, but this feature I’ve commented out in my copy of the source:
    if (filemtime($cache_path) clear($key); */
    return FALSE;
    }
    I just don’t get WHY the cache should clear a value which is expired for PARTICULAR CALLER?
    Let’s consider a dialog:
    - Customer A
    Hey, Cache! Bring me the value of this key, but take into account – I don’t those stale values older than an hour, ok?
    - Cach
    Here it is, sir!

    - Customer A
    Thanks, Cachie! Bye!

    - Customer B
    Bring me the value of this key! But don’t bother to give anything older than five minutes! Only freshest things would fit my taste!

    - Cache
    I found the value, but I’m sorry, it was ten minutes old. So I knew you don’t need it, and I wiped it out.

    - Customer C
    Cache! Give me the value of this key. You know me, I don’t show up frequently here. Anything fresher than twelve hours would be kosher enough…

    - Cache
    Uhhh! Oooops! Mister B was here 5 minutes earlier…

    - Customer C
    Soooo?!..

    - Cache
    It was too stale for him…

    - Customer C
    and…

    - Cach
    I have deleted it!…
    (Both crying…Act drop)

    • My requirements didn’t include variable cache expiration for individual buckets. I suppose that could be handy, but in practice for how this class works it ends up being the same thing. If Customer B requests something new and doesn’t get it in my example it will go ahead and re-generate it and copy over the cache. Customer C will go ahead and get the data that B generated. The net result is the same as if you kept the old cache around since it would have been re-written. I suppose it’s not necessary to clear it if it will be re-written anyway, but that’s a different debate.

      I was simply trying to cache some API calls so they were only hit a max of once per hour. Nothing fancy required :) .

  4. Sergey says:

    Anyway, this is a great peace of code, and my fiction story depicts just ONE possible scenario. It was just kinda joke, so never *really* mind ;)
    Cheers!

  5. Shimon Crown says:

    Just the thing I need for my mashup. Thanks.

  6. simon says:

    Hi

    thanks for the code

    I’m no code genius but does this lock cached files or serve an already generated page at page expiration.?

    thanks

  7. Farhad Khan says:

    Really appreciate you publishing this piece of code. Very handy. However I agree with Sergey on his comment about removing the clear() step. Consider the following scenario:

    Client A: Triggers clear();
    .. in the mean time .. Client B: Triggers get();
    Client A: Triggers set();

    There is a very good chance for client B to not get any data at all, since the file has been cleared.

    Thanks.

Leave a Reply