[COT] Add the orders cache (#34396)

* Reverse the order of "$id" and "$object" in ObjectCache::set

* Add the ObjectCache::update_if_cached method

* Modify ObjectCache:set to validate object before invoking get_id

* Add a temporary TransientsEngine class.

This is temporary! Must be removed before merging to trunk.

* Add the OrderCache class.

This class uses a TransientsEngine instance as the caching engine.
This is temporary and must be undone (get_cache_engine_instance method
must be removed) before merging to trunk!

* Use the new OrdersCache class

- When an order is retrieved, cache it
- When an order is saved, update it if it was cached already
- When an order is trashed or deleted, remove it from cache
- When the authoritative table for orders changes, flush the cache

* Remove the hardcoded usage of TransientEngine in OrderCache

It will make things easier later before merging. The transients engine
can still be used via the wc_object_cache_get_engine hook.

* Add changelog file

* Fix failing unit test

The test was failing because the order is cached by reference when
being saved in the test, and then when being deleted by the REST API
code it gets its id set to 0.

* Add a setting to enable/disable the orders cache

Also added a mechanism to temporarily disable the orders cache while
syncrhonization is in progress.

* Adjustments in the mechanism to temporarily disable the orders cache usage

* OrderCacheController: backup enable option is now stored in memory.

* Convert conditions to Yoda :-(

* Add missing $

* Use the new features engine to declare the cache as an experimental feature

Also decouple the orders cache mechanism from the COT feature,
it can be now used indepently of the COT feature and independently
of whether the new orders table is in use or not.

* Removed unused import and transient class.

Co-authored-by: Vedanshu Jain <vedanshu.jain.2012@gmail.com>
This commit is contained in:
Néstor Soriano 2022-10-07 09:16:24 +02:00 committed by GitHub
parent c67a0fbe27
commit 3f155c9a63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 503 additions and 57 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'] ) {
?>
<tr valign="top" class="<?php echo esc_attr( implode( ' ', $visibility_class ) ); ?>">
@ -510,6 +512,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

@ -8,6 +8,9 @@
* @package WooCommerce\Classes
*/
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Utilities\OrderUtil;
defined( 'ABSPATH' ) || exit;
/**
@ -28,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;
@ -54,8 +70,25 @@ 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();
foreach ( $order_ids as $order_id ) {
$classname = self::get_class_name_for_order_id( $order_id );
if ( ! $classname && ! $skip_invalid ) {
@ -97,9 +130,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 'order';
}
/**
* 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_Legacy_Order ) {
return array( 'The supplied order is not an instance of WC_Order, ' . gettype( $object ) );
}
return null;
}
}

View File

@ -0,0 +1,121 @@
<?php
namespace Automattic\WooCommerce\Caches;
use Automattic\WooCommerce\Internal\Features\FeaturesController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
/**
* A class to control the usage of the orders cache.
*/
class OrderCacheController {
use AccessiblePrivateMethods;
const FEATURE_NAME = 'orders_cache';
/**
* 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;
/**
* Creates a new instance of the class.
*/
public function __construct() {
self::add_action( FeaturesController::FEATURE_ENABLED_CHANGED_ACTION, array( $this, 'handle_feature_enable_changed' ), 10, 2 );
}
/**
* Class initialization, invoked by the DI container.
*
* @internal
* @param OrderCache $order_cache The order cache engine to use.
* @param FeaturesController $features_controller The features controller to use.
*/
final public function init( OrderCache $order_cache, FeaturesController $features_controller ) {
$this->order_cache = $order_cache;
$this->features_controller = $features_controller;
}
/**
* Handler for the feature enable changed action, when the orders cache is enabled or disabled it flushes it.
*
* @param string $feature_id The id of the feature whose enable status changed.
* @param bool $enabled Whether the feature has been enabled or disabled.
* @return void
*/
private function handle_feature_enable_changed( string $feature_id, bool $enabled ): void {
if ( self::FEATURE_NAME === $feature_id ) {
$this->order_cache->flush();
}
}
/**
* Set the value of the order cache usage setting.
*
* @param bool $enable True if the order cache should be used, false if not.
* @throws \Exception Attempt to enable the orders cache usage while it's temporarily disabled.
*/
public function set_orders_cache_usage( bool $enable ): void {
$this->features_controller->change_feature_enable( $enable );
$this->orders_cache_usage_backup = null;
}
/**
* Get the value of the order cache usage setting.
*
* @return bool True if order cache usage setting is currently enabled, false if not.
*/
public function orders_cache_usage_is_enabled(): bool {
return ! $this->orders_cache_usage_is_temporarly_disabled() && $this->features_controller->feature_is_enabled( self::FEATURE_NAME );
}
/**
* 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

@ -164,13 +164,13 @@ abstract class ObjectCache {
/**
* 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,20 +185,22 @@ abstract class ObjectCache {
$this->verify_expiration_value( $expiration );
if ( null === $id ) {
$id = $this->get_object_id( $object );
if ( null === $id ) {
throw new CacheException( "Null id supplied and the cache class doesn't implement get_object_id", $this );
$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 );
}
}
$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 );
}
$id = $this->get_id_from_object_if_null( $object, $id );
$data = $this->serialize( $object );
/**
@ -213,7 +215,49 @@ abstract class ObjectCache {
$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 $this->get_cache_engine()->cache_object(
$this->get_cache_key_prefix() . $id,
$data,
self::DEFAULT_EXPIRATION === $expiration ? $this->default_expiration : $expiration
);
}
/**
* 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 ) {
throw new CacheException( "Null id supplied and the cache class doesn't implement get_object_id", $this );
}
}
return $id;
}
/**
@ -261,7 +305,7 @@ abstract class ObjectCache {
return null;
}
$this->set( $id, $object, $expiration );
$this->set( $object, $id, $expiration );
$data = $this->last_cached_data;
}

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.
*/
@ -102,6 +119,8 @@ class CustomOrdersTableController {
self::add_filter( DataSynchronizer::PENDING_SYNCHRONIZATION_FINISHED_ACTION, array( $this, 'process_sync_finished' ), 10, 0 );
self::add_action( 'woocommerce_update_options_advanced_custom_data_stores', array( $this, 'process_options_updated' ), 10, 0 );
self::add_action( 'woocommerce_after_register_post_type', array( $this, 'register_post_type_for_order_placeholders' ), 10, 0 );
self::add_action( 'woocommerce_delete_order', array( $this, 'after_order_deleted_or_trashed' ), 10, 1);
self::add_action( 'woocommerce_trash_order', array( $this, 'after_order_deleted_or_trashed' ), 10, 1);
}
/**
@ -113,18 +132,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;
}
/**
@ -472,12 +497,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 ( $option !== self::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION || $value === $old_value || $old_value === false ) {
if ( $option === DataSynchronizer::ORDERS_DATA_SYNC_ENABLED_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.
* TODO: 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" );
@ -567,4 +599,15 @@ class CustomOrdersTableController {
)
);
}
/**
* Handle the order deleted/trashed hooks.
*
* @param int $order_id The id of the order deleted or trashed.
*/
private function after_order_deleted_or_trashed( int $order_id ): void {
if ( $this->features_controller->feature_is_enabled( OrderCacheController::FEATURE_NAME ) ) {
$this->order_cache->remove( $order_id );
}
}
}

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\Utilities\DatabaseUtil;
@ -51,6 +53,20 @@ class DataSynchronizer implements BatchProcessorInterface {
*/
private $posts_to_cot_migrator;
/**
* The orders cache to use.
*
* @var OrderCache
*/
private $cache;
/**
* The orders cache controller to use.
*
* @var OrderCacheController
*/
private $cache_controller;
/**
* Class constructor.
*/
@ -85,12 +101,21 @@ class DataSynchronizer implements BatchProcessorInterface {
* @param OrdersTableDataStore $data_store The data store to use.
* @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 OrderCache $cache The orders cache to use.
* @param OrderCacheController $cache_controller The orders cache controller to use.
*@internal
*/
final public function init( OrdersTableDataStore $data_store, DatabaseUtil $database_util, PostsToOrdersMigrationController $posts_to_cot_migrator ) {
final public function init(
OrdersTableDataStore $data_store,
DatabaseUtil $database_util,
PostsToOrdersMigrationController $posts_to_cot_migrator,
OrderCache $cache,
OrderCacheController $cache_controller ) {
$this->data_store = $data_store;
$this->database_util = $database_util;
$this->posts_to_cot_migrator = $posts_to_cot_migrator;
$this->cache = $cache;
$this->cache_controller = $cache_controller;
}
/**
@ -115,6 +140,7 @@ class DataSynchronizer implements BatchProcessorInterface {
* Delete the custom orders database tables.
*/
public function delete_database_tables() {
$this->cache->flush();
$table_names = $this->data_store->get_all_table_names();
foreach ( $table_names as $table_name ) {
@ -297,6 +323,8 @@ WHERE
* @param array $batch Batch details.
*/
public function process_batch( array $batch ) : void {
$this->cache_controller->temporarily_disable_orders_cache_usage();
if ( $this->custom_orders_table_is_authoritative() ) {
foreach ( $batch as $id ) {
$order = wc_get_order( $id );
@ -308,6 +336,7 @@ WHERE
}
if ( 0 === $this->get_total_pending_count() ) {
$this->cleanup_synchronization_state();
$this->cache_controller->maybe_restore_orders_cache_usage();
}
}

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

@ -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;
@ -35,6 +38,8 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
CLIRunner::class,
OrdersTableDataStoreMeta::class,
OrdersTableRefundDataStore::class,
OrderCache::class,
OrderCacheController::class,
);
/**
@ -44,8 +49,18 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
$this->share( OrdersTableDataStoreMeta::class );
$this->share( OrdersTableDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class ) );
$this->share( DataSynchronizer::class )->addArguments( array( OrdersTableDataStore::class, DatabaseUtil::class, PostsToOrdersMigrationController::class ) );
$this->share( DataSynchronizer::class )->addArguments(
array(
OrdersTableDataStore::class,
DatabaseUtil::class,
PostsToOrdersMigrationController::class,
OrderCache::class,
OrderCacheController::class,
)
);
$this->share( OrdersTableRefundDataStore::class )->addArguments( array( OrdersTableDataStoreMeta::class, DatabaseUtil::class ) );
$this->share( OrderCache::class )->addArgument( TransientsEngine::class );
$this->share( OrderCacheController::class )->addArguments( array( OrderCache::class, FeaturesController::class ) );
$this->share( CustomOrdersTableController::class )->addArguments(
array(
OrdersTableDataStore::class,
@ -53,6 +68,8 @@ class OrdersDataStoreServiceProvider extends AbstractServiceProvider {
OrdersTableRefundDataStore::class,
BatchProcessingController::class,
FeaturesController::class,
OrderCache::class,
OrderCacheController::class,
)
);
if ( Constants::is_defined( 'WP_CLI' ) && WP_CLI ) {

View File

@ -7,6 +7,7 @@ namespace Automattic\WooCommerce\Internal\Features;
use Automattic\WooCommerce\Internal\Admin\Analytics;
use Automattic\WooCommerce\Admin\Features\Navigation\Init;
use Automattic\WooCommerce\Caches\OrderCacheController;
use Automattic\WooCommerce\Internal\Traits\AccessiblePrivateMethods;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\ArrayUtil;
@ -63,16 +64,21 @@ class FeaturesController {
'is_experimental' => false,
'enabled_by_default' => true,
),
'new_navigation' => array(
'new_navigation' => array(
'name' => __( 'Navigation', 'woocommerce' ),
'description' => __( 'Adds the new WooCommerce navigation experience to the dashboard', 'woocommerce' ),
'is_experimental' => false,
),
'custom_order_tables' => array(
'custom_order_tables' => array(
'name' => __( 'Custom order tables', 'woocommerce' ),
'description' => __( 'Enable the custom orders tables feature (still in development)', 'woocommerce' ),
'is_experimental' => true,
),
OrderCacheController::FEATURE_NAME => array(
'name' => __( 'Orders cache', 'woocommerce' ),
'description' => __( 'Enable the usage of a cache for shop orders', 'woocommerce' ),
'is_experimental' => true,
),
);
$this->init_features( $features );

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

@ -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,7 +148,7 @@ 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 );
@ -165,9 +165,9 @@ 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|';
@ -183,7 +183,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
* @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->sut->set( array( 'foo' ), 'the_id' );
$this->assertEquals( $this->sut->get_default_expiration_value(), $this->cache_engine->last_expiration );
}
@ -191,7 +191,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
* @testdox 'set' uses the explicitly passed expiration value.
*/
public function test_set_with_explicit_expiration() {
$this->sut->set( 'the_id', array( 'foo' ), 1234 );
$this->sut->set( array( 'foo' ), 'the_id', 1234 );
$this->assertEquals( 1234, $this->cache_engine->last_expiration );
}
@ -202,7 +202,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' ) );
}
/**
@ -227,11 +227,88 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
// 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] );
}
/**
* @testdox 'update_if_cached' does nothing if no object is cached with the passed (or obtained) id.
*
* @testWith [1234]
* [null]
*
* @param ?int $id Id to pass to update_if_cached.
*/
public function test_update_if_cached_does_nothing_for_not_cached_id( ?int $id ) {
// 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'];
}
};
// phpcs:enable Squiz.Commenting
$result = $sut->update_if_cached( array( 'id' => 1234 ), $id );
$this->assertFalse( $result );
$this->assertEmpty( $this->cache_engine->cache );
}
/**
* @testdox 'update_if_cached' updates an already cached object the same way as 'set'.
*
* @testWith [1234]
* [null]
*
* @param ?int $id Id to pass to update_if_cached.
*/
public function test_update_if_cached_updates_already_cached_object( ?int $id ) {
// 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';
}
};
// phpcs:enable Squiz.Commenting
$sut->set( array( 'id' => 1234 ), $id );
$this->assertEquals( 'woocommerce_object_cache|the_type|random|1234', array_keys( $this->cache_engine->cache )[0] );
$new_value = array(
'id' => 1234,
'foo' => 'bar',
);
$result = $sut->update_if_cached( $new_value, $id );
$this->assertTrue( $result );
$this->assertEquals( $this->cache_engine->cache['woocommerce_object_cache|the_type|random|1234'], array( 'data' => $new_value ) );
}
/**
* @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' ) );
}
/**
* @testdox 'set' caches the value returned by 'serialize'.
*/
@ -252,7 +329,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
// phpcs:enable Squiz.Commenting
$sut->set( 1234, $object );
$sut->set( $object, 1234 );
$cached = array_values( $this->cache_engine->cache )[0];
$expected = array( 'the_data' => $object );
@ -269,7 +346,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
*/
public function test_set_with_custom_serialization_that_returns_errors( int $errors_count ) {
$exception = null;
$errors = 1 === $errors_count ? array( 'Foo failed' ) : array( 'Foo failed', 'Bar failed' );
$errors = $errors_count === 1 ? array( 'Foo failed' ) : array( 'Foo failed', 'Bar failed' );
$object = array( 'foo' );
// phpcs:disable Squiz.Commenting
@ -293,13 +370,13 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
// phpcs:enable Squiz.Commenting
try {
$sut->set( 1234, $object );
$sut->set( $object, 1234 );
} catch ( CacheException $thrown ) {
$exception = $thrown;
}
$expected_message = 'Object validation/serialization failed';
if ( 1 === $errors_count ) {
if ( $errors_count === 1 ) {
$expected_message .= ': Foo failed';
}
$this->assertEquals( $expected_message, $exception->getMessage() );
@ -343,7 +420,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 );
@ -465,7 +542,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
// phpcs:enable Squiz.Commenting
$sut->set( 'the_id', array( 1, 2 ) );
$sut->set( array( 1, 2 ), 'the_id' );
$result = $sut->get( 'the_id' );
$expected = array( 1, 2, 3 );
@ -476,8 +553,8 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
* @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,12 +570,12 @@ 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' );
$this->sut->flush();
$this->assertFalse( get_option( 'wp_object_cache_key_prefix_the_type' ) );
$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' ) );
@ -534,7 +611,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
// 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] );
@ -560,7 +637,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
);
$object = array( 'foo' );
$this->sut->set( 'the_id', $object );
$this->sut->set( $object, 'the_id' );
$expected_cached = array( 'data' => $object );
$this->assertEquals( $expected_cached, array_values( $engine->cache )[0] );
@ -590,7 +667,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
);
$object = array( 'fizz' );
$this->sut->set( 'the_id', $object );
$this->sut->set( $object, 'the_id' );
$expected_cached = array(
'data' => $object,
@ -626,7 +703,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
3
);
$this->sut->set( 'the_id', $original_object );
$this->sut->set( $original_object, 'the_id' );
$retrieved_object = $this->sut->get( 'the_id' );
$this->assertEquals( $replacement_object, $retrieved_object );
@ -658,7 +735,7 @@ class ObjectCacheTest extends \WC_Unit_Test_Case {
2
);
$this->sut->set( 'the_id', array( 'foo' ) );
$this->sut->set( array( 'foo' ), 'the_id' );
$this->sut->remove( $operation_succeeds ? 'the_id' : 'INVALID_ID' );
$this->assertEquals( $operation_succeeds ? 'the_id' : 'INVALID_ID', $id_passed_to_action );