PHP Anthology, Volume 2: Applications Chapter 5: Caching

PHP Anthology, Volume 2: Applications. Chapter 5: Caching

Chunked Buffering

A simplistic approach to output buffering is to cache an entire page. However, this approach forfeits the real opportunities presented by PHP’s output control functions to improve your site’s performance in a manner that’s relevant to the varying lifetimes of your content.

No doubt, some parts of the page you send to visitors change very rarely, such as the page’s header, menus and footer. But other parts, such as the table containing a forum discussion, may change quite often. Output buffering can be used to cache sections of a page in separate files, then rebuild the page from these—a solution that eliminates the need to repeat database queries, while loops, and so on. You might consider assigning each block of the page an expiry date after which the cache file is recreated, or alternatively, you may build into your application a mechanism that deletes the cache file every time the content it stores is changed.

Here’s an example that demonstrates the principle:

Example 5.3. 3.php (excerpt)
<?php
/**
 * Writes a cache file
 * @param string contents of the buffer
 * @param string filename to use when creating cache file
 * @return void
 */
function writeCache($content, $filename)
{
  $fp = fopen('./cache/' . $filename, 'w');
  fwrite($fp, $content);
  fclose($fp);
}
/**
 * Checks for cache files
 * @param string filename of cache file to check for
 * @param int maximum age of the file in seconds
 * @return mixed either the contents of the cache or false
 */
function readCache($filename, $expiry)
{
  if (file_exists('./cache/' . $filename)) {
    if ((time() - $expiry) > filemtime('./cache/' . $filename)) {
      return FALSE;
    }
    $cache = file('./cache/' . $filename);
    return implode('', $cache);
  }
  return FALSE;
}

The first two functions we’ve defined, writeCache and readCache, are used to create cache files and check for their existence, respectively. The writeCache function takes rendered output as its first argument, as well as a filename that should be used when creating the cache file. The readCache function takes a filename of a cache file as its first argument, along with the time in seconds after which the cache file should be regarded as having expired. If it finds a valid cache file, the script will return it; otherwise it returns FALSE to instruct the calling file that either no cache file exists, or it’s out of date.

For the purposes of this example, I used a procedural approach. However, I wouldn’t recommend doing this in practice, as it will result in very messy code (see later solutions for better alternatives) and is likely to cause issues with file locking (e.g. what happens when someone accesses the cache at the exact moment it’s being updated?).

Let’s continue this example. After the output buffer is started, processing begins. First, the script calls readCache to see whether the file 3_header.cache exists; this contains the top of the page—the HTML head section and the start of the body. We’ve used PHP’s date function to display the time at which the page was actually rendered, so you’ll be able to see the different cache files at work when the page is displayed.

Example 5.4. 3.php (excerpt)
// Start buffering the output
ob_start();
// Handle the page header
if (!$header = readCache('3_header.cache', 604800)) {
  // Display the header
  ?>
  <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  <html xmlns="http://www.w3.org/1999/xhtml">
  <head>
  <title> Chunked Cached Page </title>
  <meta http-equiv="Content-Type"
    content="text/html; charset=iso-8859-1" />
  </head>
  <body>
  The header time is now: <?php echo date('H:i:s'); ?><br />
  <?php
  $header = ob_get_contents();
  ob_clean();
  writeCache($header,'3_header.cache');
}

Note what happens when a cache file isn’t found. Some content is output and assigned to a variable with ob_get_contents , after which the ob_clean function empties the buffer. This allows us to capture the output in “chunks” and assign it to individual cache files with writeCache. The header of the page is now stored as a file, which can be reused without our needing to re-render the page. Look back to the start of the if condition for a moment. When we called readCache, we gave it an expiry time of 604800 seconds (one week); readCache uses the file modification time of the cache file to determine whether the cache is still valid.

For the body of the page, we’ll use the same process as before. However, this time, when we call readCache, we’ll use an expiry time of five seconds; the cache file will be updated whenever it’s more than five seconds old:

Example 5.5. 3.php (excerpt)

// Handle body of the page
if (!$body = readCache('3_body.cache', 5)) {
  echo 'The body time is now: ' . date('H:i:s') . '<br />';
  $body = ob_get_contents();
  ob_clean();
  writeCache($body, '3_body.cache');
}

The page footer is effectively the same as the header. After this, the output buffering is stopped and the content of the three variables that hold the page data is displayed:

Example 5.6. 3.php (excerpt)

// Handle the footer of the page
if (!$footer = readCache('3_footer.cache', 604800)) {
  ?>
  The footer time is now: <?php echo date('H:i:s'); ?><br />
  </body>
  </html>
  <?php
  $footer = ob_get_contents();
  ob_clean();
  writeCache($footer, '3_footer.cache');
}
// Stop buffering
ob_end_clean();
// Display the contents of the page
echo $header . $body . $footer;
?>

The end result looks like this:

The header time is now: 17:10:42
The body time is now: 18:07:40
The footer time is now: 17:10:42

The header and footer are updated on a weekly basis, while the body is updated whenever it is more than five seconds old.

The diagram in Figure 5.1 summarizes the chunked buffering methodology.

Figure 5.1. Chunked Buffering Flow Diagram

Chunked Buffering Flow Diagram

Nesting Buffers

You can nest one buffer within another practically ad infinitum simply by calling ob_start more than once. This can be useful if you have multiple operations that use the output buffer, such as one that catches the PHP error messages, and another that deals with caching. Care needs to be taken to make sure that ob_end_flush or ob_end_clean is called every time ob_start is used.

Created: March 27, 2003
Revised: January 2, 2004

URL: http://webreference.com/programming/phpanth3