Skip to content

Instantly share code, notes, and snippets.

@shadyvb
Last active September 19, 2016 13:13
Show Gist options
  • Save shadyvb/cd23e4bb9d83b1f639c3fcb1d9ae0139 to your computer and use it in GitHub Desktop.
Save shadyvb/cd23e4bb9d83b1f639c3fcb1d9ae0139 to your computer and use it in GitHub Desktop.
WordPress Scoped Cache helper
<?php
/**
* WordPress Scoped Cache Helper
*
* This addresses the problem of obsolete cached data for an object after it is updated.
*
* Sometimes we need to cache specific bits of data related to a certain object, and keep the cached data in sync
* with the object's state. This helper makes it easier to tap into object update process to clear ( and optionally
* rebuild ) the cache.
*
* Usage:
*
* - Make sure you add the custom key via `wp_scoped_cache_keys_add` so it is cleared on object update
*
* - Use `wp_scoped_cache_set` to store caches for objects, that gets automatically cleared once the object is updated,
* then `wp_scoped_cache_get` to retrieve the non-staled cached content, or the empty content to rebuild on the fly.
*
* - OPTIONAL, Tap in `wp_scopeed_cache_refresh_$TYPE_$KEY` filter to rebuild cache once the object has been updated
*
* OR, the one step to do all the above, example:
*
* ```
* wp_scoped_cache_keys_add( 'some_key', 'post', function( $value, $object_id ) {
* return get_post( $object_id )->post_title;
* }, true );
* ```
*
* @package scoped-cache
* @version 1.2
* @author Shady Sharaf <[email protected]>
*/
const OPTION_KEY_PREFIX = 'wp_scoped_cache_key_of_';
/**
* Get composite scoped-cache key from cache key, object id and type
*
* @param string $key Cache key
* @param int|object $object_id Object instance or Object ID
* @param string|null $object_type Object type, auto-detected if $object_id is a post, term, comment, or user object
*
* @return string Scoped-cache key
* @throws \Exception
*/
function wp_get_scoped_cache_key( $key, $object_id, $object_type = null ) {
// Get object type if an object instance was passed
list( $object_id, $object_type ) = wp_scoped_cache_resolve_type( $object_id, $object_type );
return implode( '|', [ $object_type, $key, $object_id ] );
}
/**
* Resolve object id/type if an actual object instance has been passed
*
* @param int|object $object_id
* @param mixed $object_type
*
* @throws Exception
*/
function wp_scoped_cache_resolve_type( $object_id, $object_type ) {
if ( ! is_object( $object_id ) ) {
return [ $object_id, $object_type ];
}
switch ( get_class( $object_id ) ) {
case 'WP_Post':
$object_type = $object_id->post_type;
$object_id = $object_id->ID;
break;
case 'WP_Term':
$object_type = $object_id->taxonomy;
$object_id = $object_id->term_id;
break;
case 'WP_Comment':
$object_type = 'comment';
$object_id = $object_id->comment_ID;
break;
case 'WP_User':
$object_type = 'user';
$object_id = $object_id->ID;
break;
default:
throw new \Exception( __( 'Invalid object type, either pass a valid object type or an ID and Type parameter' ), 422 );
break;
}
return [ $object_id, $object_type ];
}
/**
* Add a new scoped-cache key for an object type
*
* @param string $key Cache key
* @param string $object_type Object type
* @param callable $callback A callback to execute for when no data exists, and to use with the refresh filter
* @param bool $build_on_refresh Build cache on refresh
*
* @return bool
*/
function wp_scoped_cache_keys_add( $key, $object_type, $callback = null, $build_on_refresh = false ) {
if ( $callback ) {
wp_scoped_cache_register_callback( $key, $object_type, $callback, $build_on_refresh );
}
return wp_scoped_cache_keys_set( array_merge( wp_scoped_cache_keys_get( $object_type ), [ $key ] ), $object_type );
}
/**
* Set scoped-cache keys for an object type
*
* @param array $keys List of scoped-cache keys
* @param string $object_type Object Type
*
* @return bool
*/
function wp_scoped_cache_keys_set( $keys = [], $object_type = 'post' ) {
return update_option( OPTION_KEY_PREFIX . $object_type, array_filter( ( array ) $keys ) );
}
/**
* Get scoped-cache keys for an object type
*
* @param string $object_type Object Type
*
* @return array
*/
function wp_scoped_cache_keys_get( $object_type = 'post' ) {
return ( array ) get_option( OPTION_KEY_PREFIX . $object_type, [] );
}
/**
* Set scoped-cache value for an object
*
* @param string $key Cache key
* @param int|object $object_id Object instance or Object ID
* @param string|null $object_type Object type, auto-detected if $object_id is a post, term, comment, or user object
*
* @return bool
*/
function wp_scoped_cache_set( $key, $data, $object_id, $object_type = null ) {
return wp_cache_set( wp_get_scoped_cache_key( $key, $object_id, $object_type ), $data );
}
/**
* Get scoped-cache value for an object
*
* @param string $key Cache key
* @param int|object $object_id Object instance or Object ID
* @param string|null $object_type Object type, auto-detected if $object_id is a post, term, comment, or user object
*
* @return bool|mixed
*/
function wp_scoped_cache_get( $key, $object_id, $object_type = null ) {
// Get object type if an object instance was passed
list( $object_id, $object_type ) = wp_scoped_cache_resolve_type( $object_id, $object_type );
$found = null;
$value = wp_cache_get( wp_get_scoped_cache_key( $key, $object_id, $object_type ), '', false, $found );
if ( ! $found ) {
$value = apply_filters(
'wp_scoped_cache_build|' . wp_get_scoped_cache_key( $key, '', $object_type ),
$value,
$object_id,
$object_type,
$key
);
}
return $value;
}
/**
* Delete a scoped-cache value for an object
*
* @param string $key Cache key
* @param int|object $object_id Object instance or Object ID
* @param string|null $object_type Object type, auto-detected if $object_id is a post, term, comment, or user object
*
* @return bool
*/
function wp_scoped_cache_delete( $key, $object_id, $object_type = null ) {
return wp_cache_delete( wp_get_scoped_cache_key( $key, $object_id, $object_type ) );
}
/**
* Clear scoped-cache data for a certain object
*
* @param int|string|array $object_ids Object ID(s)
* @param string $object_type Object type
*/
function wp_scoped_cache_nuke( $object_ids, $object_type = 'post' ) {
foreach ( wp_scoped_cache_keys_get( $object_type ) as $cache_key ) {
foreach ( ( array ) $object_ids as $object_id ) {
/**
* Use this filter to rebuild cache data, or execute arbitrary commands, once the post has been updated.
*
* NOTE: This filter might not be very reliable, see clean_{OBJECT_TYPE}_cache function to make sure it
* happens at the expected processing point. ie: it is executed twice for wp_insert_post calls, and might
* not reflect the correct meta data that might be saved after the action is called.
*
* You can use a lambda function hooked to later actions ( like `shutdown` ) to workaround this limitation.
* Example:
*
* ```
* add_action( 'wp_scoped_cache_refresh_$TYPE_$KEY', function( $value, $object_id, $object_type, $cache_key ) {
* // .. validate if we need to rebuild the cache
*
* add_action( 'shutdown', function() use( $object_id, $object_type, $cache_key ) {
* // return some value here
* } );
* } );
* ```
*/
$new_value = apply_filters(
'wp_scoped_cache_refresh|' . wp_get_scoped_cache_key( $cache_key, '', $object_type ),
null,
$object_id,
$object_type,
$cache_key
);
if ( ! is_null( $new_value ) ) {
// If we have been passed a value, update the cache instead of clearing it
wp_scoped_cache_set( $cache_key, $new_value, $object_id, $object_type );
} else {
wp_scoped_cache_delete( $cache_key, $object_id, $object_type );
}
}
}
}
/**
* Register a callback to use to rebuild cache upon refresh or when no
*
* @param string $key Cache key
* @param string $object_type Object Type
* @param callable $callback An add_action arg list, or a callable
* @param bool $build_on_refresh Build cache on refresh
*/
function wp_scoped_cache_register_callback( $key, $object_type, $callback, $build_on_refresh = false ) {
$cache_key = wp_get_scoped_cache_key( $key, '', $object_type );
$build_filter = 'wp_scoped_cache_build|' . $cache_key;
$refresh_filter = 'wp_scoped_cache_refresh|' . $cache_key;
// If not a callable, or a callable array
if ( ! is_callable( $callback ) ) {
$args = $callback;
} else {
$args = [ $callback, 10, PHP_INT_MAX ];
}
call_user_func_array( 'add_filter', array_merge( [ $build_filter ], $args ) );
if ( $build_on_refresh ) {
call_user_func_array( 'add_filter', array_merge( [ $refresh_filter ], $args ) );
}
}
add_action( 'clean_post_cache', __NAMESPACE__ . '\\wp_scoped_cache_nuke' );
add_action( 'clean_term_cache', __NAMESPACE__ . '\\wp_scoped_cache_nuke', 10, 2 );
add_action( 'clean_comment_cache', function ( $id ) { wp_scoped_cache_nuke( $id, 'comment' ); } );
add_action( 'clean_user_cache', function ( $id ) { wp_scoped_cache_nuke( $id, 'user' ); } );
add_action( 'clean_attachment_cache', function ( $id ) { wp_scoped_cache_nuke( $id, 'attachment' ); } );
add_action( 'wp_update_nav_menu', function ( $id ) { wp_scoped_cache_nuke( $id, 'nav_menu' ); } );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment