Simple PHP Caching Using Output Buffering

I’ve worked on quite a few PHP projects recently, and all of them have required some form of caching. From working with each, I’ve come up with a pretty efficient method for caching code using PHP’s output buffering. It ends up being really quick and super flexible.

Output Buffering Basics

Output buffering is a pretty simple concept: instead of letting PHP return data to your user’s browser, you capture it and store it in a “buffer,” and you can decide what to do with it. Here’s a simple example:

ob_start();
echo "Hello!  This is buffered.";
$buffer = ob_get_clean();

Let’s go through the code line-by-line. The first line calls ob_start() (docs), which starts output buffering. The next line normally would be sent to the browser. Instead, since I called ob_start(), it gets stored in our buffer. The third line takes the current buffer and assigns it to the $buffer variable and stops the current buffer, all using the ob_get_clean() function (docs). It’s really simple stuff, and it becomes very powerful when used correctly.

How Caching Will Work

For this post, I’m going to be caching a simple API, and the general process will work like this:

  1. A user makes an API call, something like http://mysite.com/api/?method=myapp.search&type=people&query=Kyle
  2. If a cache file exists for the call, and it is younger than 15 minutes, skip to #6.
  3. Start buffering PHP’s output.
  4. Run the code to process the request.
  5. Save the contents of the buffer to a file, with a unique filename.
  6. Return result to the user.

Where Cached Output Will Be Saved

To save the output, I’m going to be creating a file for each unique request. For this application, the request will be unique based on the GET parameters passed. To do this, I’ll be creating an MD5 hash of an alphabetical list of GET keys and values. Here’s the function:

function cache_key() {
  $keys = array();
  foreach($_GET as $key => $value) {
    $keys[] = $key . "=" . $value;
  }
  sort($keys);
  return md5(implode('&', $keys));
}

function cache_filename() {
  globals $cache_dir;
  return $cache_dir . '/' . cache_key() . '.cache';
}

Please note that this will have to be customized based on what exactly you’re caching. For instance, if you’re caching individual pages, you may want to create the key using the path to the page. Whatever it is you’re using, just make sure it is unique and consistent for each page.

Checking The Cache

When a request is made, it’s necessary to first check to see if it has already been cached, and, if it has, whether the cache hasn’t expired. I’ll be using the filesystem to achieve this:

$cache_time = 15*60; // 15 minutes in seconds

function cache_exists() {
  globals $cache_time;

  if(@file_exists(cache_filename()) && time() - $cache_time < @filemtime(cache_filename())) {
    return true;
  } else {
    return false;
  }
}

So what exactly is going on here? If you take a close look, we’re using the file_exists() (docs) and filemtime() (docs) functions to see if the cache file already exists and, if it does, whether it’s recent enough to serve (in this case, if it’s less than 15 minutes old, the function returns true). I’m placing @ signs before these two functions so that, if they fail, it doesn’t return an error. Instead, the function will just return false and the code will run as if no cache file exists.

Putting It All Together

Now, it’s time to get everything working together. First, a couple of necessary functions for saving and reading the cache:

function read_cache() {
  return file_get_contents(cache_filename());
}

function save_cache($value) {
  $fp = @fopen(cache_filename(), 'w');
  @fwrite($fp, $value);
  @fclose($fp);
}

Now, a few calls to wrap around your code:

function start_cache() {
  if(cache_exists()) {
    echo read_cache();
    exit();
  } else {
    ob_start();
  }
}

function stop_cache() {
  $data = ob_get_clean();
  save_cache($data);
  echo $data;
}

And to implement it, this is all you need to do:

start_cache();
// Your code that needs to be cached
stop_cache();

And you’re done! All-in-all, it’s a very simple way to achieve a very powerful result.

Drawbacks to This Method

The first thing you want to keep in mind when using this caching method is that it caches the entire page. This can be good: if everyone visiting the page sees the same content anyways, why not cache it for everyone? However, if you’re serving a page that appears different to different users, it can be a bad idea. For instance, what if an administrator visits the page, and it gets cached? When the next non-administrator visits, they’re going to see all the administration information. Bad news.

