Skip to content

Instantly share code, notes, and snippets.

@chrisguitarguy
Created October 29, 2012 00:40
Show Gist options
  • Save chrisguitarguy/3970699 to your computer and use it in GitHub Desktop.
Save chrisguitarguy/3970699 to your computer and use it in GitHub Desktop.
WordPress object cache implementation using APC.
<?php
/**
* Plugin Name: APC Object Cache
* Description: An object cache implementation that uses APC.
* Author: Christopher Davis
* Author URI: http://christopherdavis.me
* Version: 0.1
*
* Based on Mark Jaquith's APC object cache
* http://txfx.net/wordpress-plugins/apc/
*
* Written for fun, and to see how building an object cache backend works
*
* Place this file in your `wp-content` directory.
*
* @author Christopher Davis <chris [AT] classicalguitar.org>
* @copyright Christopher Davis 2012
* @license MIT
* @version 0.1
*/
!defined('ABSPATH') && exit;
// make sure we have apc
if(!function_exists('apc_add'))
wp_die('APC is not installed');
/**
* You can use this to set up a global "prefix" for every cache key. Useful
* if you you're sharing config files or something for each install.
*
* @since 0.1
*/
if(!defined('WP_APC_KEY_SALT'))
define('WP_APC_KEY_SALT', 'wp');
/**
* A default TTL for each variable.
*
* Defaults to 3600 (one hour)
*
* @since 0.1
*/
if(!defined('WP_APC_TTL'))
define('WP_APC_TTL', 3600);
/**
* The cache implementation.
*
* @since 0.1
*/
class WP_Object_Cache
{
/**
* "global" groups won't get prefixed the same was as other cache keys.
*
* @since 0.1
* @access private
*/
private $global_groups = array();
/**
* Bucket for groups that should only be cached for the duration of a
* pageload. These groups never hit apc.
*
* @since 0.1
* @access private
*/
private $np_groups = array();
/**
* Bucket for non-persistent cache items.
*
* @since 0.1
* @access private
*/
private $cache = array();
/**
* Stats. How many gets, adds, sets, and deletes.
*
* @since 0.1
* @access private
*/
private $stats = array(
'get' => 0, 'set' => 0, 'add' => 0, 'incr' => 0,
'decr' => 0, 'delete' => 0, 'hits' => 0, 'misses' => 0
);
/**
* Operations run on the cache. Separated by group.
*
* @since 0.1
* @access private
*/
private $opts = array();
/**
* Default expiration. Change this by defining WP_APC_TTL in your
* `wp-config.php`
*
* @since 0.1
* @access private
*/
private $ttl = WP_APC_TTL;
/**
* An `md5`'d string of ABSPATH. Used to prefix keys.
*
* @since 0.1
* @access private
*/
private $abspath = '';
/**
* Are we in debug mode?
*
* @since 0.1
* @access private
*/
private $debug = WP_DEBUG;
/**
* Blog prefix.
*
* @since 0.1
* @access private
*/
private $blog_prefix = '';
/**
* Constructor. Not much going on here.
*
* @since 0.1
* @access public
* @return void
*/
public function __construct()
{
global $blog_id;
$this->abspath = md5(ABSPATH);
$this->blog_prefix = isset($blog_id) ? $blog_id : 1;
}
/**
* Generate a unique key for a cache item.
*
* @since 0.1
* @access private
* @param string $id The unprefixed cache key
* @param string $group The cache group
* @return string
*/
private function get_key($id, $group)
{
if(empty($group))
$group = 'default';
$pf = '';
if(!isset($this->global_groups[$group]))
$pf = $this->blog_prefix . ':';
return WP_APC_KEY_SALT . ":{$this->abspath}:{$pf}{$group}:{$id}";
}
/**
* Log an operation.
*
* @since 0.1
* @access private
* @param string $op The operation (add, set, get, delete)
* @param string $group The group.
* @param string $key The cache key
*/
private function log($op, $group, $key)
{
$group = empty($group) ? 'default' : $group;
if(!isset($this->opts[$group]))
$this->opts[$group] = array();
$this->opts[$group][] = "{$op} {$key}";
if(isset($this->stats[$op]))
++$this->stats[$op];
}
/**
* Whether or not a group is non-persistent
*
* @since 0.1
* @access private
* @param group
* @return bool True if the group is non-persistent
*/
private function is_np($group)
{
$group = empty($group) ? 'default' : $group;
return !empty($this->np_groups[$group]);
}
/**
* Add a "global" (not prefixed with a blog_id) group.
*
* @since 0.1
* @access public
* @param array|string $groups The groups to add.
* @return void
*/
public function add_global_groups($groups)
{
$groups = array_fill_keys((array)$groups, true);
$this->global_groups = array_merge($this->global_groups, $groups);
}
/**
* Add non-persistent groups.
*
* @since 0.1
* @access public
* @param array|string $groups Group(s) to add
* @return void
*/
public function add_nonpersistent_groups($groups)
{
$groups = array_fill_keys((array)$groups, true);
$this->np_groups = array_merge($this->np_groups, $groups);
}
/**
* Flush the entire cache.
*
* @since 0.1
* @access public
* @return bool True on success.
*/
public function flush($apc=true)
{
$this->cache = array();
$rv = true;
if($apc)
$rv = apc_clear_cache('user');
return $rv;
}
/**
* Add an item to the cache if it is not already there.
*
* @since 1.0
* @access public
* @param string $key The cache key
* @param mixed $data What to cache
* @param string $group The cache group
* @param int $ttl (optional) How long the cache item should persist
* @return bool True on success
*/
public function add($key, $data, $group='default', $ttl=0)
{
if(function_exists('wp_suspend_cache_addition') && wp_suspend_cache_addition())
return false;
$this->log('add', $group, $key);
if(!$ttl)
$tll = $this->ttl;
$_key = $this->get_key($key, $group);
$data = is_object($data) ? clone $data : $data;
// deal with non-persistent groups
if($this->is_np($group))
{
// already set, bail
if(isset($this->cache[$_key]))
return false;
$this->cache[$_key] = $data;
return true;
}
return apc_add($_key, $data, $ttl);
}
/**
* Set a cache key.
*
* @since 0.1
* @access public
* @param string $key The cache key
* @param mixed $data What to cache
* @param string $group The cache group
* @param int $ttl (optional) How long the cache item should persist
* @return bool True on success -- should always return true.
*/
public function set($key, $data, $group='default', $ttl=0)
{
$this->log('set', $group, $key);
if(!$ttl)
$tll = $this->ttl;
$_key = $this->get_key($key, $group);
$data = is_object($data) ? clone $data : $data;
if($this->is_np($group))
{
$this->cache[$_key] = $data;
return true;
}
return apc_store($_key, $data, $ttl);
}
/**
* Fetch a cached item.
*
* @since 0.1
* @access public
* @param string $key The cache key
* @param string $group The cache group
* @param bool $force Force (eg. set) the variable as well.
* @param mixed $found Whether or not the key was found.
* @return mixed
*/
public function get($key, $group='default', $force=false, &$found=null)
{
$this->log('get', $group, $key);
$_key = $this->get_key($key, $group);
$rv = false;
if(isset($this->cache[$_key]))
{
// in our non-persistent cache.
$rv = is_object($this->cache[$_key])
? clone $this->cache[$_key] : $this->cache[$_key];
$found = true;
}
elseif($this->is_np($group) && $force)
{
// in the non persistent groups and force is set
$this->cache[$_key] = $rv = false;
$found = true;
}
else
{
// hit apc
$rv = apc_fetch($_key, $found);
}
if($found)
++$this->stats['hits'];
else
++$this->stats['misses'];
return $rv;
}
/**
* Increment a stored number.
*
* @since 0.1
* @access public
* @param string $key The cache key
* @param int $offset The increment step
* @param string $group The cache group
* @param int $ttl (optional) How long the cache item should persist
* @return int The incremented number.
*/
public function incr($key, $offset=1, $group='default', $ttl=0)
{
$this->log('incr', $group, $key);
if(!$ttl)
$ttl = $this->ttl;
$_key = $this->get_key($key, $group);
if($this->is_np($group))
{
if(empty($this->cache[$_key]) || !is_numeric($this->cache[$_key]))
$this->cache[$_key] = 0;
$this->cache[$_key] += intval($offset);
return $this->cache[$_key];
}
// make sure the key is set
if(!apc_exists($_key))
apc_add($_key, 0, $ttl);
return apc_inc($_key, $offset);
}
/**
* decrement a stored number.
*
* @since 0.1
* @access public
* @param string $key The cache key
* @param int $offset The increment step
* @param string $group The cache group
* @param int $ttl (optional) How long the cache item should persist
* @return int The decremented number.
*/
public function decr($key, $offset=1, $group='default', $ttl=0)
{
$this->log('decr', $group, $key);
if(!$ttl)
$ttl = $this->ttl;
$_key = $this->get_key($key, $group);
if($this->is_np($group))
{
if(empty($this->cache[$_key]) || !is_numeric($this->cache[$_key]))
$this->cache[$_key] = 0;
$this->cache[$_key] -= intval($offset);
return $this->cache[$_key];
}
// make sure the cache key is set.
apc_add($_key, 0, $ttl);
return apc_dec($_key, $offset);
}
/**
* Delete a stored cache item.
*
* @since 0.1
* @access public
* @param string $key The cache key
* @param string $group The cache group
* @param force Whether or not to force the delete. Shouldn't really
* do anything
* @return bool True on success
*/
public function delete($key, $group='default', $force=false)
{
$this->log('delete', $group, $key);
$_key = $this->get_key($key, $group);
if(isset($this->cache[$_key]))
{
// in the non-persistent cache
unset($this->cache[$_key]);
return true;
}
elseif($this->is_np($group) && $force)
{
// will probably cause some warnings
unset($this->cache[$_key]);
return true;
}
else
{
// apc
return apc_delete($_key);
}
}
/**
* Change the blog prefix ID.
*
* @since 0.1
* @param int $blog_id The blog id to switch to.
* @return void
*/
public function switch_to_blog($blog_id)
{
$this->blog_prefix = intval($blog_id);
}
/**
* Display some info about the cache.
*
* @since 0.1
* @return void
*/
public function stats()
{
echo '<div class="wp-cache-stats" style="font-size: 0.8em">';
echo '<h2>Cache Operations</h2>';
foreach($this->stats as $stat => $c)
{
echo "<p><strong>{$stat}:</strong> $c</p>";
}
foreach($this->opts as $group => $ops)
{
echo "<h2>Operations for {$group}</h2>";
foreach($ops as $o)
echo "<p>{$o}</p>";
}
if($this->debug)
{
echo '<pre>';
print_r(apc_cache_info());
echo '</pre>';
}
echo '</div>';
}
} // end WP_Object_Cache
/********** WP Cache API **********/
function wp_cache_init()
{
global $wp_object_cache;
$wp_object_cache = new WP_Object_Cache;
}
function wp_cache_close()
{
// don't need to "close" apc
return true;
}
function wp_cache_flush()
{
global $wp_object_cache;
return $wp_object_cache->flush();
}
function wp_cache_add($key, $data, $group='', $expire=0)
{
global $wp_object_cache;
return $wp_object_cache->add($key, $data, $group, $expire);
}
function wp_cache_set($key, $data, $group='', $expire=0)
{
global $wp_object_cache;
return $wp_object_cache->set($key, $data, $group, $expire);
}
function wp_cache_replace($key, $data, $group='', $expire=0)
{
global $wp_object_cache;
// same thing as wp_cache_set
return $wp_object_cache->set($key, $data, $group, $expire);
}
function wp_cache_get($key, $group='default', $force=false, &$found=null)
{
global $wp_object_cache;
return $wp_object_cache->get($key, $group, $force, $found);
}
function wp_cache_incr($key, $offset=1, $group='', $expires=0)
{
global $wp_object_cache;
return $wp_object_cache->incr($key, $offset, $group, $expires);
}
function wp_cache_decr($key, $offset=1, $group='', $expires=0)
{
global $wp_object_cache;
return $wp_object_cache->decr($key, $offset, $group, $expires);
}
function wp_cache_delete($key, $group='')
{
global $wp_object_cache;
return $wp_object_cache->delete($key, $group);
}
function wp_cache_reset()
{
// only flush the non-persistent cache.
global $wp_object_cache;
$wp_object_cache->flush(false);
}
function wp_cache_switch_to_blog($blog_id)
{
global $wp_object_cache;
$wp_object_cache->switch_to_blog($blog_id);
}
function wp_cache_add_global_groups($groups)
{
global $wp_object_cache;
return $wp_object_cache->add_global_groups($groups);
}
function wp_cache_add_non_persistent_groups($groups)
{
global $wp_object_cache;
return $wp_object_cache->add_nonpersistent_groups($groups);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment