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 ) ) );
+	}
 }