diff --git a/includes/class-wc-cache-helper.php b/includes/class-wc-cache-helper.php index 329a0cccc5d..1a88b5c2f11 100644 --- a/includes/class-wc-cache-helper.php +++ b/includes/class-wc-cache-helper.php @@ -23,6 +23,37 @@ class WC_Cache_Helper { add_action( 'admin_notices', array( __CLASS__, 'notices' ) ); } + /** + * Get transient version + * + * When using transients with unpredictable names, e.g. those containing an md5 + * hash in the name, we need a way to invalidate them all at once. + * + * When using default WP transients we're able to do this with a DB query to + * delete transients manually. + * + * With external cache however, this isn't possible. Instead, this function is used + * to append a unique string (based on time()) to each transient. When transients + * are invalidated, the transient version will increment and data will be regenerated. + * + * Raised in issue https://github.com/woothemes/woocommerce/issues/5777 + * Adapted from ideas in http://tollmanz.com/invalidation-schemes/ + * + * @param string $group Name for the group of transients we need to invalidate + * @param boolean $refresh true to force a new version + * @return string transient version based on time(), 10 digits + */ + public static function get_transient_version( $group, $refresh = false ) { + $transient_name = $group . '-transient-version'; + $transient_value = get_transient( $key ); + + if ( false === $transient_value || true === $refresh ) { + $transient_value = time(); + set_transient( $transient_name, $transient_value ); + } + return $transient_value; + } + /** * Prevent caching on dynamic pages. * diff --git a/includes/class-wc-post-data.php b/includes/class-wc-post-data.php index 7b5dc99443a..8593fb94105 100644 --- a/includes/class-wc-post-data.php +++ b/includes/class-wc-post-data.php @@ -59,15 +59,21 @@ class WC_Post_Data { * Delete product view transients when needed e.g. when post status changes, or visibility/stock status is modified. */ public static function delete_product_query_transients() { - global $wpdb; + // Increments the transient version to invalidate cache + WC_Cache_Helper::get_transient_version( 'product_query', true ); - if ( wp_using_ext_object_cache() ) { - wp_cache_flush(); // There isn't a reliable method of looking up the names, so flush the cache. - return; + // If not using an external caching system, we can clear the transients out manually and avoid filling our DB + if ( ! wp_using_ext_object_cache() ) { + global $wpdb; + + $wpdb->query( " + DELETE FROM `$wpdb->options` + WHERE `option_name` LIKE ('\_transient\_wc\_uf\_pid\_%') + OR `option_name` LIKE ('\_transient\_timeout\_wc\_uf\_pid\_%') + OR `option_name` LIKE ('\_transient\_wc\_products\_will\_display\_%') + OR `option_name` LIKE ('\_transient\_timeout\_wc\_products\_will\_display\_%') + " ); } - - $wpdb->query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('\_transient\_wc\_uf\_pid\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_uf\_pid\_%')" ); - $wpdb->query( "DELETE FROM `$wpdb->options` WHERE `option_name` LIKE ('\_transient\_wc\_products\_will\_display\_%') OR `option_name` LIKE ('\_transient\_timeout\_wc\_products\_will\_display\_%')" ); } /** diff --git a/includes/class-wc-query.php b/includes/class-wc-query.php index 04e042a4a0b..e7546161f5d 100644 --- a/includes/class-wc-query.php +++ b/includes/class-wc-query.php @@ -435,7 +435,7 @@ class WC_Query { unset( $current_wp_query['paged'] ); // Generate a transient name based on current query - $transient_name = 'wc_uf_pid_' . md5( http_build_query( $current_wp_query ) ); + $transient_name = 'wc_uf_pid_' . md5( http_build_query( $current_wp_query ) . WC_Cache_Helper::get_transient_version( 'product_query' ) ); $transient_name = ( is_search() ) ? $transient_name . '_s' : $transient_name; if ( false === ( $unfiltered_product_ids = get_transient( $transient_name ) ) ) { @@ -464,16 +464,18 @@ class WC_Query { $this->unfiltered_product_ids = $unfiltered_product_ids; // Also store filtered posts ids... - if ( sizeof( $this->post__in ) > 0 ) + if ( sizeof( $this->post__in ) > 0 ) { $this->filtered_product_ids = array_intersect( $this->unfiltered_product_ids, $this->post__in ); - else + } else { $this->filtered_product_ids = $this->unfiltered_product_ids; + } // And filtered post ids which just take layered nav into consideration (to find max price in the price widget) - if ( sizeof( $this->layered_nav_post__in ) > 0 ) + if ( sizeof( $this->layered_nav_post__in ) > 0 ) { $this->layered_nav_product_ids = array_intersect( $this->unfiltered_product_ids, $this->layered_nav_post__in ); - else + } else { $this->layered_nav_product_ids = $this->unfiltered_product_ids; + } } diff --git a/includes/wc-template-functions.php b/includes/wc-template-functions.php index 2ae25705f8d..70cd66ff356 100644 --- a/includes/wc-template-functions.php +++ b/includes/wc-template-functions.php @@ -1340,7 +1340,9 @@ if ( ! function_exists( 'woocommerce_products_will_display' ) ) { return true; } - if ( false === ( $products_will_display = get_transient( 'wc_products_will_display_' . $parent_id ) ) ) { + $transient_name = 'wc_products_will_display_' . $parent_id . WC_Cache_Helper::get_transient_version( 'product_query' ); + + if ( false === ( $products_will_display = get_transient( $transient_name ) ) ) { $has_children = $wpdb->get_col( $wpdb->prepare( "SELECT term_id FROM {$wpdb->term_taxonomy} WHERE parent = %d AND taxonomy = %s", $parent_id, $taxonomy ) ); if ( $has_children ) { @@ -1363,7 +1365,7 @@ if ( ! function_exists( 'woocommerce_products_will_display' ) ) { } } - set_transient( 'wc_products_will_display_' . $parent_id, $products_will_display, YEAR_IN_SECONDS ); + set_transient( $transient_name, $products_will_display, YEAR_IN_SECONDS ); return $products_will_display; }