[COT] Reintroduce the orders cache (#35014)

This commit is contained in:
Vedanshu Jain 2023-02-22 18:01:17 +05:30 committed by GitHub
commit ca49caabcb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 710 additions and 630 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add a cache for orders, to use when custom order tables are enabled

View File

@ -10,9 +10,11 @@
* @package WooCommerce\Classes
*/
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
use Automattic\WooCommerce\Utilities\NumberUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
@ -203,6 +205,11 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
$this->save_items();
if ( OrderUtil::orders_cache_usage_is_enabled() ) {
$order_cache = wc_get_container()->get( OrderCache::class );
$order_cache->update_if_cached( $this );
}
/**
* Trigger action after saving to the DB.
*

View File

@ -488,6 +488,8 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
$visibility_class[] = 'show_options_if_checked';
}
$must_disable = ArrayUtil::get_value_or_default( $value, 'disabled', false );
if ( ! isset( $value['checkboxgroup'] ) || 'start' === $value['checkboxgroup'] ) {
$has_tooltip = isset( $value['tooltip'] ) && '' !== $value['tooltip'];
$tooltip_container_class = $has_tooltip ? 'with-tooltip' : '';
@ -515,6 +517,7 @@ if ( ! class_exists( 'WC_Admin_Settings', false ) ) :
?>
<label for="<?php echo esc_attr( $value['id'] ); ?>">
<input
<?php echo $must_disable ? 'disabled' : ''; ?>
name="<?php echo esc_attr( $value['field_name'] ); ?>"
id="<?php echo esc_attr( $value['id'] ); ?>"
type="checkbox"

View File

@ -5,12 +5,15 @@
* @package WooCommerce\Classes
*/
use Automattic\WooCommerce\Caching\CacheNameSpaceTrait;
defined( 'ABSPATH' ) || exit;
/**
* WC_Cache_Helper.
*/
class WC_Cache_Helper {
use CacheNameSpaceTrait;
/**
* Transients to delete on shutdown.
@ -42,7 +45,7 @@ class WC_Cache_Helper {
*/
public static function additional_nocache_headers( $headers ) {
global $wp_query;
$agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$set_cache = false;
@ -114,44 +117,6 @@ class WC_Cache_Helper {
}
}
/**
* Get prefix for use with wp_cache_set. Allows all cache in a group to be invalidated at once.
*
* @param string $group Group of cache to get.
* @return string
*/
public static function get_cache_prefix( $group ) {
// Get cache key - uses cache key wc_orders_cache_prefix to invalidate when needed.
$prefix = wp_cache_get( 'wc_' . $group . '_cache_prefix', $group );
if ( false === $prefix ) {
$prefix = microtime();
wp_cache_set( 'wc_' . $group . '_cache_prefix', $prefix, $group );
}
return 'wc_cache_' . $prefix . '_';
}
/**
* Increment group cache prefix (invalidates cache).
*
* @param string $group Group of cache to clear.
*/
public static function incr_cache_prefix( $group ) {
wc_deprecated_function( 'WC_Cache_Helper::incr_cache_prefix', '3.9.0', 'WC_Cache_Helper::invalidate_cache_group' );
self::invalidate_cache_group( $group );
}
/**
* Invalidate cache group.
*
* @param string $group Group of cache to clear.
* @since 3.9.0
*/
public static function invalidate_cache_group( $group ) {
wp_cache_set( 'wc_' . $group . '_cache_prefix', microtime(), $group );
}
/**
* Get a hash of the customer location.
*

View File

@ -8,10 +8,11 @@
* @package WooCommerce\Classes
*/
defined( 'ABSPATH' ) || exit;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
/**
* Order factory class
*/
@ -30,13 +31,26 @@ class WC_Order_Factory {
return false;
}
$use_orders_cache = OrderUtil::orders_cache_usage_is_enabled();
if ( $use_orders_cache ) {
$order_cache = wc_get_container()->get( OrderCache::class );
$order = $order_cache->get( $order_id );
if ( ! is_null( $order ) ) {
return $order;
}
}
$classname = self::get_class_name_for_order_id( $order_id );
if ( ! $classname ) {
return false;
}
try {
return new $classname( $order_id );
$order = new $classname( $order_id );
if ( $use_orders_cache && $order instanceof \WC_Abstract_Legacy_Order ) {
$order_cache->set( $order, $order_id );
}
return $order;
} catch ( Exception $e ) {
wc_caught_exception( $e, __FUNCTION__, array( $order_id ) );
return false;
@ -56,6 +70,22 @@ class WC_Order_Factory {
$result = array();
$order_ids = array_filter( array_map( array( __CLASS__, 'get_order_id' ), $order_ids ) );
$already_cached_orders = array();
$use_orders_cache = OrderUtil::orders_cache_usage_is_enabled();
if ( $use_orders_cache ) {
$uncached_order_ids = array();
$order_cache = wc_get_container()->get( OrderCache::class );
foreach ( $order_ids as $order_id ) {
$cached_order = $order_cache->get( absint( $order_id ) );
if ( is_null( $cached_order ) ) {
$uncached_order_ids[] = $order_id;
} else {
$already_cached_orders[] = $cached_order;
}
}
$order_ids = $uncached_order_ids;
}
// We separate order list by class, since their datastore might be different.
$order_list_by_class = array();
$order_id_classnames = self::get_class_names_for_order_ids( $order_ids );
@ -99,9 +129,16 @@ class WC_Order_Factory {
}
// restore the sort order.
$result = array_replace( array_flip( $order_ids ), $result );
$result = array_values( array_replace( array_flip( $order_ids ), $result ) );
return array_values( $result );
if ( $use_orders_cache ) {
foreach ( $result as $order ) {
$order_cache->set( $order );
}
return array_merge( $already_cached_orders, $result );
} else {
return $result;
}
}
/**

View File

@ -0,0 +1,44 @@
<?php
namespace Automattic\WooCommerce\Caches;
use Automattic\WooCommerce\Caching\ObjectCache;
/**
* A class to cache order objects.
*/
class OrderCache extends ObjectCache {
/**
* Get the identifier for the type of the cached objects.
*
* @return string
*/
public function get_object_type(): string {
return 'orders';
}
/**
* Get the id of an object to be cached.
*
* @param array|object $object The object to be cached.
* @return int|string|null The id of the object, or null if it can't be determined.
*/
protected function get_object_id( $object ) {
return $object->get_id();
}
/**
* Validate an object before caching it.
*
* @param array|object $object The object to validate.
* @return string[]|null An array of error messages, or null if the object is valid.
*/
protected function validate( $object ): ?array {
if ( ! $object instanceof \WC_Abstract_Order ) {
return array( 'The supplied order is not an instance of WC_Abstract_Order, ' . gettype( $object ) );
}
return null;
}
}

View File

@ -0,0 +1,87 @@
<?php
namespace Automattic\WooCommerce\Caches;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\OrderUtil;
/**
* A class to control the usage of the orders cache.
*/
class OrderCacheController {
use AccessiblePrivateMethods;
/**
* The orders cache to use.
*
* @var OrderCache
*/
private $order_cache;
/**
* The orders cache to use.
*
* @var FeaturesController
*/
private $features_controller;
/**
* The backup value of the cache usage enable status, stored while the cache is temporarily disabled.
*
* @var null|bool
*/
private $orders_cache_usage_backup = null;
/**
* Class initialization, invoked by the DI container.
*
* @internal
* @param OrderCache $order_cache The order cache engine to use.
*/
final public function init( OrderCache $order_cache ) {
$this->order_cache = $order_cache;
}
/**
* Whether order cache usage is enabled. Currently, linked to custom orders' table usage.
*
* @return bool True if the order cache is enabled.
*/
public function orders_cache_usage_is_enabled(): bool {
return OrderUtil::custom_orders_table_usage_is_enabled();
}
/**
* Temporarily disable the order cache if it's enabled.
*
* This is a purely in-memory operation: a variable is created with the value
* of the current enable status for the feature, and this variable
* is checked by orders_cache_usage_is_enabled. In the next request the
* feature will be again enabled or not depending on how the feature is set.
*/
public function temporarily_disable_orders_cache_usage(): void {
if ( $this->orders_cache_usage_is_temporarly_disabled() ) {
return;
}
$this->orders_cache_usage_backup = $this->orders_cache_usage_is_enabled();
}
/**
* Check if the order cache has been temporarily disabled.
*
* @return bool True if the order cache is currently temporarily disabled.
*/
public function orders_cache_usage_is_temporarly_disabled(): bool {
return null !== $this->orders_cache_usage_backup;
}
/**
* Restore the order cache usage that had been temporarily disabled.
*/
public function maybe_restore_orders_cache_usage(): void {
$this->orders_cache_usage_backup = null;
}
}

View File

@ -12,9 +12,11 @@ interface CacheEngine {
* Retrieves an object cached under a given key.
*
* @param string $key They key under which the object to retrieve is cached.
* @param string $group The group under which the object is cached.
*
* @return array|object|null The cached object, or null if there's no object cached under the passed key.
*/
public function get_cached_object( string $key);
public function get_cached_object( string $key, string $group = '' );
/**
* Caches an object under a given key, and with a given expiration.
@ -22,23 +24,38 @@ interface CacheEngine {
* @param string $key The key under which the object will be cached.
* @param array|object $object The object to cache.
* @param int $expiration Expiration for the cached object, in seconds.
* @param string $group The group under which the object will be cached.
*
* @return bool True if the object is cached successfully, false otherwise.
*/
public function cache_object( string $key, $object, int $expiration): bool;
public function cache_object( string $key, $object, int $expiration, string $group = '' ): bool;
/**
* Removes a cached object from the cache.
*
* @param string $key They key under which the object is cached.
* @param string $group The group under which the object is cached.
*
* @return bool True if the object is removed from the cache successfully, false otherwise (because the object wasn't cached or for other reason).
*/
public function delete_cached_object( string $key): bool;
public function delete_cached_object( string $key, string $group = '' ): bool;
/**
* Checks if an object is cached under a given key.
*
* @param string $key The key to verify.
* @param string $group The group under which the object is cached.
*
* @return bool True if there's an object cached under the given key, false otherwise.
*/
public function is_cached( string $key): bool;
public function is_cached( string $key, string $group = '' ): bool;
/**
* Deletes all cached objects under a given group.
*
* @param string $group The group to delete.
*
* @return bool True if the group is deleted successfully, false otherwise.
*/
public function delete_cache_group( string $group = '' ): bool;
}

View File

@ -0,0 +1,66 @@
<?php
namespace Automattic\WooCommerce\Caching;
/**
* Implements namespacing algorithm to simulate grouping and namespacing for wp_cache, memcache and other caching engines that don't support grouping natively.
*
* See the algorithm details here: https://github.com/memcached/memcached/wiki/ProgrammingTricks#namespacing.
*
* To use the namespacing algorithm in the CacheEngine class:
* 1. Use a group string to identify all objects of a type.
* 2. Before setting cache, prefix the cache key by using the `get_cache_prefix`.
* 3. Use `invalidate_cache_group` function to invalidate all caches in entire group at once.
*/
trait CacheNameSpaceTrait {
/**
* Get prefix for use with wp_cache_set. Allows all cache in a group to be invalidated at once.
*
* @param string $group Group of cache to get.
* @return string Prefix.
*/
public static function get_cache_prefix( $group ) {
// Get cache key - uses cache key wc_orders_cache_prefix to invalidate when needed.
$prefix = wp_cache_get( 'wc_' . $group . '_cache_prefix', $group );
if ( false === $prefix ) {
$prefix = microtime();
wp_cache_set( 'wc_' . $group . '_cache_prefix', $prefix, $group );
}
return 'wc_cache_' . $prefix . '_';
}
/**
* Increment group cache prefix (invalidates cache).
*
* @param string $group Group of cache to clear.
*/
public static function incr_cache_prefix( $group ) {
wc_deprecated_function( 'WC_Cache_Helper::incr_cache_prefix', '3.9.0', 'WC_Cache_Helper::invalidate_cache_group' );
self::invalidate_cache_group( $group );
}
/**
* Invalidate cache group.
*
* @param string $group Group of cache to clear.
* @since 3.9.0
*/
public static function invalidate_cache_group( $group ) {
return wp_cache_set( 'wc_' . $group . '_cache_prefix', microtime(), $group );
}
/**
* Helper method to get prefixed key.
*
* @param string $key Key to prefix.
* @param string $group Group of cache to get.
*
* @return string Prefixed key.
*/
public static function get_prefixed_key( $key, $group ) {
return self::get_cache_prefix( $group ) . $key;
}
}

View File

@ -17,8 +17,6 @@ namespace Automattic\WooCommerce\Caching;
*/
abstract class ObjectCache {
private const CACHE_PREFIX_OPTION_NAME = 'wp_object_cache_key_prefix_';
/**
* Expiration value to be passed to 'set' to use the value of $default_expiration.
*/
@ -54,24 +52,10 @@ abstract class ObjectCache {
/**
* The cache engine to use.
*
* @var CacheEngine
* @var ?CacheEngine
*/
private $cache_engine = null;
/**
* The prefix to use for cache keys to pass to the cache engine.
*
* @var string
*/
private $cache_key_prefix = null;
/**
* The name of the option used to store the cache prefix.
*
* @var string
*/
private $cache_key_prefix_option_name;
/**
* Gets an identifier for the types of objects cached by this class.
* This identifier will be used to compose the keys passed to the cache engine,
@ -92,8 +76,6 @@ abstract class ObjectCache {
if ( empty( $this->object_type ) ) {
throw new CacheException( 'Class ' . get_class( $this ) . ' returns an empty value for get_object_type', $this );
}
$this->cache_key_prefix_option_name = self::CACHE_PREFIX_OPTION_NAME . $this->object_type;
}
/**
@ -117,7 +99,7 @@ abstract class ObjectCache {
/**
* Filters the underlying cache engine to be used by an instance of ObjectCache.
*
* @since 6.8.0
* @since 7.4.0
*
* @param CacheEngine $engine The cache engine to be used by default.
* @param ObjectCache $cache_instance The instance of ObjectCache that will use the cache engine.
@ -128,49 +110,16 @@ abstract class ObjectCache {
return $this->cache_engine;
}
/**
* Get the current cache prefix to use, generating one if none is in use yet.
*
* @return string
*/
private function get_cache_key_prefix(): string {
$value = $this->cache_key_prefix;
if ( ! $value ) {
$value = get_option( $this->cache_key_prefix_option_name );
if ( ! $value ) {
$value = $this->create_cache_key_prefix();
}
$this->cache_key_prefix = $value;
}
return $value;
}
/**
* Generate a prefix for the cache keys to use, containing the object type and a random string,
* and store it persistently using an option.
*
* @return string The generated prefix.
* @throws CacheException Can't store the generated prefix.
*/
private function create_cache_key_prefix(): string {
$prefix_variable_part = $this->get_random_string();
$prefix = "woocommerce_object_cache|{$this->object_type}|{$prefix_variable_part}|";
if ( ! update_option( $this->cache_key_prefix_option_name, $prefix ) ) {
throw new CacheException( "Can't store the key prefix option", $this );
}
return $prefix;
}
/**
* Add an object to the cache, or update an already cached object.
*
* @param int|string|null $id Id of the object to be cached, if null, get_object_id will be used to get it.
* @param object|array $object The object to be cached.
* @param int|string|null $id Id of the object to be cached, if null, get_object_id will be used to get it.
* @param int $expiration Expiration of the cached data in seconds from the current time, or DEFAULT_EXPIRATION to use the default value.
* @return bool True on success, false on error.
* @throws CacheException Invalid parameter, or null id was passed and get_object_id returns null too.
*/
public function set( $id = null, $object, int $expiration = self::DEFAULT_EXPIRATION ): bool {
public function set( $object, $id = null, int $expiration = self::DEFAULT_EXPIRATION ): bool {
if ( null === $object ) {
throw new CacheException( "Can't cache a null value", $this, $id );
}
@ -185,6 +134,62 @@ abstract class ObjectCache {
$this->verify_expiration_value( $expiration );
$errors = $this->validate( $object );
if ( ! is_null( $errors ) ) {
try {
$id = $this->get_id_from_object_if_null( $object, $id );
} catch ( \Throwable $ex ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
// Nothing else to do, we won't be able to add any significant object id to the CacheException and that's it.
}
if ( count( $errors ) === 1 ) {
throw new CacheException( 'Object validation/serialization failed: ' . $errors[0], $this, $id, $errors );
} elseif ( ! empty( $errors ) ) {
throw new CacheException( 'Object validation/serialization failed', $this, $id, $errors );
}
}
$id = $this->get_id_from_object_if_null( $object, $id );
$this->last_cached_data = $object;
return $this->get_cache_engine()->cache_object(
$id,
$object,
self::DEFAULT_EXPIRATION === $expiration ? $this->default_expiration : $expiration,
$this->get_object_type()
);
}
/**
* Update an object in the cache, but only if an object is already cached with the same id.
*
* @param object|array $object The new object that will replace the already cached one.
* @param int|string|null $id Id of the object to be cached, if null, get_object_id will be used to get it.
* @param int $expiration Expiration of the cached data in seconds from the current time, or DEFAULT_EXPIRATION to use the default value.
* @return bool True on success, false on error or if no object wiith the supplied id was cached.
* @throws CacheException Invalid parameter, or null id was passed and get_object_id returns null too.
*/
public function update_if_cached( $object, $id = null, int $expiration = self::DEFAULT_EXPIRATION ): bool {
$id = $this->get_id_from_object_if_null( $object, $id );
if ( ! $this->is_cached( $id ) ) {
return false;
}
return $this->set( $object, $id, $expiration );
}
/**
* Get the id from an object if the id itself is null.
*
* @param object|array $object The object to get the id from.
* @param int|string|null $id An object id or null.
*
* @return int|string|null Passed $id if it wasn't null, otherwise id obtained from $object using get_object_id.
*
* @throws CacheException Passed $id is null and get_object_id returned null too.
*/
private function get_id_from_object_if_null( $object, $id ) {
if ( null === $id ) {
$id = $this->get_object_id( $object );
if ( null === $id ) {
@ -192,28 +197,7 @@ abstract class ObjectCache {
}
}
$errors = $this->validate( $object );
if ( null !== $errors && 1 === count( $errors ) ) {
throw new CacheException( 'Object validation/serialization failed: ' . $errors[0], $this, $id, $errors );
} elseif ( ! empty( $errors ) ) {
throw new CacheException( 'Object validation/serialization failed', $this, $id, $errors );
}
$data = $this->serialize( $object );
/**
* Filters the serialized object that will be passed by an instance of ObjectCache to its cache engine to be cached.
*
* @since 6.8.0
*
* @param array $data The already serialized object data.
* @param array|object $object The object before serialization.
* @returns array The actual serialized object data that will be passed to the cache engine.
*/
$data = apply_filters( "woocommerce_after_serializing_{$this->object_type}_for_caching", $data, $object, $id );
$this->last_cached_data = $data;
return $this->get_cache_engine()->cache_object( $this->get_cache_key_prefix() . $id, $data, self::DEFAULT_EXPIRATION === $expiration ? $this->default_expiration : $expiration );
return $id;
}
/**
@ -249,34 +233,21 @@ abstract class ObjectCache {
$this->verify_expiration_value( $expiration );
$data = $this->get_cache_engine()->get_cached_object( $this->get_cache_key_prefix() . $id );
$data = $this->get_cache_engine()->get_cached_object( $id, $this->get_object_type() );
if ( null === $data ) {
$object = null;
if ( $get_from_datastore_callback ) {
$object = $get_from_datastore_callback( $id );
} else {
$object = $this->get_from_datastore( $id );
}
if ( null === $object ) {
return null;
}
$this->set( $id, $object, $expiration );
$this->set( $object, $id, $expiration );
$data = $this->last_cached_data;
}
$object = $this->deserialize( $data );
/**
* Filters the deserialized object that is retrieved from the cache engine of an instance of ObjectCache and will be returned by 'get'.
*
* @since 6.8.0
*
* @param array|object $object The object after being deserialized.
* @param array $data The serialized object data that was retrieved from the cache engine.
* @returns array|object The actual deserialized object data that will be returned by 'get'.
*/
return apply_filters( "woocommerce_after_deserializing_{$this->object_type}_from_cache", $object, $data, $id );
return $data;
}
/**
@ -286,41 +257,16 @@ abstract class ObjectCache {
* @return bool True if the object is removed from the cache successfully, false otherwise (because the object wasn't cached or for other reason).
*/
public function remove( $id ): bool {
$result = $this->get_cache_engine()->delete_cached_object( $this->get_cache_key_prefix() . $id );
/**
* Action triggered by an instance of ObjectCache after an object is (attempted to be) removed from the cache.
*
* @since 6.8.0
*
* @param int|string $id The id of the object being removed.
* @param bool $result True if the object removal succeeded, false otherwise.
*/
do_action( "woocommerce_after_removing_{$this->object_type}_from_cache", $id, $result );
return $result;
return $this->get_cache_engine()->delete_cached_object( $id, $this->get_object_type() );
}
/**
* Remove all the objects from the cache.
* This is done by forcing the generation of a new cache key prefix
* and leaving the old cached objects to expire.
*
* @return void
* @return bool True on success, false on error.
*/
public function flush(): void {
delete_option( $this->cache_key_prefix_option_name );
$this->cache_key_prefix = null;
/**
* Action triggered by an instance of ObjectCache after it flushes all the cached objects.
*
* @since 6.8.0
*
* @param ObjectCache $cache_instance The instance of ObjectCache whose 'flush` method has been called.
* @param CacheEngine $engine The cache engine in use.
*/
do_action( "woocommerce_after_flushing_{$this->object_type}_cache", $this, $this->get_cache_engine() );
public function flush(): bool {
return $this->get_cache_engine()->delete_cache_group( $this->get_object_type() );
}
/**
@ -330,7 +276,7 @@ abstract class ObjectCache {
* @return bool True if there's a cached object with the specified id.
*/
public function is_cached( $id ): bool {
return $this->get_cache_engine()->is_cached( $this->get_cache_key_prefix() . $id );
return $this->get_cache_engine()->is_cached( $id, $this->get_object_type() );
}
/**
@ -340,9 +286,7 @@ abstract class ObjectCache {
* @param array|object $object The object to get the id for.
* @return int|string|null
*/
protected function get_object_id( $object ) {
return null;
}
abstract protected function get_object_id( $object );
/**
* Validate an object before it's cached.
@ -350,42 +294,7 @@ abstract class ObjectCache {
* @param array|object $object Object to validate.
* @return array|null An array with validation error messages, null or an empty array if there are no errors.
*/
protected function validate( $object ): ?array {
return null;
}
/**
* Convert an object to a serialized form suitable for caching.
* If a class overrides this method it should override 'deserialize' as well.
*
* @param array|object $object The object to serialize.
* @return array The serialized object.
*/
protected function serialize( $object ): array {
return array( 'data' => $object );
}
/**
* Deserializes a set of object data after having been retrieved from the cache.
* If a class overrides this method it should override 'serialize' as well.
*
* @param array $serialized Serialized object data as it was returned by 'validate_and_serialize'.
* @return object|array Deserialized object, ready to be returned by 'get'.
*/
protected function deserialize( array $serialized ) {
return $serialized['data'];
}
/**
* Get an object from an authoritative data store.
* This is used by 'get' if the object isn't cached and no custom object retrieval callback is suupplied.
*
* @param int|string $id The id of the object to get.
* @return array|object|null The retrieved object, or null if it's not possible to retrieve an object by the given id.
*/
protected function get_from_datastore( $id ) {
return null;
}
abstract protected function validate( $object ): ?array;
/**
* Get the instance of the cache engine to use.
@ -393,7 +302,7 @@ abstract class ObjectCache {
* @return CacheEngine
*/
protected function get_cache_engine_instance(): CacheEngine {
return wc_get_container()->get( WpCacheEngine::class );
return wc_get_container()->get( WPCacheEngine::class );
}
/**

View File

@ -0,0 +1,76 @@
<?php
namespace Automattic\WooCommerce\Caching;
/**
* Implementation of CacheEngine that uses the built-in WordPress cache.
*/
class WPCacheEngine implements CacheEngine {
use CacheNameSpaceTrait;
/**
* Retrieves an object cached under a given key.
*
* @param string $key They key under which the object to retrieve is cached.
* @param string $group The group under which the object is cached.
*
* @return array|object|null The cached object, or null if there's no object cached under the passed key.
*/
public function get_cached_object( string $key, string $group = '' ) {
$prefixed_key = self::get_prefixed_key( $key, $group );
$value = wp_cache_get( $prefixed_key, $group );
return false === $value ? null : $value;
}
/**
* Caches an object under a given key, and with a given expiration.
*
* @param string $key The key under which the object will be cached.
* @param array|object $object The object to cache.
* @param int $expiration Expiration for the cached object, in seconds.
* @param string $group The group under which the object will be cached.
*
* @return bool True if the object is cached successfully, false otherwise.
*/
public function cache_object( string $key, $object, int $expiration, string $group = '' ): bool {
$prefixed_key = self::get_prefixed_key( $key, $group );
return wp_cache_set( $prefixed_key, $object, $group, $expiration );
}
/**
* Removes a cached object from the cache.
*
* @param string $key They key under which the object is cached.
* @param string $group The group under which the object is cached.
*
* @return bool True if the object is removed from the cache successfully, false otherwise (because the object wasn't cached or for other reason).
*/
public function delete_cached_object( string $key, string $group = '' ): bool {
$prefixed_key = self::get_prefixed_key( $key, $group );
return wp_cache_delete( $prefixed_key, $group );
}
/**
* Checks if an object is cached under a given key.
*
* @param string $key The key to verify.
* @param string $group The group under which the object is cached.
*
* @return bool True if there's an object cached under the given key, false otherwise.
*/
public function is_cached( string $key, string $group = '' ): bool {
$prefixed_key = self::get_prefixed_key( $key, $group );
return false !== wp_cache_get( $prefixed_key, $group );
}
/**
* Deletes all cached objects under a given group.
*
* @param string $group The group to delete.
*
* @return bool True if the group is deleted successfully, false otherwise.
*/
public function delete_cache_group( string $group = '' ): bool {
return self::invalidate_cache_group( $group );
}
}

View File

@ -1,32 +0,0 @@
<?php
namespace Automattic\WooCommerce\Caching;
/**
* Implementation of CacheEngine that uses the built-in WordPress cache.
*/
class WpCacheEngine implements CacheEngine {
public const CACHE_GROUP_NAME = 'wc-object-cache';
// phpcs:disable Squiz.Commenting.FunctionComment.Missing
public function get_cached_object( string $key ) {
$value = wp_cache_get( $key, self::CACHE_GROUP_NAME );
return false === $value ? null : $value;
}
public function cache_object( string $key, $object, int $expiration ): bool {
return wp_cache_set( $key, $object, self::CACHE_GROUP_NAME, $expiration );
}
public function delete_cached_object( string $key ): bool {
return wp_cache_delete( $key, self::CACHE_GROUP_NAME );
}
public function is_cached( string $key ): bool {
return false !== wp_cache_get( $key, self::CACHE_GROUP_NAME );
}
// phpcs:enable Squiz.Commenting.FunctionComment.Missing
}

View File

@ -5,9 +5,12 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
@ -81,6 +84,20 @@ class CustomOrdersTableController {
*/
private $features_controller;
/**
* The orders cache object to use.
*
* @var OrderCache
*/
private $order_cache;
/**
* The orders cache controller object to use.
*
* @var OrderCacheController
*/
private $order_cache_controller;
/**
* Class constructor.
*/
@ -114,18 +131,24 @@ class CustomOrdersTableController {
* @param OrdersTableRefundDataStore $refund_data_store The refund data store to use.
* @param BatchProcessingController $batch_processing_controller The batch processing controller to use.
* @param FeaturesController $features_controller The features controller instance to use.
* @param OrderCache $order_cache The order cache engine to use.
* @param OrderCacheController $order_cache_controller The order cache controller to use.
*/
final public function init(
OrdersTableDataStore $data_store,
DataSynchronizer $data_synchronizer,
OrdersTableRefundDataStore $refund_data_store,
BatchProcessingController $batch_processing_controller,
FeaturesController $features_controller ) {
FeaturesController $features_controller,
OrderCache $order_cache,
OrderCacheController $order_cache_controller ) {
$this->data_store = $data_store;
$this->data_synchronizer = $data_synchronizer;
$this->batch_processing_controller = $batch_processing_controller;
$this->refund_data_store = $refund_data_store;
$this->features_controller = $features_controller;
$this->order_cache = $order_cache;
$this->order_cache_controller = $order_cache_controller;
}
/**
@ -462,12 +485,19 @@ class CustomOrdersTableController {
* @throws \Exception Attempt to change the authoritative orders table while orders sync is pending.
*/
private function process_pre_update_option( $value, $option, $old_value ) {
if ( DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_OPTION === $option && $value !== $old_value ) {
$this->order_cache->flush();
return $value;
}
if ( self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION !== $option || $value === $old_value || false === $old_value ) {
return $value;
}
$this->order_cache->flush();
/**
* Commenting out for better testability.
* Re-enable the following code once the COT to posts table sync is implemented (it's currently commented out to ease testing).
$sync_is_pending = 0 !== $this->data_synchronizer->get_current_orders_pending_sync_count();
if ( $sync_is_pending ) {
throw new \Exception( "The authoritative table for orders storage can't be changed while there are orders out of sync" );
@ -575,4 +605,6 @@ class CustomOrdersTableController {
)
);
}
}

View File

@ -5,6 +5,8 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessorInterface;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
@ -63,6 +65,13 @@ class DataSynchronizer implements BatchProcessorInterface {
*/
private $error_logger;
/**
* The order cache controller.
*
* @var OrderCacheController
*/
private $order_cache_controller;
/**
* Class constructor.
*/
@ -80,13 +89,21 @@ class DataSynchronizer implements BatchProcessorInterface {
* @param DatabaseUtil $database_util The database util class to use.
* @param PostsToOrdersMigrationController $posts_to_cot_migrator The posts to COT migration class to use.
* @param LegacyProxy $legacy_proxy The legacy proxy instance to use.
* @param OrderCacheController $order_cache_controller The order cache controller instance to use.
* @internal
*/
final public function init( OrdersTableDataStore $data_store, DatabaseUtil $database_util, PostsToOrdersMigrationController $posts_to_cot_migrator, LegacyProxy $legacy_proxy ) {
$this->data_store = $data_store;
$this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
final public function init(
OrdersTableDataStore $data_store,
DatabaseUtil $database_util,
PostsToOrdersMigrationController $posts_to_cot_migrator,
LegacyProxy $legacy_proxy,
OrderCacheController $order_cache_controller
) {
$this->data_store = $data_store;
$this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
$this->error_logger = $legacy_proxy->call_function( 'wc_get_logger' );
$this->order_cache_controller = $order_cache_controller;
}
/**
@ -329,6 +346,8 @@ WHERE
* @param array $batch Batch details.
*/
public function process_batch( array $batch ) : void {
$this->order_cache_controller->temporarily_disable_orders_cache_usage();
if ( $this->custom_orders_table_is_authoritative() ) {
foreach ( $batch as $id ) {
$order = wc_get_order( $id );
@ -344,6 +363,7 @@ WHERE
}
if ( 0 === $this->get_total_pending_count() ) {
$this->cleanup_synchronization_state();
$this->order_cache_controller->maybe_restore_orders_cache_usage();
}
}

View File

@ -6,6 +6,7 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
@ -1974,6 +1975,7 @@ FROM $order_meta_table
*/
public function delete_order_data_from_custom_order_tables( $order_id ) {
global $wpdb;
$order_cache = wc_get_container()->get( OrderCache::class );
// Delete COT-specific data.
foreach ( $this->get_all_table_names() as $table ) {
@ -1984,6 +1986,7 @@ FROM $order_meta_table
: array( 'order_id' => $order_id ),
array( '%d' )
);
$order_cache->remove( $order_id );
}
}

View File

@ -7,7 +7,6 @@ namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
/**
@ -25,7 +24,6 @@ class COTMigrationServiceProvider extends AbstractServiceProvider {
protected $provides = array(
PostsToOrdersMigrationController::class,
CLIRunner::class,
DataSynchronizer::class,
);
/**
@ -37,5 +35,6 @@ class COTMigrationServiceProvider extends AbstractServiceProvider {
*/
public function register() {
$this->share( PostsToOrdersMigrationController::class );
$this->share( CLIRunner::class );
}
}

View File

@ -2,7 +2,7 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Caching\WpCacheEngine;
use Automattic\WooCommerce\Caching\WPCacheEngine;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
/**
@ -16,13 +16,13 @@ class ObjectCacheServiceProvider extends AbstractServiceProvider {
* @var array
*/
protected $provides = array(
WpCacheEngine::class,
WPCacheEngine::class,
);
/**
* Register the classes.
*/
public function register() {
$this->share( WpCacheEngine::class );
$this->share( WPCacheEngine::class );
}
}

View File

@ -6,6 +6,9 @@
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Caching\TransientsEngine;
use Automattic\WooCommerce\DataBase\Migrations\CustomOrderTable\CLIRunner;
use Automattic\WooCommerce\Database\Migrations\CustomOrderTable\PostsToOrdersMigrationController;
use Automattic\WooCommerce\Internal\BatchProcessing\BatchProcessingController;
@ -36,6 +39,8 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
CLIRunner::class,
OrdersTableDataStoreMeta::class,
OrdersTableRefundDataStore::class,
OrderCache::class,
OrderCacheController::class,
);
/**
@ -45,7 +50,15 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
$this->share( OrdersTableDataStoreMeta::class );
$this->share( OrdersTableDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class, LegacyProxy::class ) );
$this->share( DataSynchronizer::class )->addArguments( array( OrdersTableDataStore::class, DatabaseUtil::class, PostsToOrdersMigrationController::class, LegacyProxy::class ) );
$this->share( DataSynchronizer::class )->addArguments(
array(
OrdersTableDataStore::class,
DatabaseUtil::class,
PostsToOrdersMigrationController::class,
LegacyProxy::class,
OrderCacheController::class,
)
);
$this->share( OrdersTableRefundDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class, LegacyProxy::class ) );
$this->share( CustomOrdersTableController::class )->addArguments(
array(
@ -54,9 +67,12 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
OrdersTableRefundDataStore::class,
BatchProcessingController::class,
FeaturesController::class,
OrderCache::class,
OrderCacheController::class,
)
);
$this->share( OrderCache::class );
$this->share( OrderCacheController::class )->addArgument( OrderCache::class );
if ( Constants::is_defined( 'WP_CLI' ) && WP_CLI ) {
$this->share( CLIRunner::class )->addArguments( array( CustomOrdersTableController::class, DataSynchronizer::class, PostsToOrdersMigrationController::class ) );
}

View File

@ -5,8 +5,10 @@
namespace Automattic\WooCommerce\Utilities;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Internal\Admin\Orders\PageController;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Utilities\COTMigrationUtil;
use WC_Order;
use WP_Post;
@ -35,6 +37,15 @@ final class OrderUtil {
return wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled();
}
/**
* Helper function to get whether the orders cache should be used or not.
*
* @return bool True if the orders cache should be used, false otherwise.
*/
public static function orders_cache_usage_is_enabled() : bool {
return wc_get_container()->get( OrderCacheController::class )->orders_cache_usage_is_enabled();
}
/**
* Checks if posts and order custom table sync is enabled and there are no pending orders.
*

View File

@ -230,8 +230,9 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
$order = new \WC_Order();
$order->set_status( 'completed' );
$order->save();
$order_id = $order->get_id();
$request = new \WP_REST_Request( 'DELETE', '/wc/v3/orders/' . $order->get_id() );
$request = new \WP_REST_Request( 'DELETE', '/wc/v3/orders/' . $order_id );
$response = $this->server->dispatch( $request );
$this->assertEquals( 200, $response->get_status() );
@ -239,13 +240,13 @@ class WC_REST_Orders_Controller_Tests extends WC_REST_Unit_Test_Case {
// Check that the response includes order data from the order (before deletion).
$data = $response->get_data();
$this->assertArrayHasKey( 'id', $data );
$this->assertEquals( $data['id'], $order->get_id() );
$this->assertEquals( $data['id'], $order_id );
$this->assertEquals( 'completed', $data['status'] );
wp_cache_flush();
// Check the order was actually deleted.
$order = wc_get_order( $order->get_id() );
$order = wc_get_order( $order_id );
$this->assertEquals( 'trash', $order->get_status( 'edit' ) );
}

View File

@ -26,6 +26,15 @@ class CacheExceptionTest extends \WC_Unit_Test_Case {
public function get_object_type(): string {
return 'the_type';
}
protected function get_object_id( $object ) {
}
protected function validate( $object ): ?array {
}
protected function get_from_datastore( $id ) {
}
};
// phpcs:enable Squiz.Commenting
}

View File

@ -1,63 +0,0 @@
<?php
namespace Automattic\WooCommerce\Tests\Caching;
use Automattic\WooCommerce\Caching\CacheEngine;
use Automattic\WooCommerce\Utilities\ArrayUtil;
/**
* An implementation of CacheEngine that simply stores cached objects in an array.
*/
class InMemoryObjectCacheEngine implements CacheEngine {
/**
* The cached objects.
*
* @var array
*/
public $cache = array();
/**
* Whether calls to 'cache_object' will succeed or not.
*
* @var bool
*/
public $caching_succeeds = true;
/**
* Value of the expiration time that was passed to 'cache_object' the last time it was called.
*
* @var int
*/
public $last_expiration;
// phpcs:disable Squiz.Commenting
public function get_cached_object( string $key ) {
return ArrayUtil::get_value_or_default( $this->cache, $key, null );
}
public function cache_object( string $key, $object, int $expiration ): bool {
if ( ! $this->caching_succeeds ) {
return false;
}
$this->cache[ $key ] = $object;
$this->last_expiration = $expiration;
return true;
}
public function delete_cached_object( string $key ): bool {
if ( array_key_exists( $key, $this->cache ) ) {
unset( $this->cache[ $key ] );
return true;
}
return false;
}
public function is_cached( $key ): bool {
return array_key_exists( $key, $this->cache );
}
// phpcs:enable Squiz.Commenting
}

View File

@ -11,10 +11,18 @@ class InvalidObjectCacheClass extends ObjectCache {
// phpcs:disable Squiz.Commenting
public function get_object_type(): string {
return '';
}
protected function get_object_id( $object ) {
}
protected function validate( $object ): ?array {
}
protected function get_from_datastore( $id ) {
}
// phpcs:enable Squiz.Commenting
}

View File

@ -5,7 +5,7 @@ namespace Automattic\WooCommerce\Tests\Caching;
use Automattic\WooCommerce\Caching\CacheException;
use Automattic\WooCommerce\Caching\ObjectCache;
use Automattic\WooCommerce\Caching\CacheEngine;
use Automattic\WooCommerce\Caching\WpCacheEngine;
use Automattic\WooCommerce\Caching\WPCacheEngine;
/**
* Tests for the ObjectCache class.
@ -19,23 +19,10 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
*/
private $sut;
/**
* The cache engine to use.
*
* @var CacheEngine
*/
private $cache_engine;
/**
* Runs before each test.
*/
public function setUp(): void {
$cache_engine = new InMemoryObjectCacheEngine();
$this->cache_engine = $cache_engine;
$container = wc_get_container();
$container->replace( WpCacheEngine::class, $cache_engine );
$this->reset_container_resolutions();
// phpcs:disable Squiz.Commenting
@ -50,9 +37,22 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
$this->random_string_index++;
return 'random_' . $this->random_string_index;
}
};
protected function get_object_id( $object ) {
return null;
}
protected function validate( $object ): ?array {
return null;
}
protected function get_from_datastore( $id ) {
return null;
}
};
// phpcs:enable Squiz.Commenting
$this->sut->flush();
}
/**
@ -94,7 +94,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
$this->expectException( CacheException::class );
$this->expectExceptionMessage( "Can't cache a null value" );
$this->sut->set( 'the_id', null );
$this->sut->set( null );
}
/**
@ -104,7 +104,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
$this->expectException( CacheException::class );
$this->expectExceptionMessage( "Can't cache a non-object, non-array value" );
$this->sut->set( 'the_id', 1234 );
$this->sut->set( 1234 );
}
/**
@ -114,7 +114,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
$this->expectException( CacheException::class );
$this->expectExceptionMessage( "Object id must be an int, a string, or null for 'set'" );
$this->sut->set( array( 1, 2 ), array( 'foo' ) );
$this->sut->set( array( 'foo' ), array( 1, 2 ) );
}
/**
@ -130,7 +130,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
$this->expectException( CacheException::class );
$this->expectExceptionMessage( 'Invalid expiration value, must be ObjectCache::DEFAULT_EXPIRATION or a value between 1 and ObjectCache::MAX_EXPIRATION' );
$this->sut->set( 'the_id', array( 'foo' ), $expiration );
$this->sut->set( array( 'foo' ), 'the_id', $expiration );
}
/**
@ -139,7 +139,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
public function try_set_when_cache_engine_fails() {
$this->cache_engine->caching_succeeds = false;
$result = $this->sut->set( 'the_id', array( 'foo' ) );
$result = $this->sut->set( array( 'foo' ), 'the_id' );
$this->assertFalse( $result );
}
@ -148,16 +148,14 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
*/
public function test_set_new_object_with_id_caches_with_expected_key() {
$object = array( 'foo' );
$result = $this->sut->set( 'the_id', $object );
$result = $this->sut->set( $object, 'the_id' );
$this->assertTrue( $result );
$expected_prefix = 'woocommerce_object_cache|the_type|random_1|';
$this->assertEquals( $expected_prefix, get_option( 'wp_object_cache_key_prefix_the_type' ) );
$expected_prefix = \WC_Cache_Helper::get_cache_prefix( 'the_type' );
$key = $expected_prefix . 'the_id';
$expected_cached = array( 'data' => $object );
$this->assertEquals( $expected_cached, $this->cache_engine->cache[ $key ] );
$this->assertEquals( $object, wp_cache_get( $key, 'the_type' ) );
}
/**
@ -165,34 +163,16 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
*/
public function test_setting_two_objects_result_in_same_prefix() {
$object_1 = array( 'foo' );
$this->sut->set( 'the_id_1', $object_1 );
$this->sut->set( $object_1, 'the_id_1' );
$object_2 = array( 1, 2, 3, 4 );
$this->sut->set( 9999, $object_2 );
$this->sut->set( $object_2, 9999 );
$prefix = 'woocommerce_object_cache|the_type|random_1|';
$prefix = \WC_Cache_Helper::get_cache_prefix( 'the_type' );
$key_1 = $prefix . 'the_id_1';
$expected_cached = array( 'data' => $object_1 );
$this->assertEquals( $expected_cached, $this->cache_engine->cache[ $key_1 ] );
$key_2 = $prefix . '9999';
$expected_cached = array( 'data' => $object_2 );
$this->assertEquals( $expected_cached, $this->cache_engine->cache[ $key_2 ] );
}
/**
* @testdox 'set' uses the default expiration value if no explicit value is passed.
*/
public function test_set_with_default_expiration() {
$this->sut->set( 'the_id', array( 'foo' ) );
$this->assertEquals( $this->sut->get_default_expiration_value(), $this->cache_engine->last_expiration );
}
/**
* @testdox 'set' uses the explicitly passed expiration value.
*/
public function test_set_with_explicit_expiration() {
$this->sut->set( 'the_id', array( 'foo' ), 1234 );
$this->assertEquals( 1234, $this->cache_engine->last_expiration );
$key_1 = $prefix . 'the_id_1';
$this->assertEquals( $object_1, wp_cache_get( $key_1, 'the_type' ) );
$key_2 = $prefix . '9999';
$this->assertEquals( $object_2, wp_cache_get( $key_2, 'the_type' ) );
}
/**
@ -202,7 +182,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
$this->expectException( CacheException::class );
$this->expectExceptionMessage( "Null id supplied and the cache class doesn't implement get_object_id" );
$this->sut->set( null, array( 'foo' ) );
$this->sut->set( array( 'foo' ) );
}
/**
@ -223,21 +203,28 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
protected function get_random_string(): string {
return 'random';
}
protected function validate( $object ): ?array {
return null;
}
protected function get_from_datastore( $id ) {
return null;
}
};
// phpcs:enable Squiz.Commenting
$sut->set( null, array( 'id' => 1234 ) );
$sut->set( array( 'id' => 1234 ) );
$this->assertEquals( 'woocommerce_object_cache|the_type|random|1235', array_keys( $this->cache_engine->cache )[0] );
$this->assertEquals( array( 'id' => 1234 ), $sut->get( '1235' ) );
}
/**
* @testdox 'set' caches the value returned by 'serialize'.
* @testdox 'update_if_cached' does nothing if no object is cached with the passed (or obtained) id.
*/
public function test_set_with_custom_serialization() {
$object = array( 'foo' );
public function test_update_if_cached_does_nothing_for_not_cached_id() {
$id = 1234;
// phpcs:disable Squiz.Commenting
$sut = new class() extends ObjectCache {
@ -245,18 +232,77 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
return 'the_type';
}
protected function serialize( $object ): array {
return array( 'the_data' => $object );
protected function get_object_id( $object ) {
return $object['id'];
}
protected function validate( $object ): ?array {
return null;
}
protected function get_from_datastore( $id ) {
return null;
}
};
// phpcs:enable Squiz.Commenting
$sut->set( 1234, $object );
$result = $sut->update_if_cached( array( 'id' => 1234 ), $id );
$this->assertFalse( $result );
$this->assertEmpty( $sut->get( $id ) );
}
$cached = array_values( $this->cache_engine->cache )[0];
$expected = array( 'the_data' => $object );
$this->assertEquals( $expected, $cached );
/**
* @testdox 'update_if_cached' updates an already cached object the same way as 'set'.
*/
public function test_update_if_cached_updates_already_cached_object() {
$id = 1234;
// phpcs:disable Squiz.Commenting
$sut = new class() extends ObjectCache {
public function get_object_type(): string {
return 'the_type';
}
protected function get_object_id( $object ) {
return $object['id'];
}
protected function get_random_string(): string {
return 'random';
}
protected function validate( $object ): ?array {
return null;
}
protected function get_from_datastore( $id ) {
return null;
}
};
// phpcs:enable Squiz.Commenting
$sut->set( array( 'id' => 1234 ), $id );
$this->assertEquals( array( 'id' => 1234 ), $sut->get( $id ) );
$new_value = array(
'id' => 1234,
'foo' => 'bar',
);
$result = $sut->update_if_cached( $new_value, $id );
$this->assertTrue( $result );
$this->assertEquals( $new_value, $sut->get( $id ) );
}
/**
* @testdox 'update_if_cached' throws an exception if no object id is passed and the class doesn't implement 'get_object_id'.
*/
public function test_update_if_cached_null_id_without_id_retrieval_implementation() {
$this->expectException( CacheException::class );
$this->expectExceptionMessage( "Null id supplied and the cache class doesn't implement get_object_id" );
$this->sut->update_if_cached( array( 'foo' ) );
}
/**
@ -288,12 +334,20 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
protected function validate( $object ): array {
return $this->errors;
}
protected function get_from_datastore( $id ) {
return null;
}
protected function get_object_id( $object ) {
return $object['id'];
}
};
// phpcs:enable Squiz.Commenting
try {
$sut->set( 1234, $object );
$sut->set( $object, 1234 );
} catch ( CacheException $thrown ) {
$exception = $thrown;
}
@ -343,7 +397,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
*/
public function test_try_getting_previously_cached_object() {
$object = array( 'foo' );
$this->sut->set( 'the_id', $object );
$this->sut->set( $object, 'the_id' );
$result = $this->sut->get( 'the_id' );
$this->assertEquals( $object, $result );
@ -369,115 +423,15 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
$expected = array( 'id' => 'the_id' );
$this->assertEquals( $expected, $result );
$this->assertEquals( array( 'data' => $expected ), array_values( $this->cache_engine->cache )[0] );
$this->assertEquals( $this->sut->get_default_expiration_value(), $this->cache_engine->last_expiration );
}
/**
* @testdox 'get' uses the passed object retrieval callback if there's no object cached under the passed id, and caches the object retrieved using the passed expiration value.
*/
public function test_try_getting_not_cached_object_with_callback_and_explicit_expiration() {
$expiration = 1234;
$callback = function( $id ) {
return array( 'id' => $id );
};
$result = $this->sut->get( 'the_id', $expiration, $callback );
$expected = array( 'id' => 'the_id' );
$this->assertEquals( $expected, $result );
$this->assertEquals( array( 'data' => $expected ), array_values( $this->cache_engine->cache )[0] );
$this->assertEquals( $expiration, $this->cache_engine->last_expiration );
}
/**
* @testdox 'get' uses the 'get_from_datastore' method if there's no object cached under the passed id, and caches the object retrieved.
*/
public function test_try_getting_not_cached_object_get_from_datastore_implemented() {
// phpcs:disable Squiz.Commenting
$sut = new class() extends ObjectCache {
public function get_object_type(): string {
return 'the_type';
}
protected function get_from_datastore( $id ) {
return array( 'id' => $id );
}
};
// phpcs:enable Squiz.Commenting
$result = $sut->get( 'the_id' );
$expected = array( 'id' => 'the_id' );
$this->assertEquals( $expected, $result );
$this->assertEquals( array( 'data' => $expected ), array_values( $this->cache_engine->cache )[0] );
$this->assertEquals( $this->sut->get_default_expiration_value(), $this->cache_engine->last_expiration );
}
/**
* @testdox 'get' uses the 'get_from_datastore' method if there's no object cached under the passed id, and caches the object retrieved using the passed expiration value.
*/
public function test_try_getting_not_cached_object_get_from_datastore_implemented_and_explicit_expiration() {
$expiration = 1234;
// phpcs:disable Squiz.Commenting
$sut = new class() extends ObjectCache {
public function get_object_type(): string {
return 'the_type';
}
protected function get_from_datastore( $id ) {
return array( 'id' => $id );
}
};
// phpcs:enable Squiz.Commenting
$result = $sut->get( 'the_id', $expiration );
$expected = array( 'id' => 'the_id' );
$this->assertEquals( $expected, $result );
$this->assertEquals( array( 'data' => $expected ), array_values( $this->cache_engine->cache )[0] );
$this->assertEquals( $expiration, $this->cache_engine->last_expiration );
}
/**
* @testdox 'get' applies 'deserialize' to the object returned by the cache engine before returning it.
*/
public function test_custom_deserialization() {
// phpcs:disable Squiz.Commenting
$sut = new class() extends ObjectCache {
public function get_object_type(): string {
return 'the_type';
}
protected function deserialize( array $serialized ) {
$object = $serialized['data'];
$object[] = 3;
return $object;
}
};
// phpcs:enable Squiz.Commenting
$sut->set( 'the_id', array( 1, 2 ) );
$result = $sut->get( 'the_id' );
$expected = array( 1, 2, 3 );
$this->assertEquals( $expected, $result );
$this->assertEquals( $expected, $this->sut->get( 'the_id' ) );
}
/**
* @testdox 'remove' removes a cached object and returns true, or returns false if there's no cached object under the passed id.
*/
public function test_remove() {
$this->sut->set( 'the_id_1', array( 'foo' ) );
$this->sut->set( 'the_id_2', array( 'bar' ) );
$this->sut->set( array( 'foo' ), 'the_id_1' );
$this->sut->set( array( 'bar' ), 'the_id_2' );
$result_1 = $this->sut->remove( 'the_id_1' );
$result_2 = $this->sut->remove( 'the_id_X' );
@ -493,15 +447,17 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
* @testdox 'flush' deletes the stored cache key prefix, effectively rendering the cached objects inaccessible.
*/
public function test_flush() {
$this->sut->set( 'the_id', array( 'foo' ) );
$this->sut->set( array( 'foo' ), 'the_id' );
$current_prefix_key = \WC_Cache_Helper::get_cache_prefix( 'the_type' );
$this->sut->flush();
$this->assertFalse( get_option( 'wp_object_cache_key_prefix_the_type' ) );
$this->assertFalse( $this->sut->is_cached( 'the_id' ) );
$expected_new_prefix = \WC_Cache_Helper::get_cache_prefix( 'the_type' );
$this->assertNotEquals( $current_prefix_key, $expected_new_prefix );
$this->sut->set( 'the_id_2', array( 'bar' ) );
$this->sut->set( array( 'bar' ), 'the_id_2' );
$expected_new_prefix = 'woocommerce_object_cache|the_type|random_2|';
$this->assertEquals( $expected_new_prefix, get_option( 'wp_object_cache_key_prefix_the_type' ) );
$this->assertEquals( $expected_new_prefix, \WC_Cache_Helper::get_cache_prefix( 'the_type' ) );
$this->assertFalse( $this->sut->is_cached( 'the_id' ) );
$this->assertTrue( $this->sut->is_cached( 'the_id_2' ) );
}
@ -510,10 +466,9 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
* @testdox A custom cache engine instance can be used by overriding 'get_cache_engine_instance'.
*/
public function test_custom_cache_engine_via_protected_method() {
$engine = new InMemoryObjectCacheEngine();
$engine = new WPCacheEngine();
// phpcs:disable Squiz.Commenting
$sut = new class($engine) extends ObjectCache {
public function get_object_type(): string {
return 'the_type';
@ -529,25 +484,51 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
protected function get_cache_engine_instance(): CacheEngine {
return $this->engine;
}
};
protected function get_object_id( $object ) {
}
protected function validate( $object ): ?array {
return null;
}
protected function get_from_datastore( $id ) {
}
};
// phpcs:enable Squiz.Commenting
$object = array( 'foo' );
$sut->set( 'the_id', $object );
$sut->set( $object, 'the_id' );
$expected_cached = array( 'data' => $object );
$this->assertEquals( $expected_cached, array_values( $engine->cache )[0] );
$this->assertEquals( $object, $sut->get( 'the_id' ) );
}
/**
* @testdox A custom cache engine instance can be used via 'wc_object_cache_get_engine' filter.
*/
public function test_custom_cache_engine_via_hook() {
$engine = new InMemoryObjectCacheEngine();
$engine = new class() extends WPCacheEngine {};
$engine_passed_to_filter = null;
$cache_passed_to_filter = null;
$sut = new class() extends ObjectCache {
// phpcs:disable Squiz.Commenting
public function get_object_type(): string {
return 'the_type';
}
protected function get_object_id( $object ) {
}
protected function validate( $object ): ?array {
return null;
}
protected function get_from_datastore( $id ) {
}
// phpcs:enable Squiz.Commenting
};
add_filter(
'wc_object_cache_get_engine',
function( $old_engine, $cache ) use ( $engine, &$engine_passed_to_filter, &$cache_passed_to_filter ) {
@ -560,131 +541,11 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
);
$object = array( 'foo' );
$this->sut->set( 'the_id', $object );
$sut->set( $object, 'the_id' );
$expected_cached = array( 'data' => $object );
$this->assertEquals( $expected_cached, array_values( $engine->cache )[0] );
$this->assertEquals( $object, $sut->get( 'the_id' ) );
$this->assertEquals( $engine_passed_to_filter, wc_get_container()->get( WpCacheEngine::class ) );
$this->assertEquals( $cache_passed_to_filter, $this->sut );
}
/**
* @testdox 'woocommerce_after_serializing_{type}_for_caching' allows to modify the serialized object before being cached.
*/
public function test_modifying_serialized_object_via_filter() {
$object_passed_to_filter = null;
$id_passed_to_filter = null;
add_filter(
'woocommerce_after_serializing_the_type_for_caching',
function( $data, $object, $id ) use ( &$object_passed_to_filter, &$id_passed_to_filter ) {
$object_passed_to_filter = $object;
$id_passed_to_filter = $id;
$data['foo'] = 'bar';
return $data;
},
10,
3
);
$object = array( 'fizz' );
$this->sut->set( 'the_id', $object );
$expected_cached = array(
'data' => $object,
'foo' => 'bar',
);
$this->assertEquals( $expected_cached, array_values( $this->cache_engine->cache )[0] );
$this->assertEquals( $object, $object_passed_to_filter );
$this->assertEquals( 'the_id', $id_passed_to_filter );
}
/**
* @testdox 'woocommerce_after_deserializing_{type}_from_cache' allows to modify the deserialized object before it's returned by 'get'.
*/
public function test_modifying_deserialized_object_via_filter() {
$object_passed_to_filter = null;
$id_passed_to_filter = null;
$data_passed_to_filter = null;
$original_object = array( 'foo' );
$replacement_object = array( 'bar' );
add_filter(
'woocommerce_after_deserializing_the_type_from_cache',
function( $object, $data, $id ) use ( &$object_passed_to_filter, &$id_passed_to_filter, &$data_passed_to_filter, $replacement_object ) {
$object_passed_to_filter = $object;
$id_passed_to_filter = $id;
$data_passed_to_filter = $data;
return $replacement_object;
},
10,
3
);
$this->sut->set( 'the_id', $original_object );
$retrieved_object = $this->sut->get( 'the_id' );
$this->assertEquals( $replacement_object, $retrieved_object );
$this->assertEquals( $original_object, $object_passed_to_filter );
$this->assertEquals( array( 'data' => $original_object ), $data_passed_to_filter );
$this->assertEquals( 'the_id', $id_passed_to_filter );
}
/**
* @testdox 'remove' triggers the 'woocommerce_after_removing_{type}_from_cache' action.
*
* @testWith [true]
* [false]
*
* @param bool $operation_succeeds Whether the removal operation succeeds or not.
*/
public function test_action_triggered_on_object_removed_from_cache( bool $operation_succeeds ) {
$id_passed_to_action = null;
$result_passed_to_action = null;
add_action(
'woocommerce_after_removing_the_type_from_cache',
function( $id, $result ) use ( &$id_passed_to_action, &$result_passed_to_action ) {
$id_passed_to_action = $id;
$result_passed_to_action = $result;
},
10,
2
);
$this->sut->set( 'the_id', array( 'foo' ) );
$this->sut->remove( $operation_succeeds ? 'the_id' : 'INVALID_ID' );
$this->assertEquals( $operation_succeeds ? 'the_id' : 'INVALID_ID', $id_passed_to_action );
$this->assertEquals( $operation_succeeds, $result_passed_to_action );
}
/**
* @testdox 'flush' triggers the 'woocommerce_after_flushing_{type}_cache' action.
*/
public function test_action_triggered_on_cache_flushed() {
$cache_passed_to_action = null;
$engine_passed_to_action = null;
add_action(
'woocommerce_after_flushing_the_type_cache',
function( $cache, $engine ) use ( &$cache_passed_to_action, &$engine_passed_to_action ) {
$cache_passed_to_action = $cache;
$engine_passed_to_action = $engine;
},
10,
2
);
$this->sut->flush();
$this->assertEquals( $this->sut, $cache_passed_to_action );
$this->assertEquals( $this->cache_engine, $engine_passed_to_action );
$this->assertEquals( $engine_passed_to_filter, wc_get_container()->get( WPCacheEngine::class ) );
$this->assertEquals( $cache_passed_to_filter, $sut );
}
}