Skip to content

Instantly share code, notes, and snippets.

@Konamiman
Created October 28, 2025 14:27
Show Gist options
  • Save Konamiman/96aeacc7933b5b89cb735c46f02adf89 to your computer and use it in GitHub Desktop.
Save Konamiman/96aeacc7933b5b89cb735c46f02adf89 to your computer and use it in GitHub Desktop.
Simple file-based persistent object cache for WordPress. For testing scenarios only.
<?php
/**
* Simple file-based WordPress object cache implementation.
*
* Drop this file in wp-content/ to enable persistent object caching.
* Each cache group stores its data in a separate file using PHP serialization.
* Files will go in WP_CONTENT_DIR . '/cache/object-cache', or if
* no WP_CONTENT_DIR constant is defined, in '/tmp/wp-object-cache'.
*
* This file is provided as-is without warranty of any kind.
* It's not officially supported by WooCommerce or Automattic.
*
* NOT FOR PRODUCTION - For development/testing only.
*/
class WP_Object_Cache {
/**
* Cache directory path.
*
* @var string
*/
private $cache_dir;
/**
* In-memory cache for this request.
*
* @var array
*/
private $cache = array();
/**
* Dirty flags to track which groups need saving.
*
* @var array
*/
private $dirty = array();
/**
* Whether cache directory has been initialized.
*
* @var bool
*/
private $initialized = false;
/**
* Initialize cache directory (lazy initialization).
*/
private function init() {
if ( $this->initialized ) {
return;
}
$this->cache_dir = defined( 'WP_CONTENT_DIR' ) ? WP_CONTENT_DIR . '/cache/object-cache' : '/tmp/wp-object-cache';
// Create cache directory if it doesn't exist.
if ( ! is_dir( $this->cache_dir ) ) {
// Use mkdir instead of wp_mkdir_p which may not be available yet.
@mkdir( $this->cache_dir, 0755, true );
}
$this->initialized = true;
}
/**
* Get a value from cache.
*
* @param string $key Cache key.
* @param string $group Cache group.
* @param bool $force Not used.
* @param bool &$found Whether the key was found.
* @return mixed|false The cached value or false if not found.
*/
public function get( $key, $group = 'default', $force = false, &$found = null ) {
$this->init();
$group = empty( $group ) ? 'default' : $group;
// Check in-memory cache first.
if ( ! isset( $this->cache[ $group ] ) ) {
$this->load_group( $group );
}
if ( isset( $this->cache[ $group ][ $key ] ) ) {
$entry = $this->cache[ $group ][ $key ];
// Check if expired.
if ( $entry['expires'] > 0 && $entry['expires'] < time() ) {
unset( $this->cache[ $group ][ $key ] );
$this->dirty[ $group ] = true;
$found = false;
return false;
}
$found = true;
return $entry['value'];
}
$found = false;
return false;
}
/**
* Set a value in cache.
*
* @param string $key Cache key.
* @param mixed $value Value to cache.
* @param string $group Cache group.
* @param int $expire Expiration in seconds (0 = no expiration).
* @return bool True on success.
*/
public function set( $key, $value, $group = 'default', $expire = 0 ) {
$this->init();
$group = empty( $group ) ? 'default' : $group;
if ( ! isset( $this->cache[ $group ] ) ) {
$this->cache[ $group ] = array();
}
$this->cache[ $group ][ $key ] = array(
'value' => $value,
'expires' => $expire > 0 ? time() + $expire : 0,
);
$this->dirty[ $group ] = true;
return true;
}
/**
* Delete a value from cache.
*
* @param string $key Cache key.
* @param string $group Cache group.
* @return bool True on success.
*/
public function delete( $key, $group = 'default' ) {
$this->init();
$group = empty( $group ) ? 'default' : $group;
if ( isset( $this->cache[ $group ][ $key ] ) ) {
unset( $this->cache[ $group ][ $key ] );
$this->dirty[ $group ] = true;
return true;
}
return false;
}
/**
* Flush all cache.
*
* @return bool True on success.
*/
public function flush() {
$this->init();
$this->cache = array();
$this->dirty = array();
// Delete all cache files.
$files = glob( $this->cache_dir . '/*.php' );
if ( $files ) {
foreach ( $files as $file ) {
unlink( $file );
}
}
return true;
}
/**
* Get cache file path for a group.
*
* @param string $group Group name.
* @return string File path.
*/
private function get_cache_file( $group ) {
// Sanitize group name for filename.
$safe_group = preg_replace( '/[^a-z0-9_\-]/', '_', strtolower( $group ) );
return $this->cache_dir . '/' . md5( $group ) . '-' . $safe_group . '.php';
}
/**
* Load a cache group from disk.
*
* @param string $group Group name.
*/
private function load_group( $group ) {
$file = $this->get_cache_file( $group );
if ( ! file_exists( $file ) ) {
$this->cache[ $group ] = array();
return;
}
$contents = file_get_contents( $file );
// Remove the PHP header if present.
if ( strpos( $contents, '<?php' ) === 0 ) {
$contents = substr( $contents, strpos( $contents, "\n" ) + 1 );
}
$data = @unserialize( $contents );
if ( $data !== false && is_array( $data ) ) {
$this->cache[ $group ] = $data;
} else {
$this->cache[ $group ] = array();
}
}
/**
* Save a cache group to disk.
*
* @param string $group Group name.
*/
private function save_group( $group ) {
$file = $this->get_cache_file( $group );
$data = isset( $this->cache[ $group ] ) ? $this->cache[ $group ] : array();
// Clean up expired entries before saving.
$now = time();
foreach ( $data as $key => $entry ) {
if ( $entry['expires'] > 0 && $entry['expires'] < $now ) {
unset( $data[ $key ] );
}
}
if ( empty( $data ) ) {
// Delete file if group is empty.
if ( file_exists( $file ) ) {
unlink( $file );
}
} else {
// Save with PHP header to prevent direct access.
$serialized = serialize( $data );
file_put_contents( $file, "<?php exit; ?>\n" . $serialized );
}
unset( $this->dirty[ $group ] );
}
/**
* Save all dirty groups to disk.
*/
private function save_dirty_groups() {
if ( ! $this->initialized ) {
return;
}
foreach ( array_keys( $this->dirty ) as $group ) {
$this->save_group( $group );
}
}
/**
* Destructor - save dirty groups on shutdown.
*/
public function __destruct() {
$this->save_dirty_groups();
}
}
/**
* Ensure cache object is initialized.
*/
function wp_cache_init() {
global $wp_object_cache;
if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) {
$wp_object_cache = new WP_Object_Cache();
}
}
// Initialize global cache object.
wp_cache_init();
/**
* WordPress object cache functions.
*/
function wp_cache_add( $key, $data, $group = '', $expire = 0 ) {
return wp_cache_set( $key, $data, $group, $expire );
}
function wp_cache_set( $key, $data, $group = '', $expire = 0 ) {
global $wp_object_cache;
if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) {
wp_cache_init();
}
return $wp_object_cache->set( $key, $data, $group, (int) $expire );
}
function wp_cache_get( $key, $group = '', $force = false, &$found = null ) {
global $wp_object_cache;
if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) {
wp_cache_init();
}
return $wp_object_cache->get( $key, $group, $force, $found );
}
function wp_cache_delete( $key, $group = '' ) {
global $wp_object_cache;
if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) {
wp_cache_init();
}
return $wp_object_cache->delete( $key, $group );
}
function wp_cache_replace( $key, $data, $group = '', $expire = 0 ) {
return wp_cache_set( $key, $data, $group, $expire );
}
function wp_cache_flush() {
global $wp_object_cache;
if ( ! ( $wp_object_cache instanceof WP_Object_Cache ) ) {
wp_cache_init();
}
return $wp_object_cache->flush();
}
function wp_cache_incr( $key, $offset = 1, $group = '' ) {
// Not used by our caches, minimal implementation.
return false;
}
function wp_cache_decr( $key, $offset = 1, $group = '' ) {
// Not used by our caches, minimal implementation.
return false;
}
function wp_cache_switch_to_blog( $blog_id ) {
// Multisite not needed for basic testing.
return true;
}
function wp_cache_add_non_persistent_groups( $groups ) {
// All groups are persistent in this implementation.
return true;
}
function wp_cache_add_global_groups( $groups ) {
// Single-site compatibility.
return true;
}
function wp_cache_close() {
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment