Created
October 29, 2012 00:40
-
-
Save chrisguitarguy/3970699 to your computer and use it in GitHub Desktop.
WordPress object cache implementation using APC.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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