Last active
September 19, 2016 13:13
-
-
Save shadyvb/cd23e4bb9d83b1f639c3fcb1d9ae0139 to your computer and use it in GitHub Desktop.
WordPress Scoped Cache helper
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 | |
/** | |
* 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