Also, due to the simplicity of this method, there’s no way to easily expire the cache of a single page. Let’s go back to the blog entry example. If you decide to make a change to the entry, you’ll have to wait at least 15 minutes before the cache is cleared, or you have to go in and delete all the cache files (since it’s difficult to determine which file goes with which page). For many applications, this probably won’t an issue, but it’s something to keep in mind.

Download the Source

Hopefully, this was helpful. If you’d like to download the entire source, you can grab it here.

Posted on September 16, 2008
Tagged with: , , , , , ,

11 Comments

Avatar

Colin Devroe 17 Sep 2008 at 8:34AM

Excellent demonstration and walk-through of what seems to be a good solution. I think I’ll use this for a few of my projects soon.

Avatar

Andrew Smith 22 Sep 2008 at 11:13PM

Hey, I found a tiny (but crucial) bug in your tuorial. It’s in this function:

function stop_cache() {
  $data = ob_get_clean();
  save_cache($data);
  echo $data;
}

When you call this function at the end of the script, it will always save the current data to the cache, regardless of whether the data is new. This effectively re-saves the cache every time the page loads.

To work around this, I made a variable that keeps track of whether or not new data has been requested. If there is new data, then I don’t save anything to the cache.

Hope this helps, and thanks for making this tutorial! Output buffering is incredibly useful.

Avatar

Andrew Smith 22 Sep 2008 at 11:17PM

Sorry for double-posting, but I have two things to add:

  1. The “Code” tag I put in my last comment got eaten, that’s why the function is on one line.
  2. I meant to say “If there is no new data, then I don’t save anything to the cache.” My bad :)
Avatar

Kyle 23 Sep 2008 at 12:02AM

Andrew: Actually, in the start_cache() function, if the cache exists, it prints it to the screen and calls the exit() function, which ends execution of the script, so it never gets to the stop_cache() line.

Also, I cleaned up your formatting in the comments a bit, sorry about the confusion! I definitely need to add info about how to mark up comments.

Thanks for the input, and I’m glad the script’s working out for you!

Avatar

Andrew Smith 23 Sep 2008 at 11:34PM

Kyle: Thanks! That line is pretty crucial, maybe you could prevent other people from making the same mistake as I did if you emphasized the importance of “exit()” somewhere in the post?

Oh, also, if your project is in PHP5, file_put_contents() is a really handy function you can use in place of fopen(), fwrite(), and fclose().

Avatar

Kyle 23 Sep 2008 at 11:42PM

Andrew: Definitely, I’ll update the post when I get a chance to explain the start_cache() and stop_cache() functions a bit more.

Good point about file_put_contents(), for some reason I never even considered using that, but it’s definitely a much cleaner method to write to a file.

Avatar

owen 26 Nov 2008 at 10:45AM

you could (or in some later article) make start_cache() a boolean function so you could have a IF block like:

if( ! start_cache() ) {
  stop_cache();
}

or something like that so you have more control over what is cached.

Avatar

eLouai 06 Jan 2009 at 7:18PM

Nicely done!

I used it for my proxy to retrieve my xml weather feed. Also credited this page under references.

See: http://weatherdoll.com/google-weather-api-flash.php

Avatar

Kyle 06 Jan 2009 at 7:58PM

eLouai: Glad it helped you!

Avatar

Lutsen 25 Mar 2009 at 7:32PM

Nice piece of code!

If the data you want to cache is a single string (for example the output of a function) you can combine the start_cache() and stop_cache() function in one function, like this:

function do_cache($string_to_cache) {
    if(cache_exists()) {
        echo read_cache();
    } else {
        save_cache($string_to_cache);
        echo $string_to_cache;
    }
}

This way you don’t need the exit() command in the start_cache() function and the code further down your php page gets executed as well.

Avatar

Kyle 26 Mar 2009 at 5:37AM

Thanks for the tip, Lutsen!

Leave A Comment

Ajax-loader