AttachmentSize
globalstaticcache.zip381.95 KB

This blog post introduces a simple global static caching mechanism for PHP. But first, some background. One of the joys of developing this website, the Teradata Developer Exchange, has been building the site on Drupal, a PHP-based Content Management System. Having spent the last decade building heavyweight J2EE apps like Viewpoint, I was curious to see what life was like in the fast-and-loose world of PHP development.

Once you get your head around both PHP and Drupal, you can - as advertised - build webapps very quickly indeed. As a J2EE developer, I was well aware of the criticisms of PHP, which basically boil down to this: PHP doesn't force you to do things a certain way, therefore it's hard to enforce good design (think "use design patterns"), resulting in horrible spaghetti code.

There's certainly some validity to that argument, and I've seen some ghastly PHP code. Anyway, after the initial beta launch of DevX, it was time for a bit of a code/project review/refactor, and so I started with profiling the site, using the Performance Logging component of the Drupal Devel module. It was immediately obvious from the 720 SQL queries it was taking to render the home page (including one function that was being called with the same arguments 22 times) that I needed to do a little caching. In the Drupal/PHP world there's two key forms of caching (from the developer's perspective): static caching, and the Cache API.

The Cache API is a Drupal component that you typically use to cache chunks of your rendered HTML page so that you don't have to re-build that HTML on each page request. Under some circumstances you might even cache an entire page, obviously saving yourself a lot of processing. However, that's a discussion for another day, as we first need to look at static caching.

Firstly, static caching is not Drupal-specific, but is a general PHP construct. Basically, it works as follows. On execution of PHP script, it is possible to declare a variable in a function as static. Next time that function is called (in the same execution of the PHP script), the static variable will have retained its value. A simple example of this is a counter variable that you increment each time the function is called. Another use is to store the result of a calculation so you don't have to do it again: i.e. caching. What we're eventually going to arrive at here is a pattern (and implementation of that pattern) for performing global static caching in PHP.

First cut

Let's start with a simple case. These example code snippets show usage of the Drupal API, but remember that this mechanism can apply to any PHP code. So, let's say you've got a function that returns all the tags associated with a content node (e.g. for this blog post, the tags might be "cache", "drupal", etc.). Here's how you might initially write the function:

/**
 * Get the tags associated with a given node
 *
 * @param int $nid
 *   The node id (nid) of the node we want to get the tags for
 */
function get_node_tags_uncached($nid) {
  
  // The $tag_vocabulary_id should be a constant
  $tag_vocabulary_id = 2;
  
  // The cache has not been set; load using the taxonomy API
  $tags = taxonomy_node_get_terms_by_vocabulary(node_load($nid), $tag_vocabulary_id);
  error_log("Retrieving from database");
  
  // Return the result.
  return $tags;
}

If we call this method three times, our PHP script will hit the database three times.

// 203 is the nid (node id) of a test content node
$tags = get_node_tags_uncached(203);
$tags = get_node_tags_uncached(203);
$tags = get_node_tags_uncached(203);

// Log output
[01-Jun-2009 21:31:11] Retrieving from database
[01-Jun-2009 21:31:11] Retrieving from database
[01-Jun-2009 21:31:11] Retrieving from database

Hopefully we can do better than this.

Add static caching

The next part is not rocket science: let's cache the result of the database call. Here's what our function looks like now:

/**
 * Get the tags associated with a given node
 *
 * @param int $nid
 *   The node id (nid) of the node we want to get the tags for
 */
function get_node_tags_standard_caching($nid) {
  
  // Create an array to hold the cache
  static $tag_cache = array();
  
  if (isset($tag_cache[$nid])) {
    $tags = $tag_cache[$nid];
    // Retreiving from local static cache
    return $tags;   
  }
 
  // The $tag_vocabulary_id should be a constant
  $tag_vocabulary_id = 2;
  
  // The cache has not been set; load using the taxonomy API
  $tags = taxonomy_node_get_terms_by_vocabulary(node_load($nid), $tag_vocabulary_id);
  error_log("Retrieving from database");
  
  $tag_cache[$nid] = $tags;
  
  // Return the result.
  return $tags;
}

The relevant chunk is lines 10 - 15. We declare a static variable $tag_cache which is initialized to an array the first time this function is called. (Remember that in PHP an array can act like as an associative array, or HashMap for those coming from the Java world). Then using the isset() function, we test if there's an object stored in the $tag_cache array under a unique key, which is the $nid variable in this example. If there's an object stored, we return it; if not, call the database, and before returning the object, we stuff it into $tag_cache (line 25). Now, let's call this function three times:

$tags = get_node_tags_standard_caching(203);
$tags = get_node_tags_standard_caching(203);
$tags = get_node_tags_standard_caching(203);

// Log output
[01-Jun-2009 21:41:23] Retrieving from database

Excellent. We've only hit the database once. All standard fare so far.

So, what's the problem?

If I left the blog post at this, we'd have an example of standard PHP static caching. After I implemented these changes, the number of times the database was hit dropped dramatically, from 22 times down to about 8. The problem was that there were other functions calling the same API method - taxonomy_node_get_terms_by_vocabulary() - with the same arguments. When those other methods were called, the results were already cached in $tag_cache in my original function, but since static variables are tied to a particular function, I had no way of accessing those results from other functions. What I needed was a global static cache. I'll spare you the details of my first steps at coding this, but after implementing the global static cache twice, it was fairly obvious that the functionality should be refactored out into a reusable library. This is what the code looks like now:

/**
 * Get the tags associated with a given node
 *
 * @param int $nid
 *   The node id (nid) of the node we want to get the tags for
 */
