The Spotless Developer Blog

Older Posts »« Newer Posts

Output Caching With PHP

By Dustin Hendricks - March 6th, 2011

We all know that PHP is not always fast, but achieving good performance is possible through the use of various forms of caching. Today I would like to talk about output caching. Output caching involves saving a page's output to a file, so that the next time someone requests that file, we will simply output the saved cache file instead of re-processing that entire page. To go even further with this idea, you would want to cache individual blocks of that page instead of the entire page. Each cache file would be used for a specified length of time, and then regenerated after that time has expired. This is a great way to speed up code especially if you have pages that make database calls, and do not necessarily need to be updated each and every time the page is loaded.

To perform our output caching, we need to become familiar with the ob_start() and ob_get_flush() functions. ob_start() will begin buffering anything that we output to the page, and ob_get_flush() will give us the contents of the output buffer as well as flush the buffer.

My output caching class includes begin and end caching methods. The begin caching method looks like this.

<?
// begin output caching
public function beginCache($cache_id$expire_duration 86400) {

    
// if a cache file has already begun buffering, throw an error
    
if ($this->currentOutputFile) throw new Exception('You must end your previous caching before beginning a new one.'E_USER_ERROR);

    
$this->currentOutputFile 'application/cache/' crc32($cache_id);
    
$this->expireDuration $expire_duration;
    
    
// if cache file exists
    
if (is_readable($this->currentOutputFile)) {
        if (!
$this->cacheExpired()) {
            
// output it
            
echo file_get_contents($this->currentOutputFile);
            return 
false;
        }
    }
    
    
ob_start();
    return 
true;
}

private function 
cacheExpired() {
    return 
time() - filectime($this->currentOutputFile) > $this->expireDuration;
}
?>

You can see that the method has two arguments. The first argument gives the cache file a unique name, while the second argument gives the cache file an expiration date. I use crc32 hashing on the cache id in order to give it a fixed length, and make it file name safe. In larger programs you may need to use a slower hashing function in order to prevent collision. The cache id input can be a concatenation of the request path, the particular page block name, and whatever other variables make that output block unique. Whatever the cache id is, it must be the same each time that particular output block is requested.

If the cache file already exists, and it was last modified longer ago than the set expire duration, then we will create a new cache file, otherwise we will simply output the cache file and return true. The return value lets us know whether we need to process and output the page or page block as normal, or if we can skip all of that. If we do need to process the page as normal, we will start output buffering with the ob_start() function so that we can save a new cache file.

Now at the end of the page or page block that we are caching, we need to use a method that will save our cache file, which should look something like this.

<?
// end output caching
public static function endCache() {

    
// if output buffer has not yet been started, throw and error
    
if (!$this->currentOutputFile) throw new Exception('You must begin a cache buffer before you can end it.'E_USER_ERROR);
    
    
// create buffer file
    
if ($this->cacheExpired()) {
        
$file fopen($this->currentOutputFile'c');
        if (
flock($fileLOCK_EX)) {
            
ftruncate($file0);
            
fwrite($fileob_get_flush());
        } else {
            
ob_flush();
        }
        
fclose($file);
    }

    
    
// unset buffer file name
    
$this->currentOutputFile null;
}
?>

This will save the buffer into a cache file, and unset the current cache file's name. You will want to encapsulate these methods into a class which we will call OutputCache. The implementation of this code may seem strange at first, but you will see that it is necessary. If you want to cache multiple blocks on one page, implementation looks something like this.

<?
$output_cache 
= new OutputCache();
if (
$output_cache->beginCache($unique_name_for_this_page_output)) {
    
// process and output page or page block as normal
    
    
$output_cache->endCache();
}
?>

So if we are beginning caching (no usable cache file exists already), process and output the page block as normal, otherwise, skip the processing and output for that page block, and simply output the contents of the cache file.

If you are simply caching an entire page, implimentation is a bit simpler.

<?
$output_cache 
= new OutputCache();
if (!
$output_cache->begin_cache($unique_name_for_this_page_output)) exit;
// process and output page as normal
    
$output_cache->endCache();
?>

This implementation does not require your page processing and output to be wrapped in a conditional.

The final class may look something like this.

<?
// output caching library
class OutputCache {
    
    protected 
$currentOutputFile;
    protected 
$expireDuration;

    
// begin output caching
    
public function beginCache($cache_id$expire_duration 86400) {
    
        
// if a cache file has already begun buffering, throw an error
        
if ($this->currentOutputFiletrigger_error('You must end your previous caching before beginning a new one.'E_USER_ERROR);
    
        
$this->currentOutputFile __DIR__ '/OutputCache/cache/' crc32($cache_id);
        
$this->expireDuration $expire_duration;
        
        
// if cache file exists
        
if (is_readable($this->currentOutputFile)) {
            if (!
$this->cacheExpired()) {
                
// output it
                
echo file_get_contents($this->currentOutputFile);
                return 
false;
            }
        }
        
        
ob_start();
        return 
true;
    }
    
    
// end output caching
    
public static function endCache() {
    
        
// if output buffer has not yet been started, throw and error
        
if (!$this->currentOutputFiletrigger_error('You must begin a cache buffer before you can end it.'E_USER_ERROR);

        
// create buffer file
        
if ($this->cacheExpired()) {
            
$file fopen($this->currentOutputFile'c');
            if (
flock($fileLOCK_EX)) {
                
ftruncate($file0);
                
fwrite($fileob_get_flush());
            } else {
                
ob_flush();
            }
            
fclose($file);
        }
        
        
// unset buffer file name
        
$this->currentOutputFile null;
    }
    
    private function 
cacheExpired() {
        return 
time() - filectime($this->currentOutputFile) > $this->expireDuration;
    }
}
?>

Tags: #php #cache #caching #output-caching #output-buffer

Older Posts »« Newer Posts