diff --git a/README.md b/README.md index 1b70d8a..f56127e 100644 --- a/README.md +++ b/README.md @@ -58,9 +58,12 @@ dfm_transient_meta_update_cb( $args ) { } ``` ## Retrieve the transient data -You can use the `dfm_get_transient()` function to retrieve the data for a transient. The first parameter passed to this function is the name of the transient you are trying to retrieve (should be the same name that you registered the transient with). The second parameter is the "modifier" for the transient. You can read more about modifiers below. +You can use the `dfm_get_transient()` function to retrieve the data for a transient. The first parameter passed to this function is the name of the transient you are trying to retrieve (should be the same name that you registered the transient with). The second parameter is the "modifier" for the transient. You can read more about modifiers below. The third parameter is the object_id (if you are using post_meta, term_meta, or user_meta as the cache type). ```php -$result = dfm_get_transient( 'sample_transient', '' ); +$result = dfm_get_transient( 'sample_transient' ); +``` +```php +$post_transient = dfm_get_transient( 'sample_transient', '', $post_id ); ``` ## Arguments for registering a transient @@ -78,7 +81,7 @@ $result = dfm_get_transient( 'sample_transient', '' ); 8. **soft_expiration** (bool) - Whether or not the data should soft expire or not. If this is set to true, it will check to see if the data has expired when retrieving it. If it is expired, it will spawn a background process to update the transient data. *Default: False* ## Transient Modifier -The transient modifier, (second parameter passed to the `dfm_get_transient` function) is used in a variety of different ways throughout this library. For a transient stored in metadata, it will be used as the object ID the transient is attached to. It will be used when using the `get_metadata()` and `save_metadata()` functions, so it is crucial that it is passed for transients stored in metadata. For global transients, it can be used to store variations of the same type of transient. It will append the `$modifier` to the end of the transient key. This way you could store and retrieve different variations of the same transient that are mostly the same without registering a whole new transient. You can use the modifier to change the data saved to the transient by using it to alter your logic in your callback (the modifier is passed as the only argument to your callback function). +The Transient modifier (second parameter passed to the `dfm_get_transient` function) is used to store variations of the same type of transient. It will append the $modifier to the end of the transient key. This way you could store and retrieve different variations of the same transient that are mostly the same without registering a whole new transient. You can use the modifier to change the data saved to the transient by using it to alter your logic in your callback (the modifier is passed as the first argument to your callback function). ## Debugging To help with debugging, you can set a constant in your codebase called `DFM_TRANSIENTS_HOT_RELOAD` and set it to `true` to enable "hot reload" mode. This will essentially make it so that transient data will be regenerated every time it is called. This is handy if you are working on adding a transient, and want it to keep regenerating while you are working on it. This saves the need from manually deleting it from your database, or setting an extremely short timeout. **NOTE:** This constant should only ever be used on a development environment. Using this on production could cause serious performance issues depending on the data you are storing in your transients. diff --git a/composer.json b/composer.json index 7a663bf..e83ca6b 100644 --- a/composer.json +++ b/composer.json @@ -3,5 +3,8 @@ "type": "wordpress-plugin", "description": "Get better control over your transients", "homepage": "https://github.com/dfmedia/DFM-Transients", - "license": "GPLv3" + "license": "GPLv3", + "require-dev": { + "phpunit/phpunit": "6.1.*" + } } diff --git a/dfm-transients.php b/dfm-transients.php index ad33872..2a443c7 100644 --- a/dfm-transients.php +++ b/dfm-transients.php @@ -3,7 +3,7 @@ * Plugin Name: Transient Control * Plugin URI: https://github.com/dfmedia/DFM-Transients * Description: Better control for transients -* Version: 1.2.0 +* Version: 1.3.0 * Author: Ryan Kanner, Digital First Media * License: GPL-3 */ @@ -12,6 +12,7 @@ exit; } +require_once( plugin_dir_path( __FILE__ ) . 'includes/class-dfm-transient-utils.php' ); require_once( plugin_dir_path( __FILE__ ) . 'includes/class-dfm-async-nonce.php' ); require_once( plugin_dir_path( __FILE__ ) . 'includes/class-dfm-async-handler.php' ); require_once( plugin_dir_path( __FILE__ ) . 'includes/class-dfm-transient-hook.php' ); diff --git a/includes/class-dfm-async-handler.php b/includes/class-dfm-async-handler.php index 49bdf72..007354f 100644 --- a/includes/class-dfm-async-handler.php +++ b/includes/class-dfm-async-handler.php @@ -35,13 +35,17 @@ class DFM_Async_Handler { * DFM_Async_Handler constructor. * * @param string $transient Name of the transient - * @param string $modifier Unique modifier for the transient - * @param string $lock_key Key for matching the update locking + * @param string $modifier Unique modifier for the transient + * @param int $object_id ID of the object where the transient data is stored + * @param string $lock_key Key for matching the update locking + * + * @return void */ - function __construct( $transient, $modifier, $lock_key = '' ) { + function __construct( $transient, $modifier, $object_id = 0, $lock_key = '' ) { $this->transient_name = $transient; $this->modifier = $modifier; + $this->object_id = $object_id; $this->lock_key = $lock_key; // Spawn the event on shutdown so we are less likely to run into timeouts, or block other processes add_action( 'shutdown', array( $this, 'spawn_event' ) ); @@ -73,6 +77,7 @@ public function spawn_event() { 'body' => array( 'transient_name' => $this->transient_name, 'modifier' => $this->modifier, + 'object_id' => $this->object_id, 'action' => 'dfm_' . $this->transient_name, '_nonce' => $nonce, 'async_action' => true, diff --git a/includes/class-dfm-transient-hook.php b/includes/class-dfm-transient-hook.php index 7089084..c671695 100644 --- a/includes/class-dfm-transient-hook.php +++ b/includes/class-dfm-transient-hook.php @@ -14,7 +14,15 @@ class DFM_Transient_Hook { * @var string * @access private */ - private $transient; + private $transient_name; + + /** + * The settings registered to the transient + * + * @var object $transient_obj + * @access private + */ + private $transient_obj; /** * Name of hook we want to hook into @@ -32,20 +40,31 @@ class DFM_Transient_Hook { */ private $callback; + /** + * Whether or not the update should happen asynchronously + * + * @var bool $async + * @access private + * + */ + private $async = false; + /** * DFM_Transient_Hook constructor. * - * @param string $transient - * @param string $hook - * @param bool $async_update - * @param string $callback + * @param string $transient_name Name of the transient to add the hook callback for + * @param object $transient_obj Settings that were added when the transient was registered + * @param string $hook Name of the hook + * @param bool $async_update Whether or not the update should happen asynchronously or not + * @param string $callback The callable method to be added to the hook */ - public function __construct( $transient, $hook, $async_update, $callback = '' ) { + public function __construct( $transient_name, $transient_obj, $hook, $async_update, $callback = '' ) { - $this->transient = $transient; - $this->hook = $hook; - $this->callback = $callback; - $this->async = $async_update; + $this->transient_name = $transient_name; + $this->transient_obj = $transient_obj; + $this->hook = $hook; + $this->callback = $callback; + $this->async = $async_update; $this->add_hook( $hook ); @@ -70,9 +89,14 @@ private function add_hook( $hook ) { * @return void * @access private */ - private function run_update( $modifier ) { + private function run_update( $modifier, $object_id = null ) { - $transient_obj = new DFM_Transients( $this->transient, $modifier ); + if ( $this->async ) { + new DFM_Async_Handler( $this->transient_name, $modifier, $object_id ); + return; + } + + $transient_obj = new DFM_Transients( $this->transient_name, $modifier, $object_id ); // Bail if another process is already trying to update this transient. if ( $transient_obj->is_locked() && ! $transient_obj->owns_lock( '' ) ) { @@ -83,7 +107,8 @@ private function run_update( $modifier ) { $transient_obj->lock_update(); } - $data = call_user_func( $transient_obj->transient_object->callback, $modifier ); + $data = call_user_func( $transient_obj->transient_object->callback, $modifier, $object_id ); + $transient_obj->set( $data ); $transient_obj->unlock_update(); @@ -116,30 +141,66 @@ public function spawn() { } } - // If we are running an async task, instantiate a new async handler. - if ( $this->async ) { - - // If we have an array of modifiers, update each of them. - if ( is_array( $modifiers ) ) { - foreach ( $modifiers as $modifier ) { - new DFM_Async_Handler( $this->transient, $modifier ); + /** + * Sample return formats from the callback + * + * - True/False: If you return false, it won't update anything, if you return true it will continue with the update process + * - 20: Return an int of the object ID to update the transient value for a specific object when the object cache type is being used. If you have transients using modifiers on this object all modifiers will be updated + * - 'blue': Return the string name of the transient modifier you would like to update. This only works for transients using the "transient" storage engine, since it needs the context of the object ID for object type caches + * - [ 10, 20, 30 ]: Return an array of object ID's to update transients on those specific object ID's + * - [ + * 10 => [ 'blue', 'yellow', 'green' ] Update transients with the modifiers blue, yellow, and green in the object ID: 10 + * 20 => [ 'blue', 'green' ] Update transients with the modifiers blue, and green in the object ID: 20 + * 30 => [] Update all transients within the group on object ID: 30 + * ] + */ + if ( is_array( $modifiers ) && ! empty( $modifiers ) ) { + foreach ( $modifiers as $key => $modifier ) { + if ( is_array( $modifier ) && ! empty( $modifier ) ) { + foreach ( $modifier as $key_modifier ) { + $this->dispatch_update( $key_modifier, $key ); + } + } else { + $this->dispatch_update( $modifier, $key ); } - } else { - new DFM_Async_Handler( $this->transient, $modifiers ); } - - // Run the update in real time if we aren't using an async process } else { + $this->dispatch_update( $modifiers ); + } - // If we have an array of modifiers, update each of them. - if ( is_array( $modifiers ) ) { - foreach ( $modifiers as $modifier ) { - $this->run_update( $modifier ); + } + + /** + * Retrieve data from the hook callback and process it for updating + * + * @param mixed|string|int|bool $modifier The data passed back from the callback + * @param null $object_id ID of the object an update needs to be performed for + * + * @access private + */ + private function dispatch_update( $modifier, $object_id = null ) { + + if ( 'transient' !== $this->transient_obj->cache_type ) { + + if ( empty( $object_id ) ) { + $object_id = absint( $modifier ); + $translated_obj_id = true; + } + + if ( empty( $modifier ) || isset( $translated_obj_id ) ) { + $modifier_map = DFM_Transients::get_meta_map( DFM_Transient_Utils::get_meta_type( $this->transient_obj->cache_type ), $object_id, $this->transient_obj->key ); + if ( ! empty( $modifier_map ) && is_array( $modifier_map ) ) { + foreach ( $modifier_map as $modifier => $modifier_key ) { + $this->run_update( $modifier, $object_id ); + } + } else { + $this->run_update( '', $object_id ); } } else { - $this->run_update( $modifiers ); + $this->run_update( $modifier, $object_id ); } - + } else { + $this->run_update( $modifier ); } } diff --git a/includes/class-dfm-transient-scheduler.php b/includes/class-dfm-transient-scheduler.php index 306385f..d9b8ea8 100644 --- a/includes/class-dfm-transient-scheduler.php +++ b/includes/class-dfm-transient-scheduler.php @@ -53,10 +53,10 @@ public function get_transients() { // If there are multiple hooks where this should fire, loop through all of them, and build a hook for each. if ( is_array( $transient_args->update_hooks ) ) { foreach ( $transient_args->update_hooks as $hook_name => $callback ) { - new DFM_Transient_Hook( $transient_id, $hook_name, $async_update, $callback ); + new DFM_Transient_Hook( $transient_id, $transient_args, $hook_name, $async_update, $callback ); } } else { - new DFM_Transient_Hook( $transient_id, $transient_args->update_hooks, $async_update ); + new DFM_Transient_Hook( $transient_id, $transient_args, $transient_args->update_hooks, $async_update ); } } @@ -90,6 +90,7 @@ public function run_update() { $transient_name = empty( $_POST['transient_name'] ) ? false : sanitize_text_field( $_POST['transient_name'] ); $modifier = empty( $_POST['modifier'] ) ? '' : sanitize_text_field( $_POST['modifier'] ); + $object_id = empty( $_POST['object_id'] ) ? null : sanitize_text_field( $_POST['object_id'] ); $nonce = empty( $_POST['_nonce'] ) ? '' : sanitize_text_field( $_POST['_nonce'] ); $lock_key = empty( $_POST['lock_key'] ) ? '' : sanitize_text_field( $_POST['lock_key'] ); @@ -105,7 +106,11 @@ public function run_update() { return; } - $transient_obj = new DFM_Transients( $transient_name, $modifier ); + $transient_obj = new DFM_Transients( $transient_name, $modifier, $object_id ); + + if ( 'transient' !== $transient_obj->transient_object->cache_type && empty( $object_id ) ) { + $object_id = absint( $modifier ); + } // Bail if another process is already trying to update this transient. if ( $transient_obj->is_locked() && ! $transient_obj->owns_lock( $lock_key ) ) { @@ -116,7 +121,7 @@ public function run_update() { $transient_obj->lock_update(); } - $data = call_user_func( $transient_obj->transient_object->callback, $modifier ); + $data = call_user_func( $transient_obj->transient_object->callback, $modifier, $object_id ); $transient_obj->set( $data ); $transient_obj->unlock_update(); diff --git a/includes/class-dfm-transient-utils.php b/includes/class-dfm-transient-utils.php new file mode 100644 index 0000000..a96e963 --- /dev/null +++ b/includes/class-dfm-transient-utils.php @@ -0,0 +1,38 @@ +transient = $transient; $this->modifier = $modifier; + $this->object_id = $object_id; $this->transient_object = $dfm_transients[ $this->transient ]; - $this->key = $this->cache_key(); $this->lock_key = uniqid( 'dfm_lt_' ); - $this->prefix = apply_filters( 'dfm_transient_prefix', 'dfm_transient_' ); + self::$prefix = apply_filters( 'dfm_transient_prefix', 'dfm_transient_', $this->transient, $this->modifier, $this->object_id ); + + /** + * For backwards compatibility, use the modifier value as the object ID if no ID is supplied, but it's an object type cache. + */ + if ( 'transient' !== $this->transient_object->cache_type && empty( $object_id ) ) { + $this->object_id = absint( $modifier ); + $this->bc_modifier = $modifier; + $this->modifier = ''; + } + + /** + * Generate the cache key after we've applied the backward compat fix above ^ + */ + $this->key = $this->cache_key(); } @@ -111,6 +141,17 @@ function __construct( $transient, $modifier ) { */ public function get() { + if ( 'transient' === $this->transient_object->cache_type ) { + $this->get_from_transient(); + } else { + $meta_type = DFM_Transient_Utils::get_meta_type( $this->transient_object->cache_type ); + if ( is_wp_error( $meta_type ) ) { + return $meta_type; + } else { + $this->get_from_meta( $meta_type ); + } + } + switch ( $this->transient_object->cache_type ) { case 'transient': return $this->get_from_transient(); @@ -135,7 +176,7 @@ public function get() { * * @param string|array $data The data to add to the transient * @access public - * @return void + * @return mixed|WP_Error|Void * @throws Exception */ public function set( $data ) { @@ -145,21 +186,15 @@ public function set( $data ) { return; } - switch ( $this->transient_object->cache_type ) { - case 'transient': - $this->save_to_transient( $data ); - break; - case 'post_meta': - $this->save_to_metadata( $data, 'post' ); - break; - case 'term_meta': - $this->save_to_metadata( $data, 'term' ); - break; - case 'user_meta': - $this->save_to_metadata( $data, 'user' ); - break; - default: - throw new Exception( __( 'When registering your transient, you used an invalid cache type. Valid options are transient, post_meta, term_meta.', 'dfm-transients' ) ); + if ( 'transient' === $this->transient_object->cache_type ) { + $this->save_to_transient( $data ); + } else { + $meta_type = DFM_Transient_Utils::get_meta_type( $this->transient_object->cache_type ); + if ( is_wp_error( $meta_type ) ) { + return $meta_type; + } else { + $this->save_to_metadata( $data, $meta_type ); + } } } @@ -167,27 +202,21 @@ public function set( $data ) { /** * This method handles the deletion of a transient * - * @return void + * @return mixed|void|WP_Error * @access public * @throws Exception */ public function delete() { - switch ( $this->transient_object->cache_type ) { - case 'transient': - $this->delete_from_transient(); - break; - case 'post_meta': - $this->delete_from_metadata( 'post' ); - break; - case 'term_meta': - $this->delete_from_metadata( 'term' ); - break; - case 'user_meta': - $this->delete_from_metadata( 'user' ); - break; - default: - throw new Exception( __( 'When registering your transient, you used an invalid cache type. Valid options are transient, post_meta, term_meta.', 'dfm-transients' ) ); + if ( 'transient' === $this->transient_object->cache_type ) { + $this->delete_from_transient(); + } else { + $meta_type = DFM_Transient_Utils::get_meta_type( $this->transient_object->cache_type ); + if ( is_wp_error( $meta_type ) ) { + return $meta_type; + } else { + $this->delete_from_metadata( $meta_type ); + } } } @@ -267,7 +296,7 @@ private function get_from_transient() { */ private function get_from_meta( $type ) { - $data = get_metadata( $type, $this->modifier, $this->key, true ); + $data = get_metadata( $type, $this->object_id, $this->key, true ); if ( empty( $data ) ) { $data_exists = metadata_exists( $type, $this->modifier, $this->key ); @@ -287,19 +316,26 @@ private function get_from_meta( $type ) { */ private function get_transient_data( $data ) { + // Check to see if we set a backwards compatible modifier + if ( 'transient' !== $this->transient_object->cache_type && ! empty( $this->bc_modifier ) ) { + $modifier = $this->bc_modifier; + } else { + $modifier = $this->modifier; + } + if ( false === $data || ( defined( 'DFM_TRANSIENTS_HOT_RELOAD' ) && true === DFM_TRANSIENTS_HOT_RELOAD ) ) { if ( true === $this->doing_retry() ) { return false; } - $data = call_user_func( $this->transient_object->callback, $this->modifier ); + $data = call_user_func( $this->transient_object->callback, $modifier, $this->object_id ); $this->set( $data ); } elseif ( $this->is_expired( $data ) && ! $this->is_locked() ) { $this->lock_update(); if ( $this->should_soft_expire() ) { - new DFM_Async_Handler( $this->transient, $this->modifier, $this->lock_key ); + new DFM_Async_Handler( $this->transient, $modifier, $this->object_id, $this->lock_key ); } else { - $data = call_user_func( $this->transient_object->callback, $this->modifier ); + $data = call_user_func( $this->transient_object->callback, $modifier, $this->object_id ); $this->set( $data ); $this->unlock_update(); } @@ -359,7 +395,11 @@ private function save_to_metadata( $data, $type ) { ); } - update_metadata( $type, $this->modifier, $this->key, $data ); + $r = update_metadata( $type, $this->object_id, $this->key, $data ); + + if ( ! empty( $this->modifier ) ) { + $this->add_meta_map( $type, $this->transient_object->key ); + } } @@ -383,7 +423,76 @@ private function delete_from_transient() { * @access private */ private function delete_from_metadata( $type ) { - delete_metadata( $type, $this->modifier, $this->key ); + if ( ! empty( $this->modifier ) ) { + $meta_map = $this->get_meta_map( $type, $this->object_id, $this->key ); + if ( ! empty( $meta_map ) && is_array( $meta_map ) ) { + foreach ( $meta_map as $key ) { + delete_metadata( $type, $this->object_id, $key ); + } + } + } + delete_metadata( $type, $this->object_id, $this->key ); + } + + /** + * Add the transient keys to the mapping so they can be found easily later + * + * @param string $type The type of meta to store the map in + * @param string $transient_key The key for the transient group to create the map key off of + * + * @return void + * @access private + */ + private function add_meta_map( $type, $transient_key ) { + + $map = get_metadata( $type, $this->object_id, self::meta_map_key( $transient_key ), true ); + if ( ! empty( $map ) ) { + $map[ $this->modifier ] = $this->key; + } else { + $map = [ $this->modifier => $this->key ]; + } + + update_metadata( $type, $this->object_id, $this->meta_map_key( $transient_key ), array_unique( $map ) ); + + } + + /** + * Return a map of meta keys for all of the modifiers for a transient registered in the group + * + * @param string $type The meta type to use for storage + * @param int $object_id The ID of the object to store the meta map in + * @param string $transient_key Key for the transient group the transient belongs to + * + * @return array + * @access public + * @static + */ + public static function get_meta_map( $type, $object_id, $transient_key ) { + + if ( is_wp_error( $type ) ) { + return array(); + } + + $map = get_metadata( $type, $object_id, self::meta_map_key( $transient_key ), true ); + if ( empty( $map ) ) { + $map = []; + } + + return $map; + + } + + /** + * Return the meta map key for the transient group + * + * @param string $transient_key Transient key for the transient group + * + * @return string + * @access public + * @static + */ + public static function meta_map_key( $transient_key ) { + return self::$prefix . $transient_key . '_map'; } /** @@ -393,6 +502,7 @@ private function delete_from_metadata( $type ) { * * @access private * @return void + * @throws Exception */ private function facilitate_retry() { @@ -455,7 +565,7 @@ private function hash_key( $key ) { * If the storage type is *_meta then prepend the prefix after we hash so we can * still find it for debugging */ - $hashed_key = $this->prefix . $hashed_key; + $hashed_key = self::$prefix . $hashed_key; } return $hashed_key; @@ -472,7 +582,7 @@ private function cache_key() { $key = $this->transient_object->key; - if ( 'transient' === $this->transient_object->cache_type && ! empty( $this->modifier ) ) { + if ( ! empty( $this->modifier ) ) { // Add the unique modifier to the key for regular transients $key = $key . '_' . $this->modifier; } @@ -483,7 +593,7 @@ private function cache_key() { if ( in_array( $this->transient_object->cache_type, $this->meta_types, true ) ) { // Add the prefix to transients stored in meta only so they can be identified - $key = $this->prefix . $key; + $key = self::$prefix . $key; } return $key; diff --git a/includes/template-tags.php b/includes/template-tags.php index d727117..cd51b12 100644 --- a/includes/template-tags.php +++ b/includes/template-tags.php @@ -1,8 +1,6 @@ get(), $transient, $modifier ); @@ -116,16 +117,18 @@ function dfm_get_transient( $transient, $modifier = '' ) { } /** - * dfm_set_transient - * * Handles the setting of data to a particular transient * - * @param string $transient The name of the transient we would like to set data to - * @param string $data The data we want to set to the transient - * @param string|int $modifier The unique modifier for the transient. In the case of transients stored in metadata, - * this value should be the object ID related to this piece of metadata. + * @param string $transient The name of the transient we would like to set data to + * @param string $data The data we want to set to the transient + * @param string|int $modifier The unique modifier for the transient to be appended to the + * transient name for the key + * @param null|int $object_id ID of the object the transient data is related to + * + * @throws Exception + * @return void */ -function dfm_set_transient( $transient, $data, $modifier = '' ) { +function dfm_set_transient( $transient, $data, $modifier = '', $object_id = null ) { $transients = new DFM_Transients( @@ -133,8 +136,9 @@ function dfm_set_transient( $transient, $data, $modifier = '' ) { * Filters the name of the transient to set * * @param string $transient The name of the transient - * @param string $modifier The unique modifier - * @param mixed $data The data you want to save to your transient + * @param string $modifier The unique modifier + * @param mixed $data The data you want to save to your transient + * * @return string $transient The name of the transient to set data to */ apply_filters( 'dfm_transients_set_transient_name', $transient, $modifier, $data ), @@ -142,12 +146,14 @@ function dfm_set_transient( $transient, $data, $modifier = '' ) { /** * Filters the unique modifier for the transient * - * @param string $modifier The unique modifier + * @param string $modifier The unique modifier * @param string $transient The name of the transient we want to save data to - * @param mixed $data The data that we want to save to the transient + * @param mixed $data The data that we want to save to the transient + * * @return string $modifier The unique modifier for the transient */ - apply_filters( 'dfm_transients_set_transient_modifier', $modifier, $transient, $data ) + apply_filters( 'dfm_transients_set_transient_modifier', $modifier, $transient, $data ), + $object_id ); // Invoke the set method @@ -156,18 +162,17 @@ function dfm_set_transient( $transient, $data, $modifier = '' ) { } /** - * dfm_delete_transient - * * Handles the deletion of a single transient * - * @param string $transient Name of the transient you want to delete. Must match what is set when registering - * the transient. - * @param string|int $modifier Unique modifier for the transient that you want to delete. In the case of a transient stored - * in metadata it must be the object ID that the metadata is related to. + * @param string $transient Name of the transient you want to delete. Must match what is set + * when registering the transient. + * @param string|int $modifier Unique modifier for the transient that you want to delete. + * @param null|int $object_id ID of the object the transient data is related to + * * @return void - * @access public + * @throws Exception */ -function dfm_delete_transient( $transient, $modifier = '' ) { +function dfm_delete_transient( $transient, $modifier = '', $object_id = null ) { $transients = new DFM_Transients( @@ -187,7 +192,8 @@ function dfm_delete_transient( $transient, $modifier = '' ) { * @param string $transient Name of the transient to be deleted * @return string $modifier Unique modifier for the transient to be deleted */ - apply_filters( 'dfm_transients_delete_transient_modifier', $modifier, $transient ) + apply_filters( 'dfm_transients_delete_transient_modifier', $modifier, $transient ), + $object_id ); // Invoke the delete method diff --git a/tests/test-class-dfm-transient-hook.php b/tests/test-class-dfm-transient-hook.php index 555cb68..9f337dc 100644 --- a/tests/test-class-dfm-transient-hook.php +++ b/tests/test-class-dfm-transient-hook.php @@ -5,7 +5,14 @@ class Test_Class_DFM_Transient_Hook extends WP_UnitTestCase { * Test to make sure the hooks get setup correctly */ public function testHookAddition() { - $hook_obj = new DFM_Transient_Hook( 'testName', 'testHook', true ); + + $transient_object = [ + 'cache_type' => 'transient', + 'callback' => '__return_false', + 'key' => 'test', + ]; + + $hook_obj = new DFM_Transient_Hook( 'testName', (object) $transient_object, 'testHook', true ); $this->assertTrue( has_filter( 'testHook' ) ); } @@ -22,7 +29,10 @@ public function testNonAsyncHandlerUpdate() { }, ] ); - $hook_obj = new DFM_Transient_Hook( $transient_name, 'testHook', false ); + global $dfm_transients; + $transient_obj = $dfm_transients[ $transient_name ]; + + $hook_obj = new DFM_Transient_Hook( $transient_name, $transient_obj, 'testHook', false ); $hook_obj->spawn(); @@ -42,12 +52,15 @@ public function testUpdateFailureBecauseOfLock() { }, ] ); + global $dfm_transients; + $transient_obj = $dfm_transients[ $transient_name ]; + /** * Lock the update manually */ set_transient( 'dfm_lt_' . $transient_name, '1234' ); - $hook_obj = new DFM_Transient_Hook( $transient_name, 'testHook', false ); + $hook_obj = new DFM_Transient_Hook( $transient_name, $transient_obj, 'testHook', false ); $hook_obj->spawn(); $this->assertFalse( get_transient( $transient_name ) ); @@ -66,7 +79,10 @@ public function testNonAsyncUpdateMultipleModifiers() { }, ] ); - $hook_obj = new DFM_Transient_Hook( $transient_name, 'testHook', false, function() { + global $dfm_transients; + $transient_obj = $dfm_transients[ $transient_name ]; + + $hook_obj = new DFM_Transient_Hook( $transient_name, $transient_obj, 'testHook', false, function() { return [ 1, 2, 3 ]; } ); @@ -85,7 +101,10 @@ public function testShortCircuitHookUpdate() { $transient_name = 'testShortCircuitHookUpdate'; dfm_register_transient( $transient_name, [ 'callback' => '__return_true' ] ); - $hook_obj = new DFM_Transient_Hook( $transient_name, 'testHook', false, function() { + global $dfm_transients; + $transient_obj = $dfm_transients[ $transient_name ]; + + $hook_obj = new DFM_Transient_Hook( $transient_name, $transient_obj, 'testHook', false, function() { return false; } ); @@ -100,7 +119,12 @@ public function testShortCircuitHookUpdate() { */ public function testAsyncHandlerUpdate() { - $hook_obj = new DFM_Transient_Hook( 'testTransient', 'testHook', true ); + $transient_object = [ + 'cache_type' => 'transient', + 'callback' => '__return_false', + 'key' => 'test', + ]; + $hook_obj = new DFM_Transient_Hook( 'testTransient', (object) $transient_object, 'testHook', true ); $hook_obj->spawn(); global $wp_filter; @@ -113,7 +137,12 @@ public function testAsyncHandlerUpdate() { */ public function testMultipleAsyncHandlerUpdates() { - $hook_obj = new DFM_Transient_Hook( 'testTransient', 'testHook', true, function() { + $transient_object = [ + 'cache_type' => 'transient', + 'callback' => '__return_false', + 'key' => 'test', + ]; + $hook_obj = new DFM_Transient_Hook( 'testTransient', (object) $transient_object, 'testHook', true, function() { return [ 1, 2, 3 ]; } ); @@ -124,4 +153,162 @@ public function testMultipleAsyncHandlerUpdates() { } + /** + * Test that the hook handler returns the proper data to the callback for post object cache transients without modifiers + */ + public function testPostObjectCacheHandlerUpdate() { + + $transient_name = 'testPostObjectCacheHandlerUpdate'; + $post_id = $this->factory->post->create(); + $return_string = 'Post ID: %d, Modifier: %s'; + dfm_register_transient( $transient_name, [ + 'cache_type' => 'post_meta', + 'callback' => function( $modifier, $post_id ) use ( $return_string ) { + return sprintf( $return_string, $post_id, $modifier ); + } + ] ); + + global $dfm_transients; + $transient_obj = $dfm_transients[ $transient_name ]; + + $hook_obj = new DFM_Transient_Hook( $transient_name, (object) $transient_obj, $transient_name, false, function( $id ) { + return $id[0]; + } ); + + do_action( $transient_name, $post_id ); + + $expected = sprintf( $return_string, $post_id, '' ); + $transient_obj = new DFM_Transients( $transient_name, '', $post_id ); + $actual = get_post_meta( $post_id, $transient_obj->key, true ); + $this->assertEquals( $expected, $actual ); + $this->assertEquals( [], DFM_Transients::get_meta_map( 'post', $post_id, $transient_name ) ); + $this->assertFalse( metadata_exists( 'post', $post_id, 'dfm_transient_' . $transient_name . '_map' ) ); + + } + + /** + * Test that all transients within a transient group are refreshed when just the object ID is returned from the hook callback + */ + public function testTermObjectCacheHandlerWithModifiers() { + + $transient_name = 'testTermObjectCacheHandlerWithModifiers'; + $term_id = $this->factory->category->create(); + $return_string = 'Term ID: %d, Modifier: %s'; + + dfm_register_transient( $transient_name, [ + 'cache_type' => 'term_meta', + 'callback' => function( $modifier, $term_id ) use ( $return_string ) { + return sprintf( $return_string, $term_id, $modifier ); + }, + 'hash_key' => true, + ] ); + + $modifiers = [ + 'rainbow', + 'apples', + 'unicorn', + ]; + + /** + * Add some data to start out with, so we get a map. + */ + foreach ( $modifiers as $modifier ) { + $transient_data = dfm_get_transient( $transient_name, $modifier, $term_id ); + $this->assertEquals( sprintf( $return_string, $term_id, $modifier ), $transient_data ); + } + + $transient_obj = new DFM_Transients( $transient_name, $modifiers[0], $term_id ); + $transient_obj->delete(); + + global $dfm_transients; + $transient_obj = $dfm_transients[ $transient_name ]; + + $hook_obj = new DFM_Transient_Hook( $transient_name, (object) $transient_obj, $transient_name, false, function( $term_id ) { + return $term_id[0]; + } ); + + do_action( $transient_name, $term_id ); + + $transient_keys = DFM_Transients::get_meta_map( 'term', $term_id, $transient_obj->key ); + $this->assertEquals( 3, count( $transient_keys ) ); + + foreach ( $transient_keys as $modifier => $key ) { + $this->assertEquals( sprintf( $return_string, $term_id, $modifier ), get_term_meta( $term_id, $key, true ) ); + } + + } + + /** + * Test that all the transients on a user type cache are cleared only when the modifiers for the + * object ID are passed back in the callback, or if it returns an empty array for the ID. + */ + public function testUserObjectCacheHandlerWithModifiers() { + + $transient_name = 'testUserObjectCacheHandlerWithModifiers'; + $users = [ + $this->factory->user->create(), + $this->factory->user->create(), + $this->factory->user->create(), + $this->factory->user->create(), + ]; + $return_string = 'User ID: %d, Modifier: %s'; + + dfm_register_transient( $transient_name, [ + 'cache_type' => 'user_meta', + 'callback' => function( $modifier, $user_id ) use ( $return_string ){ + return sprintf( $return_string, $user_id, $modifier ); + } + ] ); + + $modifiers = [ + 'blue', + 'purple', + 'test', + ]; + + foreach ( $users as $user ) { + foreach( $modifiers as $modifier ) { + dfm_set_transient( $transient_name, 'test', $modifier, $user ); + } + } + + global $dfm_transients; + $transient_obj = $dfm_transients[ $transient_name ]; + + $hook_obj = new DFM_Transient_Hook( $transient_name, (object) $transient_obj, $transient_name, false, function( $data ) { + return $data[0]; + } ); + + $hook_data = [ + $users[0] => $modifiers, + $users[1] => $modifiers, + $users[2] => array_slice( $modifiers, 0, 2 ), + $users[3] => [], + ]; + + do_action( $transient_name, $hook_data ); + + $transient_keys = []; + foreach ( $users as $user ) { + $transient_keys[ $user ] = DFM_Transients::get_meta_map( 'user', $user, $transient_name ); + } + + foreach ( $transient_keys as $user_id => $keys ) { + $hook_modifiers = $hook_data[ $user_id ]; + if ( is_array( $hook_modifiers ) && ! empty( $hook_modifiers ) ) { + foreach ( $hook_modifiers as $modifier ) { + $this->assertEquals( sprintf( $return_string, $user_id, $modifier ), get_user_meta( $user_id, $keys[ $modifier ], true ) ); + } + } else { + foreach ( $modifiers as $modifier ) { + $this->assertEquals( sprintf( $return_string, $user_id, $modifier ), get_user_meta( $user_id, $keys[ $modifier ], true ) ); + } + } + } + + // Test to make sure the "test" modifier on the second to last user isn't updated since we didn't pass it to the hook in the $hook_data var + $this->assertEquals( 'test', get_user_meta( $users[2], $transient_keys[ $users[2] ]['test'], true ) ); + + } + } diff --git a/tests/test-class-dfm-transients.php b/tests/test-class-dfm-transients.php index 24017f6..d7807e5 100644 --- a/tests/test-class-dfm-transients.php +++ b/tests/test-class-dfm-transients.php @@ -355,13 +355,13 @@ public function testGetTransientFromTermMeta() { $transient_name = 'testGetTransientFromTermMeta'; $this->register_transient( $transient_name, [ - 'callback' => function( $modifier ) { - return 'term id: ' . $modifier; + 'callback' => function( $modifier, $term_id ) { + return 'term id: ' . $term_id; }, 'cache_type' => 'term_meta', ] ); - $transient_obj = new DFM_Transients( $transient_name, $term_id ); + $transient_obj = new DFM_Transients( $transient_name, '', $term_id ); $key = $transient_obj->key; $actual = $transient_obj->get(); @@ -379,6 +379,137 @@ public function testGetTransientFromTermMeta() { } + /** + * Test getting a transient from term meta when there are multiple modifiers + * @throws Exception + */ + public function testGetTransientFromTermMetaWithModifier() { + + $term_id = $this->factory->term->create(); + + $transient_name = 'testGetTransientFromTermMetaWithModifier'; + $return_string = 'Term ID: %d, Modifier: %s'; + $test_value = 'some test value'; + + $this->register_transient( $transient_name, [ + 'callback' => function( $modifier, $term_id ) use ( $return_string ) { + return sprintf( $return_string, $term_id, $modifier ); + }, + 'cache_type' => 'term_meta', + 'hash_key' => true, + ] ); + + $modifier_1 = 'first_mod'; + $modifier_2 = 'second_mod'; + + $transient_obj_1 = new DFM_Transients( $transient_name, $modifier_1, $term_id ); + $transient_obj_2 = new DFM_Transients( $transient_name, $modifier_2, $term_id ); + $key_1 = $transient_obj_1->key; + $key_2 = $transient_obj_2->key; + + $this->assertEquals( sprintf( $return_string, $term_id, $modifier_1 ), $transient_obj_1->get() ); + $this->assertEquals( sprintf( $return_string, $term_id, $modifier_2 ), $transient_obj_2->get() ); + + $transient_obj_1->set( $test_value ); + $transient_obj_2->set( $test_value ); + $this->assertEquals( $test_value, $transient_obj_1->get() ); + $this->assertEquals( $test_value, $transient_obj_2->get() ); + + $transient_obj_1->delete(); + $this->assertEquals( '', get_term_meta( $term_id, $key_1, true ) ); + $this->assertEquals( sprintf( $return_string, $term_id, $modifier_1 ), $transient_obj_1->get() ); + $this->assertEquals( sprintf( $return_string, $term_id, $modifier_1 ), get_term_meta( $term_id, $key_1, true ) ); + + $transient_obj_2->delete(); + $this->assertEquals( '', get_term_meta( $term_id, $key_2, true ) ); + $this->assertEquals( sprintf( $return_string, $term_id, $modifier_2 ), $transient_obj_2->get() ); + $this->assertEquals( sprintf( $return_string, $term_id, $modifier_2 ), get_term_meta( $term_id, $key_2, true ) ); + + } + + /** + * Test for retrieving transients with a modifier using the post meta cache + */ + public function testGetTransientFromPostMetaWithModifier() { + + $post_id = $this->factory->post->create(); + + $transient_name = 'testGetTransientFromPostMetaWithModifier'; + $return_string = 'Post ID: %d, Modifier: %s'; + $test_value = 'test value'; + + $this->register_transient( $transient_name, [ + 'callback' => function( $modifier, $post_id ) use ( $return_string ) { + return sprintf( $return_string, $post_id, $modifier ); + }, + 'cache_type' => 'post_meta', + 'async_updates' => true, + 'update_hooks' => 'updated_post_meta', + 'expiration' => HOUR_IN_SECONDS, + 'soft_expiration' => true, + ] ); + + $modifier_1 = 'dragons'; + $modifier_2 = 'rainbows'; + + $transient_obj_1 = new DFM_Transients( $transient_name, $modifier_1, $post_id ); + $transient_obj_2 = new DFM_Transients( $transient_name, $modifier_2, $post_id ); + $key_1 = $transient_obj_1->key; + $key_2 = $transient_obj_2->key; + + $this->assertEquals( sprintf( $return_string, $post_id, $modifier_1 ), $transient_obj_1->get() ); + $this->assertEquals( sprintf( $return_string, $post_id, $modifier_2 ), $transient_obj_2->get() ); + + $transient_obj_1->set( $test_value ); + $transient_obj_2->set( $test_value ); + $this->assertEquals( $test_value, $transient_obj_1->get() ); + $this->assertEquals( $test_value, $transient_obj_2->get() ); + + $transient_obj_1->delete(); + $this->assertEquals( '', get_post_meta( $post_id, $key_1, true) ); + $this->assertEquals( sprintf( $return_string, $post_id, $modifier_1 ), $transient_obj_1->get() ); + $this->assertEquals( sprintf( $return_string, $post_id, $modifier_1 ), get_post_meta( $post_id, $key_1, true )['data'] ); + $this->assertArrayHasKey( 'expiration', get_post_meta( $post_id, $key_1, true ) ); + + $transient_obj_2->delete(); + $this->assertEquals( '', get_post_meta( $post_id, $key_2, true) ); + $this->assertEquals( sprintf( $return_string, $post_id, $modifier_2 ), $transient_obj_2->get() ); + $this->assertEquals( sprintf( $return_string, $post_id, $modifier_2 ), get_post_meta( $post_id, $key_2, true )['data'] ); + $this->assertArrayHasKey( 'expiration', get_post_meta( $post_id, $key_2, true ) ); + + } + + /** + * Tests to ensure we haven't broken object type cache transients before we implemented modifiers for them + */ + public function testTermMetaTransientBackwardsCompat() { + + $term_id = $this->factory->term->create(); + $transient_name = 'testTermMetaTransientBackwardsCompat'; + $return_string = 'term id: %d'; + + $this->register_transient( $transient_name, [ + 'callback' => function( $modifier ) use ( $return_string ) { + return sprintf( $return_string, $modifier ); + }, + 'cache_type' => 'term_meta', + ] ); + + $transient_obj = new DFM_Transients( $transient_name, $term_id ); + $key = $transient_obj->key; + + $this->assertEquals( sprintf( $return_string, $term_id ), $transient_obj->get() ); + + $transient_obj->set( 'test data' ); + $this->assertEquals( 'test data', $transient_obj->get() ); + + $transient_obj->delete(); + $this->assertEquals( '', get_term_meta( $term_id, $key, true ) ); + $this->assertEquals( sprintf( $return_string, $term_id ), $transient_obj->get() ); + $this->assertEquals( sprintf( $return_string, $term_id ), get_term_meta( $term_id, $key, true ) ); + + } + /** * Test to make sure getting transient data from post meta works correctly */ @@ -386,19 +517,20 @@ public function testGetTransientFromPostMeta() { $post_id = $this->factory->post->create(); $transient_name = 'testGetTransientFromPostMeta'; + $custom_modifier = 'test'; $this->register_transient( $transient_name, [ 'callback' => function( $modifier ) { - return 'post id: ' . $modifier; + return 'transient data: ' . $modifier; }, 'cache_type' => 'post_meta', ] ); - $transient_obj = new DFM_Transients( $transient_name, $post_id ); + $transient_obj = new DFM_Transients( $transient_name, $custom_modifier, $post_id ); $key = $transient_obj->key; $actual = $transient_obj->get(); - $expected = 'post id: ' . $post_id; + $expected = 'transient data: ' . $custom_modifier; $this->assertEquals( $expected, $actual ); $transient_obj->set( 'some test value' ); @@ -420,8 +552,8 @@ public function testGetTransientFromMetaWithExpiration() { $transient_name = 'testGetTransientFromMetaWithExpiration'; $this->register_transient( $transient_name, [ - 'callback' => function( $modifier ) { - return 'post id: ' . $modifier; + 'callback' => function() { + return 'transient data for post'; }, 'cache_type' => 'post_meta', 'expiration' => DAY_IN_SECONDS, @@ -429,11 +561,11 @@ public function testGetTransientFromMetaWithExpiration() { 'hash_key' => true, ] ); - $transient_obj = new DFM_Transients( $transient_name, $post_id ); + $transient_obj = new DFM_Transients( $transient_name, '', $post_id ); $key = $transient_obj->key; $actual = $transient_obj->get(); - $expected = 'post id: ' . $post_id; + $expected = 'transient data for post'; $this->assertEquals( $expected, $actual ); $transient_obj->set( 'some test value' ); @@ -455,28 +587,27 @@ public function testGetTransientFromUserMeta() { $user_id = $this->factory->user->create(); $transient_name = 'testGetTransientFromUserMeta'; + $return_string = 'User ID: %d, Modifier: %s'; $this->register_transient( $transient_name, [ - 'callback' => function( $modifier ) { - return 'user id: ' . $modifier; + 'callback' => function( $modifier, $user_ID ) use ( $return_string ) { + return sprintf( $return_string, $user_ID, $modifier ); }, 'cache_type' => 'user_meta', ] ); - $transient_obj = new DFM_Transients( $transient_name, $user_id ); + $transient_obj = new DFM_Transients( $transient_name, '', $user_id ); $key = $transient_obj->key; - $actual = $transient_obj->get(); - $expected = 'user id: ' . $user_id; - $this->assertEquals( $actual, $expected ); + $this->assertEquals( sprintf( $return_string, $user_id, '' ), $transient_obj->get() ); $transient_obj->set( 'some test value' ); $this->assertEquals( 'some test value', $transient_obj->get() ); $transient_obj->delete(); $this->assertEquals( '', get_user_meta( $user_id, $key, true ) ); - $this->assertEquals( $expected, $transient_obj->get() ); - $this->assertEquals( $expected, get_user_meta( $user_id, $key, true ) ); + $this->assertEquals( sprintf( $return_string, $user_id, '' ), $transient_obj->get() ); + $this->assertEquals( sprintf( $return_string, $user_id, '' ), get_user_meta( $user_id, $key, true ) ); } @@ -582,10 +713,7 @@ public function testGetTransientFromNonExistentType() { $transient_obj = new DFM_Transients( $transient_name, '' ); $this->assertTrue( is_wp_error( $transient_obj->get() ) ); - - $this->setExpectedException( 'Exception', 'When registering your transient, you used an invalid cache type. Valid options are transient, post_meta, term_meta.' ); - $this->expectException( $transient_obj->set( 'test' ) ); - + $this->assertTrue( is_wp_error( $transient_obj->set( 'test' ) ) ); } @@ -600,8 +728,7 @@ public function testDeleteTransientFromNonExistentType() { $transient_obj = new DFM_Transients( $transient_name, '' ); - $this->setExpectedException( 'Exception', 'When registering your transient, you used an invalid cache type. Valid options are transient, post_meta, term_meta.' ); - $this->expectException( $transient_obj->delete() ); + $this->assertTrue( is_wp_error( $transient_obj->delete() ) ); }