WordPress中插件提供的对象缓存感觉使用体验都一般,有一些可能还需要订阅制才能使用高级功能,其实PHP本身带了一个apcu扩展来实现基于内存的缓存,这里记录一下具体实现。
<?php
/**
* Plugin Name: APCu Object Cache
* Plugin URI: https://example.com/apcu-object-cache
* Description: 使用APCu扩展的WordPress对象缓存插件,支持AdminBar统计和刷新,强制使用HTTP_HOST前缀防跨站/跨实例混淆
* Version: 1.0.0
* Author: Ian Zhi
* License: GPL v2 or later
*
* 注意:使用前请确保服务器已安装并启用APCu扩展
* 默认缓存键前缀强制使用 $_SERVER['HTTP_HOST'],多站点时在其后追加 Blog ID
*/
// 防止直接访问文件
if ( !defined('ABSPATH') ) {
exit;
}
/**
* APCu对象缓存实现类
*/
class APCu_Object_Cache {
/**
* 缓存统计信息
* @var array
*/
private $stats = [
'get' => 0,
'set' => 0,
'delete' => 0,
'hit' => 0,
'miss' => 0,
'adds' => 0,
'replaces' => 0,
'incrs' => 0,
'decrs' => 0
];
/**
* 是否启用缓存
* @var bool
*/
private $enabled = true;
/**
* 全局缓存组
* @var array
*/
private $global_groups = [];
/**
* 全局缓存组(使用关联数组实现快速查找)
* @var array
*/
private $global_groups_hash = [];
/**
* 缓存键前缀
* @var string
*/
private $key_prefix = '';
/**
* 构造函数
*/
public function __construct() {
// 检查APCu扩展是否可用
if ( !function_exists('apcu_fetch') ) {
$this->enabled = false;
error_log('APCu Object Cache: APCu扩展未安装或未启用');
return;
}
// 注册关闭时的统计信息输出
if ( defined('WP_DEBUG') && WP_DEBUG ) {
register_shutdown_function(array($this, 'output_stats'));
}
// 初始化默认的全局组
$this->global_groups = ['users', 'userlogins', 'usermeta', 'user_meta', 'site-transient', 'site-options', 'site-lookup', 'blog-lookup', 'blog-details', 'rss', 'global-posts'];
// 使用 array_flip 比 array_fill_keys 更简洁高效
$this->global_groups_hash = array_flip($this->global_groups);
// 生成缓存键前缀(强制 HTTP_HOST 前缀)
$this->generate_key_prefix();
// 添加AdminBar菜单
add_action('admin_bar_menu', array($this, 'add_admin_bar_menu'), 100);
// 处理缓存刷新请求
add_action('admin_init', array($this, 'handle_flush_request'));
// 注册AJAX处理
add_action('wp_ajax_apcu_reset_stats', array($this, 'ajax_reset_stats'));
}
/**
* 生成缓存键前缀(强制 HTTP_HOST 前缀)
* 优先级:
* 1. 始终使用 HTTP_HOST 作为基础前缀
* 2. 如果是多站点,追加 Blog ID 做进一步隔离
*/
private function generate_key_prefix(): void {
// 获取 HTTP_HOST,清理非法字符并截断
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
// 验证Host格式,防止恶意注入
if ( !filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) ) {
$host = 'localhost';
}
$host = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $host);
$host = substr($host, 0, 50);
// 基础前缀 - 添加随机盐增强安全性
$salt = defined('APCU_CACHE_SALT') ? APCU_CACHE_SALT : md5(__FILE__);
$prefix = $host . '_' . substr($salt, 0, 8) . '_';
// 如果是多站点环境,追加 Blog ID 做隔离
if ( is_multisite() ) {
$blog_id = get_current_blog_id();
$this->key_prefix = $prefix . 'ms_' . $blog_id . '_';
if ( WP_DEBUG ) {
error_log('APCu Object Cache: Multisite detected, prefix: ' . $this->sanitize_for_log($this->key_prefix));
}
return;
}
// 非多站点直接使用 HTTP_HOST 前缀
$this->key_prefix = $prefix;
if ( WP_DEBUG ) {
error_log('APCu Object Cache: Using HTTP_HOST based prefix: ' . $this->sanitize_for_log($this->key_prefix));
}
}
/**
* 清理日志输出,防止日志注入
*
* @param string $data 要记录的数据
* @return string 清理后的数据
*/
private function sanitize_for_log(string $data): string {
// 移除换行符和其他可能导致日志注入的字符
$data = str_replace(["\r", "\n"], '', $data);
return $data;
}
/**
* 构建完整的缓存键
*
* @param string $key 原始键
* @param string $group 缓存组
* @return string
*/
private function build_key(string $key, string $group): string {
// 验证并清理key参数,防止缓存键注入
$key = $this->sanitize_key($key);
$group = $this->sanitize_key($group);
$prefix = $this->key_prefix;
// 如果是全局组,不使用博客ID前缀(但仍保留 HTTP_HOST)
// 使用 isset 替代 in_array 提升性能 O(1) vs O(n)
if ( isset($this->global_groups_hash[$group]) ) {
// 去掉末尾的多站点Blog ID部分(如果有)
$prefix = preg_replace('/ms_\d+_$/', '', $prefix);
$prefix = rtrim($prefix, '_') . '_global_';
}
return "{$prefix}{$group}_{$key}";
}
/**
* 清理缓存键,防止注入攻击
*
* @param string $key 原始键
* @return string 清理后的键
*/
private function sanitize_key(string $key): string {
// 限制长度和允许的字符
$key = preg_replace('/[^a-zA-Z0-9_\-\.]/', '_', $key);
$key = substr($key, 0, 100);
return $key;
}
/**
* 从缓存获取数据
*
* @param string $key 缓存键
* @param string $group 缓存组
* @param bool $force 是否强制从缓存获取
* @param bool $found 是否找到
* @return mixed
*/
public function get(string $key, string $group = 'default', bool $force = false, ?bool &$found = null): mixed {
$this->stats['get']++;
if ( !$this->enabled ) {
$found = false;
$this->stats['miss']++;
return false;
}
$final_key = $this->build_key($key, $group);
$value = apcu_fetch($final_key, $success);
if ( $success ) {
$found = true;
$this->stats['hit']++;
return $value instanceof \stdClass ? clone $value : $value;
}
$found = false;
$this->stats['miss']++;
return false;
}
/**
* 设置缓存数据
*
* @param string $key 缓存键
* @param mixed $data 缓存数据
* @param string $group 缓存组
* @param int $expire 过期时间(秒)
* @return bool
*/
public function set(string $key, mixed $data, string $group = 'default', int $expire = 0): bool {
$this->stats['set']++;
if ( !$this->enabled ) {
return false;
}
$final_key = $this->build_key($key, $group);
if ( $data instanceof \stdClass ) {
$data = clone $data;
}
// 使用 isset 替代 in_array 提升性能 O(1) vs O(n)
if ( isset($this->global_groups_hash[$group]) ) {
$expire = 0;
}
return apcu_store($final_key, $data, $expire);
}
/**
* 添加缓存数据(仅当键不存在时)
*/
public function add(string $key, mixed $data, string $group = 'default', int $expire = 0): bool {
if ( !$this->enabled ) {
return false;
}
$final_key = $this->build_key($key, $group);
if ( apcu_exists($final_key) ) {
return false;
}
$this->stats['adds']++;
// 处理全局组过期时间
if ( isset($this->global_groups_hash[$group]) ) {
$expire = 0;
}
// 克隆对象(与 set 方法保持一致)
if ( $data instanceof \stdClass ) {
$data = clone $data;
}
// 避免重复构建 key,直接使用已构建的 $final_key
return apcu_store($final_key, $data, $expire);
}
/**
* 替换缓存数据(仅当键存在时)
*/
public function replace(string $key, mixed $data, string $group = 'default', int $expire = 0): bool {
if ( !$this->enabled ) {
return false;
}
$final_key = $this->build_key($key, $group);
if ( !apcu_exists($final_key) ) {
return false;
}
$this->stats['replaces']++;
// 处理全局组过期时间
if ( isset($this->global_groups_hash[$group]) ) {
$expire = 0;
}
// 克隆对象(与 set 方法保持一致)
if ( $data instanceof \stdClass ) {
$data = clone $data;
}
return apcu_store($final_key, $data, $expire);
}
/**
* 删除缓存数据
*/
public function delete(string $key, string $group = 'default'): bool {
$this->stats['delete']++;
if ( !$this->enabled ) {
return false;
}
return apcu_delete($this->build_key($key, $group));
}
/**
* 递增数值
*/
public function incr(string $key, int $offset = 1, string $group = 'default'): int|false {
if ( !$this->enabled ) {
return false;
}
$this->stats['incrs']++;
return apcu_inc($this->build_key($key, $group), $offset);
}
/**
* 递减数值
*/
public function decrement(string $key, int $offset = 1, string $group = 'default'): int|false {
if ( !$this->enabled ) {
return false;
}
$this->stats['decrs']++;
return apcu_dec($this->build_key($key, $group), $offset);
}
/**
* 清空缓存
*/
public function flush(): bool {
return $this->enabled && apcu_clear_cache();
}
/**
* 获取缓存统计信息
*/
public function stats(): array {
$apcu_info = apcu_cache_info(true);
return [
'enabled' => $this->enabled,
'hits' => $this->stats['hit'],
'misses' => $this->stats['miss'],
'gets' => $this->stats['get'],
'sets' => $this->stats['set'],
'deletes' => $this->stats['delete'],
'adds' => $this->stats['adds'],
'replaces' => $this->stats['replaces'],
'incrs' => $this->stats['incrs'],
'decrs' => $this->stats['decrs'],
'hit_rate' => $this->stats['get'] > 0 ? round(($this->stats['hit'] / $this->stats['get']) * 100, 2) : 0,
'apcu_memory' => $apcu_info['memory_size'] ?? 0,
'apcu_memory_free' => $apcu_info['mem_size'] ?? 0,
'apcu_entries' => $apcu_info['num_entries'] ?? 0,
'apcu_hits' => $apcu_info['num_hits'] ?? 0,
'apcu_misses' => $apcu_info['num_misses'] ?? 0,
];
}
/**
* 输出统计信息(调试用)
*/
public function output_stats(): void {
if ( WP_DEBUG && $this->enabled ) {
$stats = $this->stats();
// 清理统计数据防止日志注入
foreach ($stats as $key => $value) {
if (is_string($value)) {
$stats[$key] = $this->sanitize_for_log($value);
}
}
error_log('APCu Object Cache Stats: ' . print_r($stats, true));
}
}
/**
* 添加全局缓存组
*/
public function add_global_groups(string|array $groups): void {
if ( !is_array($groups) ) {
$groups = [$groups];
}
// 只处理不存在的组,避免不必要的 array_merge 和 array_unique
foreach ( $groups as $group ) {
// 清理group名称,防止注入
$group = $this->sanitize_key($group);
if ( !isset($this->global_groups_hash[$group]) ) {
$this->global_groups[] = $group;
$this->global_groups_hash[$group] = true;
}
}
// 确保 global_groups 中的值唯一(避免重复添加)
$this->global_groups = array_unique($this->global_groups);
}
/**
* 获取全局缓存组
*/
public function get_global_groups(): array {
return $this->global_groups;
}
/**
* AdminBar菜单
*/
public function add_admin_bar_menu(\WP_Admin_Bar $wp_admin_bar): void {
if ( !current_user_can('manage_options') ) {
return;
}
$stats = $this->stats();
$wp_admin_bar->add_node([
'id' => 'apcu-cache',
'title' => '<span class="ab-icon dashicons dashicons-performance"></span> APCu缓存',
'href' => '#',
'meta' => ['title' => 'APCu对象缓存管理'],
]);
$wp_admin_bar->add_node([
'parent' => 'apcu-cache',
'id' => 'apcu-cache-stats',
'title' => sprintf('📊 命中率 %s%% | 命中 %d | 未命中 %d',
esc_html($stats['hit_rate']),
(int)$stats['hits'],
(int)$stats['misses']
),
'href' => admin_url('?apcu_cache_action=stats'),
'meta' => ['title' => '查看详细统计信息'],
]);
$memory_mb = round($stats['apcu_memory'] / 1024 / 1024, 2);
$wp_admin_bar->add_node([
'parent' => 'apcu-cache',
'id' => 'apcu-cache-memory',
'title' => sprintf('💾 内存: %s MB | 条目: %d',
esc_html($memory_mb),
(int)$stats['apcu_entries']
),
'href' => '#',
'meta' => ['title' => 'APCu内存使用情况'],
]);
$wp_admin_bar->add_node([
'parent' => 'apcu-cache',
'id' => 'apcu-cache-separator',
'title' => '',
'href' => '#',
]);
$wp_admin_bar->add_node([
'parent' => 'apcu-cache',
'id' => 'apcu-cache-flush',
'title' => '🔄 刷新缓存',
'href' => wp_nonce_url(admin_url('?apcu_cache_action=flush'), 'apcu_cache_flush'),
'meta' => ['title' => '清空所有APCu缓存'],
]);
}
/**
* 处理缓存刷新请求
*/
public function handle_flush_request(): void {
if ( !isset($_GET['apcu_cache_action']) ) {
return;
}
// 添加权限检查 - 只有管理员可以刷新缓存
if ( !current_user_can('manage_options') ) {
wp_die('权限不足', 403);
}
// 验证nonce
if ( !isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'apcu_cache_flush') ) {
wp_die('安全验证失败,请重试。');
}
$action = sanitize_text_field($_GET['apcu_cache_action']);
if ( $action === 'flush' ) {
$result = $this->flush();
$message = $result ? '已刷新所有APCu缓存' : '缓存刷新失败';
add_action('admin_notices', static function() use ($message) {
$class = str_contains($message, '失败') ? 'error' : 'success';
echo '<div class="notice notice-' . $class . ' is-dismissible"><p>' . esc_html($message) . '</p></div>';
});
wp_redirect(remove_query_arg(['apcu_cache_action', '_wpnonce']));
exit;
}
}
/**
* AJAX重置统计
*/
public function ajax_reset_stats() {
check_ajax_referer('apcu_reset_stats', '_wpnonce');
if ( !current_user_can('manage_options') ) {
wp_send_json_error('权限不足');
}
$this->stats = array_fill_keys(array_keys($this->stats), 0);
wp_send_json_success('统计已重置');
}
}
/**
* 初始化缓存对象
*/
function wp_cache_init(): void {
global $wp_object_cache;
if ( !($wp_object_cache instanceof APCu_Object_Cache) ) {
$wp_object_cache = new APCu_Object_Cache();
}
}
function wp_cache_get(string $key, string $group = '', bool $force = false, ?bool &$found = null): mixed {
global $wp_object_cache;
return $wp_object_cache->get($key, $group, $force, $found);
}
function wp_cache_set(string $key, mixed $data, string $group = '', int $expire = 0): bool {
global $wp_object_cache;
return $wp_object_cache->set($key, $data, $group, $expire);
}
function wp_cache_add(string $key, mixed $data, string $group = '', int $expire = 0): bool {
global $wp_object_cache;
return $wp_object_cache->add($key, $data, $group, $expire);
}
function wp_cache_replace(string $key, mixed $data, string $group = '', int $expire = 0): bool {
global $wp_object_cache;
return $wp_object_cache->replace($key, $data, $group, $expire);
}
function wp_cache_delete(string $key, string $group = ''): bool {
global $wp_object_cache;
return $wp_object_cache->delete($key, $group);
}
function wp_cache_incr(string $key, int $offset = 1, string $group = ''): int|false {
global $wp_object_cache;
return $wp_object_cache->incr($key, $offset, $group);
}
function wp_cache_decr(string $key, int $offset = 1, string $group = ''): int|false {
global $wp_object_cache;
return $wp_object_cache->decrement($key, $offset, $group);
}
function wp_cache_flush(): bool {
global $wp_object_cache;
return $wp_object_cache->flush();
}
function wp_cache_add_global_groups(string|array $groups): void {
global $wp_object_cache;
$wp_object_cache->add_global_groups($groups);
}
function wp_cache_add_non_persistent_groups(string|array $groups): void {
// APCu是持久化缓存,此函数在此实现中不做任何操作
}
function wp_cache_close(): bool {
return true;
}
// 初始化缓存
wp_cache_init();
?>
发表回复