Tuesday, January 18, 2011

CodeIgniter and Caching, a love hate relationship

As I am sure you have realized by now I am a huge fan of CodeIgniter. I find it amazingly easy to work with and feel that it really stays out of my way when I need it to. That being said one of its few weak spots is caching. Lets be clear I am talking about page caching now query caching which has its own short coming but in general can be useful.

Page caching in CodeIgniter is an all or nothing affair. Meaning that you either cache a full page or you don't cache anything. This might have been useful in the 90's when websites were far less interactive than they are today. However in the modern age of web design pages often have dynamic content that changes on a per user basis. For instance showing a users name at the top of a page when they are logged in, or providing an administration option if someone has the proper security clearance. These things would cause a page to look different from one user to the next. This simply isn't possible with the default implementation of caching, the last thing we need is having someone without clearance to be able to know more about our sites administration area.

Many people have written caching libraries for CI and I am sure there are some good ones. All of them lack the shear simplicity of CI's implementation, so here is my stab at it.

The name of this library is called scache for s(imple)cache it has one relatively small library file and a config file. If you would prefer you can combine the config file with CI's config file.

This library lets you cache whatever you need whether that be a full page or a small page segment. You define the unique key used to retrieve the cache file.

The library methods are as follows:


$this->scache->clear($key);
$this->scache->read($key);
$this->scache->write($key);


The clear method should generally be used to clear a cache file if you cannot wait for it to expire normally. Say for example you allow commenting on a page you obviously don't want your users to have to wait several minutes before they can view what they posted. Speaking of expiration, remember that config file I told you about? It only has a single line which defines how long to cache a file in minutes here are its contents:


$config['cache_expiration'] = 1;


Let's see this sucker in action now.


if(!$cache = $this->scache->read($key))
{
    //Build your view data array here
    //
    //
    //
    $cache = $this->load->view('page_segment', $data, true);
    $this->scache->write($key, $cache);
}
$this->load->view('full_page', array('segment'=>$cache));


Somethings to keep in mind looking at this implementation:
  1. The key value can be whatever you want, as long as it is always unique to that particular page segment. The key is how the library will name and find the cache file so it needs to be unique.
  2. In this setup I have a wrapper called "full_page". This file would include things like the main nav the logo the footer etc. Where as "page_segment" is only meant to contain something that will remain static like an 'About Us' paragraph or something.
  3. Remember the data contained in "page_segment" should not change between users or at least not change much.
Below is a link to a zip of both the library and the config file. Please let me know if you have any issues.

http://dl.dropbox.com/u/13081549/Scache.zip

8 comments:

  1. Thanks, this has given me a useful headstart with caching on my website. I couldn't use the default web page caching feature because our site uses a post_controller hook to count page impressions, but I have incorporated your library into the template library I already use, and I think it might work...

    ReplyDelete
  2. Thanks, It works like a charm, exactly what I needed. I wonder can the fread be replaced with file_get_contents ?

    ReplyDelete
    Replies
    1. The expiration for a cache file is written in the file. So doing a file_get_contents would return more than just the file data. A fread is necessary because the first line of the file is read to determine if the cache is stale.

      If not then the rest of the cached file is returned.

      If so then the cache is rebuilt.

      Delete
  3. function read($key)
    {
    $filepath = $this->path.md5($key);

    if ( ! @is_file($filepath))
    return false;

    $cache = file_get_contents($filepath);

    list($time_of_cachefile,$cache) = explode("\n",$cache,2);
    $now = ( isset($_SERVER['REQUEST_TIME']) ) ? $_SERVER['REQUEST_TIME'] : time();

    if ($now < (int) $time_of_cachefile) return $cache;
    @unlink($filepath);

    return false;
    }

    ReplyDelete
    Replies
    1. This solution isn't as efficient as the one already implemented. The reason being that with this solution the cache is read in regardless of if it will be used or not. The cache could be rather large so allocating that much memory for something that will just be deleted seems less than optimal.

      It's much better to do a fread and determine from that if the rest of the cache file should be read or if the cache should be expired. Also additional data can be put in the "cache header" that would be read in.

      Delete
  4. Hi am I missing something? In your code I see

    $size = filesize($filepath);
    if($size > 0)$cache = fread($fp, $size);

    That reads the complete file, after that you check the time and if that is not ok you delete the file, also reading the complete file and deleting it. An improvement could be reading the first line with

    $expire = fgets($fp, 4096);
    fseek($fp, 0)
    fread($fp, $size);

    am I right?

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. Caches been saved in applications folder :( where did i misconfigure?

    ReplyDelete