Commit 9aa21ef2333 for woocommerce
commit 9aa21ef233354671c31143fdd4716b2916be3c70
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Tue Mar 24 13:55:39 2026 +0100
[Performance] Enhance woocommerce_refresh_order_count_cache action (#63753)
The action should consider warm cache and object cache availability to avoid unnecessary, resource-intensive SQL queries.
diff --git a/plugins/woocommerce/changelog/performance-order-count-action-enhancements b/plugins/woocommerce/changelog/performance-order-count-action-enhancements
new file mode 100644
index 00000000000..4fe9cd3fc4d
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-order-count-action-enhancements
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Improved cache warmup for the `woocommerce_refresh_order_count_cache` action to increase efficiency.
diff --git a/plugins/woocommerce/includes/wc-order-functions.php b/plugins/woocommerce/includes/wc-order-functions.php
index 635805097ae..5ad099b157a 100644
--- a/plugins/woocommerce/includes/wc-order-functions.php
+++ b/plugins/woocommerce/includes/wc-order-functions.php
@@ -405,7 +405,7 @@ function wc_orders_count( $status, string $type = '' ) {
foreach ( $types_for_count as $type ) {
$cache = $order_count_cache->get( $type, array( $status ) );
- if ( false !== $cache && isset( $cache[ $status ] ) ) {
+ if ( isset( $cache[ $status ] ) ) {
$count += $cache[ $status ];
} else {
$count_for_type = OrderUtil::get_count_for_type( $type );
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index 5831eb14c78..9f56a27909d 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -56883,12 +56883,6 @@ parameters:
count: 1
path: src/Caches/OrderCountCache.php
- -
- message: '#^Method Automattic\\WooCommerce\\Caches\\OrderCountCache\:\:get\(\) should return array\<int\> but returns null\.$#'
- identifier: return.type
- count: 2
- path: src/Caches/OrderCountCache.php
-
-
message: '#^Method Automattic\\WooCommerce\\Caches\\OrderCountCache\:\:increment\(\) should return int but returns int\|false\.$#'
identifier: return.type
diff --git a/plugins/woocommerce/src/Caches/OrderCountCache.php b/plugins/woocommerce/src/Caches/OrderCountCache.php
index f7e7d2cf652..23209e205a6 100644
--- a/plugins/woocommerce/src/Caches/OrderCountCache.php
+++ b/plugins/woocommerce/src/Caches/OrderCountCache.php
@@ -161,9 +161,9 @@ class OrderCountCache {
/**
* Get the cache value for a given order type and set of statuses.
*
- * @param string $order_type The type of order.
+ * @param string $order_type The type of order.
* @param string[] $order_statuses The statuses of the order.
- * @return int[] The cache value.
+ * @return null|array<string, int> The cache value.
*/
public function get( $order_type, $order_statuses = array() ) {
$order_type = (string) $order_type;
diff --git a/plugins/woocommerce/src/Caches/OrderCountCacheService.php b/plugins/woocommerce/src/Caches/OrderCountCacheService.php
index 179892a4a9b..08786b6e8ad 100644
--- a/plugins/woocommerce/src/Caches/OrderCountCacheService.php
+++ b/plugins/woocommerce/src/Caches/OrderCountCacheService.php
@@ -49,7 +49,7 @@ class OrderCountCacheService {
add_action( 'woocommerce_order_status_changed', array( $this, 'update_on_order_status_changed' ), 10, 4 );
add_action( 'woocommerce_before_trash_order', array( $this, 'update_on_order_trashed' ), 10, 2 );
add_action( 'woocommerce_before_delete_order', array( $this, 'update_on_order_deleted' ), 10, 2 );
- add_action( self::BACKGROUND_EVENT_HOOK, array( $this, 'refresh_cache' ) );
+ add_action( self::BACKGROUND_EVENT_HOOK, array( $this, 'prime_cache_if_cold' ) );
add_action( 'action_scheduler_ensure_recurring_actions', array( $this, 'schedule_background_actions' ) );
if ( defined( 'WC_PLUGIN_BASENAME' ) ) {
@@ -60,6 +60,9 @@ class OrderCountCacheService {
/**
* Refresh the cache for a given order type.
*
+ * @internal
+ * @deprecated 10.7.0 Was used for handling `woocommerce_refresh_order_count_cache` actions.
+ *
* @param string $order_type The order type.
* @return void
*/
@@ -68,6 +71,24 @@ class OrderCountCacheService {
OrderUtil::get_count_for_type( $order_type );
}
+ /**
+ * Keeps the cache warm for a specific order type to maintain admin performance, especially after extended
+ * periods of inactivity or when the cache has been cleared.
+ *
+ * @internal
+ * @since 10.7.0
+ *
+ * @param string $order_type The order type.
+ * @return void
+ */
+ public function prime_cache_if_cold( $order_type ) {
+ // Cache warm-up is only effective when an object cache plugin is active, and the cache entry is missing.
+ if ( wp_using_ext_object_cache() && null === $this->order_count_cache->get( $order_type ) ) {
+ $this->order_count_cache->flush( $order_type );
+ OrderUtil::get_count_for_type( $order_type );
+ }
+ }
+
/**
* Register background caching for each order type.
*
@@ -76,8 +97,9 @@ class OrderCountCacheService {
public function schedule_background_actions() {
$order_types = wc_get_order_types( 'order-count' );
$frequency = HOUR_IN_SECONDS * 12;
+ $timestamp = time() + $frequency;
foreach ( $order_types as $order_type ) {
- as_schedule_recurring_action( time() + $frequency, $frequency, self::BACKGROUND_EVENT_HOOK, array( $order_type ), 'count', true );
+ as_schedule_recurring_action( $timestamp, $frequency, self::BACKGROUND_EVENT_HOOK, array( $order_type ), 'count', true );
}
}
@@ -98,23 +120,26 @@ class OrderCountCacheService {
* @param WC_Order $order The order.
*/
public function update_on_new_order( $order_id, $order ) {
- if ( ! $this->order_count_cache->is_cached( $order->get_type(), $this->get_prefixed_status( $order->get_status() ) ) ) {
+ $order_type = $order->get_type();
+ $order_status = $order->get_status();
+
+ if ( ! $this->order_count_cache->is_cached( $order_type, $this->get_prefixed_status( $order_status ) ) ) {
return;
}
// If the order status was updated, we need to increment the order count cache for the
// initial status that was errantly decremented on order status change.
if ( isset( $this->initial_order_statuses[ $order_id ] ) ) {
- $this->order_count_cache->increment( $order->get_type(), $this->get_prefixed_status( $this->initial_order_statuses[ $order_id ] ) );
+ $this->order_count_cache->increment( $order_type, $this->get_prefixed_status( $this->initial_order_statuses[ $order_id ] ) );
}
// If the order status count has already been incremented, we can skip incrementing it again.
- if ( isset( $this->order_statuses[ $order->get_id() ] ) && $this->order_statuses[ $order->get_id() ] === $order->get_status() ) {
+ if ( isset( $this->order_statuses[ $order_id ] ) && $this->order_statuses[ $order_id ] === $order_status ) {
return;
}
- $this->order_statuses[ $order_id ] = $order->get_status();
- $this->order_count_cache->increment( $order->get_type(), $this->get_prefixed_status( $order->get_status() ) );
+ $this->order_statuses[ $order_id ] = $order_status;
+ $this->order_count_cache->increment( $order_type, $this->get_prefixed_status( $order_status ) );
}
/**
@@ -124,14 +149,17 @@ class OrderCountCacheService {
* @param WC_Order $order The order.
*/
public function update_on_order_trashed( $order_id, $order ) {
+ $order_type = $order->get_type();
+ $order_status = $order->get_status();
+
if (
- ! $this->order_count_cache->is_cached( $order->get_type(), $this->get_prefixed_status( $order->get_status() ) ) ||
- ! $this->order_count_cache->is_cached( $order->get_type(), OrderStatus::TRASH ) ) {
+ ! $this->order_count_cache->is_cached( $order_type, $this->get_prefixed_status( $order_status ) ) ||
+ ! $this->order_count_cache->is_cached( $order_type, OrderStatus::TRASH ) ) {
return;
}
- $this->order_count_cache->decrement( $order->get_type(), $this->get_prefixed_status( $order->get_status() ) );
- $this->order_count_cache->increment( $order->get_type(), OrderStatus::TRASH );
+ $this->order_count_cache->decrement( $order_type, $this->get_prefixed_status( $order_status ) );
+ $this->order_count_cache->increment( $order_type, OrderStatus::TRASH );
}
/**
@@ -141,11 +169,14 @@ class OrderCountCacheService {
* @param WC_Order $order The order.
*/
public function update_on_order_deleted( $order_id, $order ) {
- if ( ! $this->order_count_cache->is_cached( $order->get_type(), $this->get_prefixed_status( $order->get_status() ) ) ) {
+ $order_type = $order->get_type();
+ $order_status = $order->get_status();
+
+ if ( ! $this->order_count_cache->is_cached( $order_type, $this->get_prefixed_status( $order_status ) ) ) {
return;
}
- $this->order_count_cache->decrement( $order->get_type(), $this->get_prefixed_status( $order->get_status() ) );
+ $this->order_count_cache->decrement( $order_type, $this->get_prefixed_status( $order_status ) );
}
/**
@@ -157,9 +188,11 @@ class OrderCountCacheService {
* @param WC_Order $order The order.
*/
public function update_on_order_status_changed( $order_id, $previous_status, $next_status, $order ) {
+ $order_type = $order->get_type();
+
if (
- ! $this->order_count_cache->is_cached( $order->get_type(), $this->get_prefixed_status( $next_status ) ) ||
- ! $this->order_count_cache->is_cached( $order->get_type(), $this->get_prefixed_status( $previous_status ) )
+ ! $this->order_count_cache->is_cached( $order_type, $this->get_prefixed_status( $next_status ) ) ||
+ ! $this->order_count_cache->is_cached( $order_type, $this->get_prefixed_status( $previous_status ) )
) {
return;
}
@@ -170,8 +203,8 @@ class OrderCountCacheService {
}
$this->order_statuses[ $order_id ] = $next_status;
- $was_decremented = $this->order_count_cache->decrement( $order->get_type(), $this->get_prefixed_status( $previous_status ) );
- $this->order_count_cache->increment( $order->get_type(), $this->get_prefixed_status( $next_status ) );
+ $was_decremented = $this->order_count_cache->decrement( $order_type, $this->get_prefixed_status( $previous_status ) );
+ $this->order_count_cache->increment( $order_type, $this->get_prefixed_status( $next_status ) );
// Set the initial order status in case this is a new order and the previous status should not be decremented.
if ( ! isset( $this->initial_order_statuses[ $order_id ] ) && $was_decremented ) {
diff --git a/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php b/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php
index 775744673fa..cf06a74afdb 100644
--- a/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php
+++ b/plugins/woocommerce/tests/php/src/Caching/OrderCountCacheServiceTest.php
@@ -148,7 +148,7 @@ class OrderCountCacheServiceTest extends \WC_Unit_Test_Case {
/**
* Test that refresh cache works.
*/
- public function test_refresh_cache() {
+ public function test_refresh_cache(): void {
$count = OrderUtil::get_count_for_type( 'shop_order' );
$pending_count = $count[ OrderInternalStatus::PENDING ];
// Set the pending count to a higher value to ensure it is refreshed.
@@ -159,4 +159,60 @@ class OrderCountCacheServiceTest extends \WC_Unit_Test_Case {
$this->assertSame( $pending_count, $this->order_cache->get( 'shop_order', array( OrderInternalStatus::PENDING ) )[ OrderInternalStatus::PENDING ] );
}
+
+ /**
+ * Test prime_cache_if_cold method: engaging when the cache is cold.
+ */
+ public function test_prime_cache_if_cold_when_cache_is_cold(): void {
+ global $_wp_using_ext_object_cache;
+ $_before = $_wp_using_ext_object_cache;
+ $_wp_using_ext_object_cache = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ $this->order_cache->flush();
+ $this->assertNull( $this->order_cache->get( 'shop_order', array( OrderInternalStatus::PENDING ) ) );
+
+ // We expect the cache to be populated with the relevant values.
+ $order_count_cache_service = wc_get_container()->get( OrderCountCacheService::class );
+ $order_count_cache_service->prime_cache_if_cold( 'shop_order' );
+
+ $cached = $this->order_cache->get( 'shop_order', array( OrderInternalStatus::PENDING ) );
+ $this->assertNotNull( $cached );
+ $this->assertArrayHasKey( OrderInternalStatus::PENDING, $cached );
+
+ $_wp_using_ext_object_cache = $_before; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ /**
+ * Test prime_cache_if_cold method: not engaging when cache is warm.
+ */
+ public function test_prime_cache_if_cold_when_cache_is_warm(): void {
+ global $_wp_using_ext_object_cache;
+ $_before = $_wp_using_ext_object_cache;
+ $_wp_using_ext_object_cache = true; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+
+ $pending_count = OrderUtil::get_count_for_type( 'shop_order' )[ OrderInternalStatus::PENDING ];
+ $this->order_cache->set( 'shop_order', OrderInternalStatus::PENDING, $pending_count + 10 );
+
+ // We expect the cached values to remain same as counting skipped for warm caches.
+ $order_count_cache_service = wc_get_container()->get( OrderCountCacheService::class );
+ $order_count_cache_service->prime_cache_if_cold( 'shop_order' );
+
+ $this->assertSame( $pending_count + 10, $this->order_cache->get( 'shop_order', array( OrderInternalStatus::PENDING ) )[ OrderInternalStatus::PENDING ] );
+
+ $_wp_using_ext_object_cache = $_before; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
+ }
+
+ /**
+ * Test prime_cache_if_cold method: not engaging when no object caching plugins are in use.
+ */
+ public function test_prime_cache_if_cold_when_object_cache_unavailable(): void {
+ $this->order_cache->flush();
+ $this->assertNull( $this->order_cache->get( 'shop_order', array( OrderInternalStatus::PENDING ) ) );
+
+ // We expect the cache to remain unpopulated as object caching is unavailable.
+ $order_count_cache_service = wc_get_container()->get( OrderCountCacheService::class );
+ $order_count_cache_service->prime_cache_if_cold( 'shop_order' );
+
+ $this->assertNull( $this->order_cache->get( 'shop_order', array( OrderInternalStatus::PENDING ) ) );
+ }
}