diff --git a/README.md b/README.md index 942c770..37b68f7 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,13 @@ $result = dfm_get_transient( 'sample_transient', '' ); ## 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). + +## 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. + +## Retries +Since version 1.1.0 there is a retry facilitation system for DFM Transients. This is helpful if you are storing data from an external API, and want to serve stale data if the API is down. To use this feature, all you have to do is return `false` or a `wp_error` object in your transient callback if your remote request failed. This will then store the stale expired data back into the transient, and will use an expiration timeout that increases exponentially every time it fails to fetch the data. Essentially it stores a `failed` value in the cache for each transient, and adds one to the value every time the retry method runs. It then mulitplies this number by its self to figure out how many minutes it should set the expiration to. For example, if the fetch has failed 5 times, it will set the timeout to 25 minutes, and will retry again after that. + ## Contributing To contribute to this repo, please fork it and submit a pull request. If there is a larger feature you would like to see, or something you would like to discuss, please open an issue. ## Copyright diff --git a/dfm-transients.php b/dfm-transients.php index cc4e005..523c6c8 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.0.1 +* Version: 1.1.0 * Author: Ryan Kanner, Digital First Media * License: MIT */ @@ -23,3 +23,8 @@ if ( is_admin() ) { require_once( plugin_dir_path( __FILE__ ) . 'includes/admin/class-dfm-transient-admin.php' ); } + +// CLI Commands +if ( defined( 'WP_CLI' ) && true === WP_CLI ) { + require_once( plugin_dir_path( __FILE__ ) . 'includes/cli.php' ); +} diff --git a/includes/class-dfm-transients.php b/includes/class-dfm-transients.php index c9933ed..36d2835 100644 --- a/includes/class-dfm-transients.php +++ b/includes/class-dfm-transients.php @@ -36,7 +36,7 @@ class DFM_Transients { * @var string * @access private */ - private $modifier = ''; + public $modifier = ''; /** * The storage key for the transient @@ -44,7 +44,7 @@ class DFM_Transients { * @var string * @access private */ - private $key = ''; + public $key = ''; /** * Lock stored in transient. @@ -62,6 +62,14 @@ class DFM_Transients { */ private $lock_key = ''; + /** + * Flag for if we are attempting a retry + * + * @var $doing_retry bool + * @access private + */ + private $doing_retry = false; + /** * DFM_Transients constructor. * @@ -125,7 +133,7 @@ public function set( $data ) { } if ( false === $data || is_wp_error( $data ) ) { - return; + $this->facilitate_retry(); } switch ( $this->transient_object->cache_type ) { @@ -147,6 +155,37 @@ public function set( $data ) { } + /** + * This method handles the deletion of a transient + * + * @return WP_Error|void + * @access public + */ + public function delete() { + + if ( ! isset( $this->transient_object ) ) { + return new WP_Error( 'invalid-transient', __( 'You are trying to retrieve a transient that doesn\'t exist', 'dfm-transients' ) ); + } + + 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: + new WP_Error( 'invalid-cache-type', __( 'When registering your transient, you used an invalid cache type. Valid options are transient, post_meta, term_meta.', 'dfm-transients' ) ); + } + + } + /** * Locks the ability to update the transient data. This will prevent race conditions. * @@ -206,7 +245,11 @@ private function get_from_transient() { $data = get_transient( $this->key ); - if ( false === $data ) { + 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 ); $this->set( $data ); } elseif ( $this->is_expired( $data ) && ! $this->is_locked() ) { @@ -238,8 +281,18 @@ private function get_from_transient() { private function get_from_meta( $type ) { $data = get_metadata( $type, $this->modifier, $this->key, true ); + + $data_exists = true; + + if ( empty( $data ) ) { + $data_exists = metadata_exists( $type, $this->modifier, $this->key ); + } + + if ( false === $data_exists || ( defined( 'DFM_TRANSIENTS_HOT_RELOAD' ) && true === DFM_TRANSIENTS_HOT_RELOAD ) ) { - if ( false === $data ) { + if ( true === $this->doing_retry ) { + return false; + } $data = call_user_func( $this->transient_object->callback, $this->modifier ); $this->set( $data ); } elseif ( $this->is_expired( $data ) && ! $this->is_locked() ) { @@ -308,6 +361,82 @@ private function save_to_metadata( $data, $type ) { } + /** + * Deletes a transient stored in the default transient storage engine + * + * @access private + * @uses delete_transient() + * @return void + */ + private function delete_from_transient() { + delete_transient( $this->key ); + } + + /** + * Deletes a transient stored in metadata + * + * @param string $type The object type related to the metadata + * @uses delete_metadata() + * @return void + * @access private + */ + private function delete_from_metadata( $type ) { + delete_metadata( $type, $this->modifier, $this->key ); + } + + /** + * If a callback function fails to return the correct data, this will store the stale data back into the + * transient, and then set the expiration of the data at an exponential scale, so we are not constantly + * retrying to get the data (if an API is down or something). + * + * @access private + * @return void + */ + private function facilitate_retry() { + + // Set flag while doing a retry to prevent infinite loops. + $this->doing_retry = true; + + // Retrieve the stale data. + $current_data = $this->get(); + + // If there is nothing already stored for the transient, bail. + if ( false === $current_data ) { + return; + } + + // Store the expiration set when registering the transient. Our timeout should not exceed this number. + $max_expiration = $this->transient_object->expiration; + + // Retrieve the cache fail amount from the cache + $failed_num = wp_cache_get( $this->key . '_failed', 'dfm_transients_retry' ); + + // Default to 1 failure if there's nothing set, or it's set to zero. This is so it doesn't mess with + // the `pow` func. + if ( false === $failed_num || 0 === $failed_num ) { + $failures = 1; + } else { + $failures = $failed_num; + } + + // Generate the new expiration time. This essentially just muliplies the amount of failures by itself, and + // then multiplies it by one minute to get the expiration, so if it is retrying it for the 5th time, it will + // do 5*5 (which is 25) so it will set the retry to 25 minutes. + $new_expiration = ( pow( $failures, 2 ) * MINUTE_IN_SECONDS ); + + // Only set the new expiration if it's less than the original registered expiration. + if ( $new_expiration < $max_expiration ) { + $this->transient_object->expiration = $new_expiration; + } + + // Save the stale data with the new expiration + $this->set( $current_data ); + + // Add 1 to the the failures in the cache. + wp_cache_set( $this->key . '_failed', ( $failures + 1 ), 'dfm_transients_retry', DAY_IN_SECONDS ); + + } + /** * Hashes storage key * @@ -341,12 +470,17 @@ private function cache_key() { $key = $this->hash_key( $key ); } - if ( 'post_meta' === $this->transient_object->cache_type || 'term_meta' === $this->transient_object->cache_type ) { - $key = $this->prefix . $key; - } - - if ( 'transient' === $this->transient_object->cache_type && ! empty( $this->modifier ) ) { - $key = $key . '_' . $this->modifier; + switch( $this->transient_object->cache_type ) { + case 'post_meta': + case 'term_meta': + case 'user_meta': + $key = $this->prefix . $key; + break; + case 'transient': + if ( ! empty( $this->modifier ) ) { + $key = $key . '_' . $this->modifier; + } + break; } return $key; @@ -390,7 +524,7 @@ private function should_soft_expire() { * @return bool */ private function is_expired( $data ) { - if ( '' !== $this->transient_object->expiration && is_array( $data ) && $data['expiration'] < time() ) { + if ( ! empty( $this->transient_object->expiration ) && is_array( $data ) && $data['expiration'] < time() ) { return true; } else { return false; diff --git a/includes/cli.php b/includes/cli.php new file mode 100644 index 0000000..ab8cd35 --- /dev/null +++ b/includes/cli.php @@ -0,0 +1,361 @@ + + * : Name of the transient you want to retrieve information about + * + * [...] + * : List of modifiers to get data about + * + * [--format] + * : Render the output in a particular format + * --- + * default: table + * options: + * - table + * - csv + * - ids + * - json + * - count + * - yaml + * --- + * + * [--limit] + * : The amount of transients to retrieve. Pass -1 for no limit. + * --- + * default: 100 + * --- + * + * [--fields] + * : The fields you would like to return + * + * ## AVAILABLE FIELDS + * * modifier + * * data + * + * @param array $args + * @param $assoc_args + */ + public function get( $args, $assoc_args ) { + + $transient_name = array_shift( $args ); + $modifiers = $args; + + $this->supported_props = array( 'modifier', 'data' ); + + $options = wp_parse_args( $assoc_args, array( + 'format' => '', + 'limit' => 100, + ) + ); + + if ( empty( $transient_name ) ) { + parent::error( 'A transient name must be passed' ); + } + + $transient_obj = new DFM_Transients( $transient_name, '' ); + $transient_type = $transient_obj->transient_object->cache_type; + $transient_key = $transient_obj->key; + + switch ( $options['format'] ) { + case 'ids': + $data = $this->get_all_modifiers( $transient_key, $transient_type, false, $options['limit'] ); + break; + case 'count': + $data = $this->get_all_modifiers( $transient_key, $transient_type, true ); + $data = ( ! empty( $data ) && is_array( $data ) ) ? $data[0] : 0; + break; + default: + if ( 'all' === $modifiers[0] ) { + $modifier_keys = $this->get_all_modifiers( $transient_key, $transient_type, false, $options['limit'] ); + } elseif ( ! empty( $modifiers ) ) { + $modifier_keys = $modifiers; + } else { + $data = array( + array( + 'modifier' => '', + 'data' => dfm_get_transient( $transient_name ), + ), + ); + } + + if ( ! isset( $data ) ) { + + $data = array(); + + if ( isset( $modifier_keys ) && ! empty( $modifier_keys ) && is_array( $modifier_keys ) ) { + foreach ( $modifier_keys as $modifier_key ) { + $transient_obj->modifier = absint( $modifier_key ); + $data[] = array( + 'modifier' => $modifier_key, + 'data' => $transient_obj->get(), + ); + } + } + } + break; + } + + if ( 'count' !== $options['format'] ) { + $this->format_output( $data, $assoc_args ); + parent::line(); + } else { + parent::success( sprintf( '%d transients found', $data ) ); + } + + } + + /** + * Sets data for a particular DFM Transient + * + * ## OPTIONS + * + * : Name of the transient you would like to set data for + * + * ... + * : List of modifiers you want to update the transient data for + * + * [--data=] + * : The new data you want to store in the transient + * + * @param $args + * @param $assoc_args + */ + public function set( $args, $assoc_args ) { + + $transient_name = array_shift( $args ); + $modifiers = $args; + + if ( empty( $transient_name ) ) { + parent::error( 'A transient name must be passed' ); + } + + $data = \WP_CLI\Utils\get_flag_value( $assoc_args, 'data', '' ); + + if ( ! empty( $modifiers ) && is_array( $modifiers ) ) { + + if ( 10 < count( $modifiers ) ) { + $progress = \WP_CLI\Utils\make_progress_bar( 'Updating Transients', count( $modifiers ) ); + } + + foreach ( $modifiers as $modifier ) { + dfm_set_transient( $transient_name, $data, absint( $modifier ) ); + if ( isset( $progress ) ) { + $progress->tick(); + } + } + + if ( isset( $progress ) ) { + $progress->finish(); + } + + parent::success( sprintf( 'Successfully updated %d transients', count( $modifiers ) ) ); + + } else { + dfm_set_transient( $transient_name, $data, '' ); + parent::success( sprintf( 'Successfully updated the %s transient', $transient_name ) ); + } + + } + + /** + * Deletes some particular DFM Transients + * + * ## OPTIONS + * + * : Name of the transient you would like to delete data for + * + * ... + * : List of modifiers you want to delete the transients for + * + * @param array $args + * @param array $assoc_args + */ + public function delete( $args, $assoc_args ) { + + $transient_name = array_shift( $args ); + $modifiers = $args; + + if ( empty( $transient_name ) ) { + parent::error( 'A transient name must be passed' ); + } + + if ( empty( $modifiers ) ) { + dfm_delete_transient( $transient_name ); + parent::success( sprintf( 'Successfully deleted transient: %s', $transient_name ) ); + } else { + + if ( count( $modifiers ) > 10 ) { + $progress = \WP_CLI\Utils\make_progress_bar( 'Deleting transients', count( $modifiers ) ); + } + + if ( is_array( $modifiers ) ) { + + foreach ( $modifiers as $modifier ) { + + dfm_delete_transient( $transient_name, $modifier ); + + if ( isset( $progress ) ) { + $progress->tick(); + } + + } + + if ( isset( $progress ) ) { + $progress->finish(); + } + + } + + parent::success( sprintf( 'Successfully deleted %d transients', count( $modifiers ) ) ); + + } + + } + + /** + * Lists all of the registered transients through DFM Transients + * + * ## OPTIONS + * [...] + * : Optionally pass the names of the transients you want to get information about + * + * [--fields] + * : Fields to return + * --- + * default: all + * options: + * - key + * - hash_key + * - cache_type + * - async_updates + * - expiration + * - soft_expiration + * --- + * + * [--format] + * : Render the output in a particular format + * --- + * default: table + * options: + * - table + * - csv + * - ids + * - json + * - yaml + * --- + * + * [--=] + * : One or more fields to filter the list with + * + * @param $args + * @param $assoc_args + */ + public function list( $args, $assoc_args ) { + + global $dfm_transients; + + $transients = $dfm_transients; + + $this->supported_props = array( 'key', 'hash_key', 'cache_type', 'async_updates', 'expiration', 'soft_expiration' ); + + if ( empty( $dfm_transients ) ) { + parent::error( 'Looks like you don\'t have any transients registered' ); + } + + $transient_names = ( isset( $args ) ) ? $args : array(); + + if ( ! empty( $transient_names ) ) { + $transients = array_intersect_key( $transients, array_flip( $transient_names ) ); + } + + if ( ! empty( $assoc_args ) ) { + $filter_args = array_intersect_key( $assoc_args, array_flip( $this->supported_props ) ); + $transients = wp_list_filter( $transients, $filter_args ); + } + + if ( empty( $transients ) ) { + parent::error( 'No transients found with this criteria' ); + } + + $this->format_output( $transients, $assoc_args ); + + } + + /** + * Method to retrieve modifier ID's or the count of modifiers + * + * @param string $meta_key Name of the meta key to look for + * @param string $type Meta type so we know which table to search in + * @param bool $count Whether or not we should return the total count + * @param bool|int $limit Whether or not we should limit results, and if so what that limit is + * + * @return array|bool + */ + private function get_all_modifiers( $meta_key, $type, $count = false, $limit = false ) { + + global $wpdb; + + $object_type = substr( $type, 0, 4 ); + + $table = _get_meta_table( $object_type ); + + $select = ( true === $count ) ? 'count(*)' : $object_type . '_id'; + + $limit = ( false !== $limit && '-1' !== $limit ) ? 'LIMIT ' . absint( $limit ) : ''; + + if ( false === $table ) { + return false; + } + + $modifiers = $wpdb->get_col( $wpdb->prepare( " + SELECT $select + FROM $table + WHERE meta_key='%s' + $limit + ", + $meta_key + ) ); + + return $modifiers; + + } + + /** + * Handles the formatting of output + * + * @param array $transients The data to display + * @param array $assoc_args Args so we know how to display it + */ + private function format_output( $transients, $assoc_args ) { + + if ( ! empty( $assoc_args['fields'] ) ) { + if ( is_string( $assoc_args['fields'] ) ) { + $fields = explode( ',', $assoc_args ); + } else { + $fields = $assoc_args['fields']; + } + $fields = array_intersect( $fields, $this->supported_props ); + } else { + $fields = $this->supported_props; + } + + $formatter = new \WP_CLI\Formatter( $assoc_args, $fields ); + $formatter->display_items( $transients ); + + } + + } + + WP_CLI::add_command( 'dfm-transients', 'DFM_Transients_CLI' ); + +} diff --git a/includes/template-tags.php b/includes/template-tags.php index f80a067..0cd7800 100644 --- a/includes/template-tags.php +++ b/includes/template-tags.php @@ -80,7 +80,7 @@ function dfm_register_transient( $transient, $args = array() ) { * * @return mixed|WP_Error|array|string */ -function dfm_get_transient( $transient, $modifier ) { +function dfm_get_transient( $transient, $modifier = '' ) { $transients = new DFM_Transients( /** @@ -113,3 +113,83 @@ function dfm_get_transient( $transient, $modifier ) { return apply_filters( 'dfm_transients_get_result', $transients->get(), $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. + */ +function dfm_set_transient( $transient, $data, $modifier = '' ) { + + $transients = new DFM_Transients( + + /** + * 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 + * @return string $transient The name of the transient to set data to + */ + apply_filters( 'dfm_transients_set_transient_name', $transient, $modifier, $data ), + + /** + * Filters the unique modifier for the transient + * + * @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 + * @return string $modifier The unique modifier for the transient + */ + apply_filters( 'dfm_transients_set_transient_modifier', $modifier, $transient, $data ) + ); + + // Invoke the set method + $transients->set( $data ); + +} + +/** + * 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. + * @return void + * @access public + */ +function dfm_delete_transient( $transient, $modifier = '' ) { + + $transients = new DFM_Transients( + + /** + * Filters the name of the transient to delete + * + * @param string $transient Name of the transient + * @param string $modifier The unique modifier for the transient you want to delete + * @return string $transient Name of the transient to be deleted + */ + apply_filters( 'dfm_transients_delete_transient_name', $transient, $modifier ), + + /** + * Filters the modifier of the transient to delete + * + * @param string $modifier Unique modifier for the transient you want to delete + * @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 ) + ); + + // Invoke the delete method + $transients->delete(); + +}