ONLamp.com    
 Published on ONLamp.com (http://www.onlamp.com/)
 See this if you're having trouble printing code examples


Caching PHP Programs with PEAR

by Sebastian Bergmann
10/11/2001

Contents:

Caching in context

Caching is currently a hot topic in the PHP world. Because PHP produces dynamic web pages, scripts must be run and results must be calculated each time a web page is requested, regardless if the results are the same each time. In addition, PHP compiles the script every time it is requested. This overhead can seriously slow down a site with heavy traffic. Fortunately, the results of a web request can be stored, or cached, and presented to matching requests without having to re-run or recompile the scripts. Commercial products like ZendCache or open-source solutions such as Alternate PHP Cache provide a means to cache the compiled version of a PHP script -- the byte-code.

While these "PHP land" solutions scratch an itch in PHP's design, "Userland" solutions can go a step further and address general bottlenecks in Web application design and programming. (The term PHP Land refers to the language level of PHP, for instance, the Zend Engine, that drives PHP 4. The term userland refers to something that is written by users of PHP.)

Imagine a commerce application with a large catalog stored in a database. It is realistic to assume that the catalog information will change only at specific times, such as once or twice a day. Still, for every request to the product's page, a database query is performed. This overhead could be easily avoided by caching either the query's result or the complete HTML output of the requested page.

PEAR's Cache package offers a framework for the caching of dynamic content, database queries, and PHP function calls.

Where to get PEAR Cache

Perl has CPAN, and TeX has CTAN. But PHP also has a central repository for classes, libraries, and modules. It's called PEAR, which stands for PHP Extension and Add-On Repository. You can read all about PEAR in OnLAMP.com's recent articles An Introduction to PEAR and A Detailed Look at PEAR.

For this article, I'll assume that you already have a PEAR environment set up. The examples in this article have been developed and tested with a development version of PEAR Cache, available either via CVS or as a snapshot here.

How PEAR Cache works

The PEAR Cache package consists of a generic Cache class and several specialized subclasses -- for example, a class to cache function calls or a script's output. The Cache class can use a variety of so-called Container classes that actually store and manage the cached data.

Following is a list of PEAR Cache's current container implementations along with their respective parameters

The performance gain from the use of PEAR Cache greatly depends on your choice for the cache container to be used. For instance, it makes obviously no sense to store the result of a database query again into a database.

Function call caching

PEAR Cache's Function Cache module caches the output and result of any function or method, no matter if they are built-in PHP functions or user-defined ones. By default, it uses the File container and puts the cache data into a directory named function_cache.

The Cache_Function class's constructor accepts up to three parameters, all three being optional:

A cached function call is triggered by wrapping the normal function call using the Cache_Function class's call() method. Using call() is quite easy. Its first parameter gives the name of the function (or method) to call, followed by the parameters of the function (or method) to be called. The second parameter of call() is the first one of the function (or method) to be called, and so on. Let's have a look at an example:

Example 1: Caching function and method calls

<.;?php
// Load PEAR Cache's Function Cache

< 'Cache/Function.php';

// Define some classes and functions / methods
// for demonstration

class foo {
  function bar($test) {
    echo "foo::bar($test)<br>";
  }
}

class bar {
  function foobar($object) {
    echo '$'.$object.'->foobar('.$object.')<br>';
  }
}

$bar = new bar;

function foobar() {
  echo 'foobar()';
}

// Get Cache_Function object

$cache = new Cache_Function();

// Cached call to static method bar() of class foo
// (foo::bar())

$cache->call('foo::bar', 'test');

// Cached call to method foobar() of object bar
// ($bar->foobar())

$cache->call('bar->foobar', 'bar');

// Cached call to function foobar()

$cache->call('foobar');
?>

Caching of function calls comes in handy in a variety of situations such as time-consuming XSL transformations of XML sources that only change on a daily basis.

Output caching

Looking back at the introductory example of a commerce application, you're now able to cache parsed template elements, or catalog information you used to pull from the database on each request.

We now go a step further and cache a script's complete output with the Cache_Output class.

Example 2: Caching a script's output

<?php
// Load PEAR Cache's Output Cache

require_once 'Cache/Output.php';

$cache = new Cache_Output('file', array('cache_dir' => '.') );

// Compute unique cache identifier for the page we're about
// to cache. We'll assume that the page's output depends on
// the URL, HTTP GET and POST variables and cookies.

$cache_id = $cache->generateID(array('url' => $REQUEST_URI, 'post' => $HTTP_POST_VARS, 'cookies' => $HTTP_COOKIE_VARS) );

// Query the cache

if ($content = $cache->start($cache_id)) {
// Cache Hit

echo $content;
die();
}

// Cache Miss

// -- content producing code here --

// Store page into cache

echo $cache->end();
?>

With the Cache_Output class, it is easily possible to turn a dynamic, database-driven web application into a static one. This can drastically improve a site's performance.

Related Reading

PHP Pocket ReferencePHP Pocket Reference
By Rasmus Lerdorf
Table of Contents
Sample Section
Full Description
Read Online -- Safari

More and more web sites are using GZIP compression for their HTML content. This reduces the server's bandwidth, and thus the costs for the generated traffic. Furthermore, it increases the user experience for those using modem connections. Cache_OutputCompression extends the functionality of the Cache_Output class, as it caches the GZIP compressed version of the generated HTML to save the CPU time needed to compress the content.

Customized solutions

In this last section, I explain how the PEAR Cache framework can be used to develop customized caching solutions. As an example, I have chosen a class called MySQL_Query_Cache that caches the result sets of SELECT queries.

Let's start with the class's variables -- constructor and destructor. The constructor is used, as before with the Cache_Function and Cache_Output classes, to transport the cache container options. The destructor closes the MySQL connection and runs the cache's garbage collection, if needed.

<?php
require_once 'Cache.php';

class MySQL_Query_Cache extends Cache {
  var $connection = null;
  var $expires    = 3600;

  var $cursor = 0;
  var $result = array();

  function MySQL_Query_Cache($container  = 'file', 
      $container_options = array('cache_dir'=> '.', 
      'filename_prefix' => 'cache_'), $expires = 3600)
  {
    $this->Cache($container, $container_options);
    $this->expires = $expires;      
  }

  function _MySQL_Query_Cache() {
      if (is_resource($this->connection)) {
        mysql_close($this->connection);
      }

      $this->_Cache();
  }
}
?>

Before we come to the juicy part, where we actually perform and cache the query, we need some more helper functions.

<?php
function connect($hostname, $username, $password, $database) {
  $this->connection = mysql_connect($hostname, $username, $password) or trigger_error('Could not connect to database.', E_USER_ERROR);

  mysql_select_db($database, $this->connection) or trigger_error('Could not select database.', E_USER_ERROR);
}

function fetch_row() {
  if ($this->cursor < sizeof($this->result)) {
    return $this->result[$this->cursor++];
  } else {
    return false;
  }
}

function num_rows() {
  return sizeof($this->result);
}
?>

We already have ready the functionality needed to connect to a MySQL database, to fetch a row from a cached result set, and to get the number of rows in the current set. Let's see how we perform -- and cache -- a database query:

<?php
function query($query) {
  if (stristr($query, 'SELECT')) {
    // Compute unique cache identifier for this query

    $cache_id = md5($query);

    // Query the cache

    $this->result = $this->get($cache_id, 'mysql_query_cache');

    if ($this->result == NULL) {
      // Cache Miss

      $this->cursor = 0;
      $this->result = array();

      if (is_resource($this->connection)) {
        // Use mysql_unbuffered_query(), if available

        if (function_exists('mysql_unbuffered_query')) {$result = mysql_unbuffered_query($query, $this->connection);
        } else {$result = mysql_query($query, $this->connection);
        }

        // Fetch all result rows

        while ($row = mysql_fetch_assoc($result)) {$this->result[] = $row;
        }

        // Free MySQL Result Resource

        mysql_free_result($result);

        // Store result set in cache

        $this->save($cache_id, $this->result, $this->expires, 'mysql_query_cache');
      }
    }
  } else {
    // No SELECT query, don't cache it

    return mysql_query($query, $this->connection);
  }
}
?>

Example 3: The MySQL query cache in action

<?php
require_once 'MySQL_Query_Cache.php';

$cache = new MySQL_Query_Cache();
$cache->connect('hostname', 'username', 'password', 'database');
$cache->query('select * from table');

while ($row = $cache->fetch_row()) {
  echo '<p>';
  print_r($row);
  echo '</p>';
}
?>

With this information, you should be able to get started caching PHP web pages for most common applications.

Sebastian Bergmann is the author of a variety of PHP software projects such as PHPUnit and phpOpenTracker.


Return to the PHP DevCenter.

Copyright © 2009 O'Reilly Media, Inc.