function get_node_tags($nid) {
  
  // $cache_name should be stored in a constant
  $cache_name = "node_tag_cache";
  
  // The $tag_vocabulary_id should also be a constant
  $tag_vocabulary_id = 2;
  
  // Check if the item has already been cached
  if (globalstaticccache_isset($cache_name, $nid)) {
    // The cache has been set. Get the tags and return them.
    // Note that the tags could previously been set to NULL.
    $tags = globalstaticccache_get($cache_name, $nid);
    return $tags;
  }
  
  // The cache has not been set; load using the taxonomy API
  $tags = taxonomy_node_get_terms_by_vocabulary(node_load($nid), $tag_vocabulary_id);
  error_log("Retrieving from database");
  
  
  // Cache the result.
  globalstaticccache_set($cache_name, $nid, $tags);
  
  // Return the result.
  return $tags;
}

Note that the code is extremely similar to the earlier static caching example, and you use the same three operations on the cache. The major coding difference is that you need to pass in the cache name to each of the globalstaticcache methods.

  • Testing the cache: use globalstaticccache_isset()
  • Retrieving from the cache: use globalstaticccache_get()
  • Storing into the cache: use globalstaticccache_set()
  • Also, not shown above, clear the cache: globalstaticcache_clear()

But there's a major functional difference: the static caches are now available globally. So, you can call globalstaticcache_get($cache_name, $key) from any method, and hit the same named cache. Wonderful.

How do I use it?

The library is contained in a single PHP file, globalstaticcache.inc, which is included in the zip in the Attachments block to your right. Then, just include the file in your PHP script using the usual PHP include directive. The zip includes the PHPDoc. But for completeness, here's the source code of globalstaticcache.inc (minus the MIT License piece etc.).

<?php

/**
 * The globalstaticcache functions provide a clean and simple mechanism for implementing
 * global static caching, alleviating much boilerplate code. The advantage of using
 * globalstatic instead of local static variables is that any objects you cache
 * are available globally, not just inside one function.
 * 
 * @author Neil O'Toole
 */

/**
 * The global cache of caches.
 */
$globalstaticcache_data = array();


/**
 * Get an object from the named static cache, or the entire
 * cache if $key is null. Note that this item will return NULL
 * if the $key is not cached OR if $key's cached value is NULL.
 * Use #staticcache_isset to distinguish between these cases.
 *
 * @param $cache_name
 *  The name of the cache
 * @param $key
 *  The key of the object to retrieve, may be NULL.
 * @return an object, or NULL
 */
function globalstaticcache_get($cache_name, $key = NULL) {
  
  // The global array of static caches
  global $globalstaticcache_data;
  
  // Check if the global cache exists
  if ($globalstaticcache_data) {
    
    // The global cache exists; check if the named cache exists
    $cache = $globalstaticcache_data[$cache_name];
  
    if ($cache && $key) {
      // If the named cache exists, and $key is set, return the cached value
      return $cache[$key];
    }
  
    // Else return the entire cache (which could be NULL)
    return $cache;
  }
}


/**
 * Return true if the cache item has been set (even if
 * the value is NULL). If
 * $key is null, this method returns true if the
 * named cache exists.
 *
 * @param $cache_name
 *  The name of the cache
 * @param $key
 *  The key of the object to test, or NULL to test if the cache exists
 * 
 * @return true if the cache or key is set (even if NULL), false otherwise
 */
function globalstaticcache_isset($cache_name, $key = NULL) {
  
  // The global array of static caches
  global $globalstaticcache_data;
  
  if ($key) {
    // Check if the keyed element exists
    if(!$globalstaticcache_data[$cache_name]) {
      return FALSE;
    }
    
    return array_key_exists($key, $globalstaticcache_data[$cache_name]);
  }
  
  // Else, check if the named cache exists
  return array_key_exists($cache_name, $globalstaticcache_data);

 
}


/**
 * Put an object into the named static cache.
 *
 * @param $cache_name
 *  The name of the cache
 * @param $key
 *  The key of the object to cache
 * @param $data
 *   The object to cache
 */
function globalstaticcache_set($cache_name, $key, $data) {
  
  // The global array of static caches
  global $globalstaticcache_data;
  
  if (!isset($globalstaticcache_data)) {
    // If $globalstaticcache_data is not set, create it.
    $globalstaticcache_data = array();
  }
  
  if (!isset($globalstaticcache_data[$cache_name])) {
    // If the named cache does not exist, create it.
    $globalstaticcache_data[$cache_name] = array();
  }
  
  // Store $data under $key in the named cache $cache_name
  $globalstaticcache_data[$cache_name][$key] = $data;
}


/**
 * Clear the key/value pair from the named cache. If $key is NULL,
 * the entire named cache will be cleared.
 *
 * @param $cache_name
 *  The name of the cache
 * @param $key
 *  The key of the object to cache; if NULL, will clear the entire named cache.
 */
function globalstaticcache_clear($cache_name, $key = NULL) {
  
  // The global array of static caches
  global $globalstaticcache_data;
  
  if ($key) {
    // Clear the keyed object
    unset($globalstaticcache_data[$cache_name][$key]);
  }
  else {
    // Clear the entire named cache
    unset($globalstaticcache_data[$cache_name]);
  }
}

 

Discussion
lucky 16 comments Joined 12/08
03 Jun 2009

This would be a great article to Digg. When will that slacker lucky get the sharing tools working?!

devxadmin 3 comments Joined 12/08
08 Jun 2009

Any day now one might imagine...

You must sign in to leave a comment.