Commit 1879d1d18af for woocommerce

commit 1879d1d18af6067bb03fd7d19e0b7552b4a6a2b7
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Fri Mar 27 11:43:50 2026 +0100

    [Performance] Non-transient based caching for needs_processing (#63768)

    This update reduces the cost of constructing product objects in needs_processing by bulk priming order(s) products, allowing a transition from transients to cache APIs.

diff --git a/plugins/woocommerce/changelog/performance-57937-explore-needs-processing-enhancement b/plugins/woocommerce/changelog/performance-57937-explore-needs-processing-enhancement
new file mode 100644
index 00000000000..9e8129a8c0a
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-57937-explore-needs-processing-enhancement
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Reduced the number of SQL queries required to handle order collections in both the storefront and REST interfaces.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
index ab1975c7764..a21c69f667a 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-order.php
@@ -306,7 +306,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		}

 		if ( $items_changed ) {
-			delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' );
+			wp_cache_delete( 'order-needs-processing-' . $this->get_id(), 'orders' );

 			// Invalidate the order cache to prevent stale item data.
 			// This fixes a race condition where get_items() may have been called
@@ -947,7 +947,17 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {

 			if ( $group ) {
 				if ( ! isset( $this->items[ $group ] ) ) {
-					$this->items[ $group ] = array_filter( $this->data_store->read_items( $this, $type ) );
+					$read_items = array_filter( $this->data_store->read_items( $this, $type ) );
+
+					// Prime the product cache to ensure that methods such as needs_processing, get_downloadable_items, and has_downloadable_item run
+					// on warm post meta caches for products. This addresses scenarios where the order object was not populated during a batch population.
+					if ( 'line_item' === $type && ! empty( $read_items ) ) {
+						$product_ids = array_map( static fn( $item ) => $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(), $read_items );
+						$product_ids = array_unique( array_filter( $product_ids ) );
+						_prime_post_caches( $product_ids );
+					}
+
+					$this->items[ $group ] = $read_items;
 				}
 				// Don't use array_merge here because keys are numeric.
 				$items = $items + $this->items[ $group ];
@@ -1660,7 +1670,7 @@ abstract class WC_Abstract_Order extends WC_Abstract_Legacy_Order {
 		$item->save();
 		$this->add_item( $item );
 		wc_do_deprecated_action( 'woocommerce_order_add_product', array( $this->get_id(), $item->get_id(), $product, $qty, $args ), '3.0', 'woocommerce_new_order_item action instead' );
-		delete_transient( 'wc_order_' . $this->get_id() . '_needs_processing' );
+		wp_cache_delete( 'order-needs-processing-' . $this->get_id(), 'orders' );
 		return $item->get_id();
 	}

diff --git a/plugins/woocommerce/includes/class-wc-order.php b/plugins/woocommerce/includes/class-wc-order.php
index f93fd6244b8..4a011e4c26e 100644
--- a/plugins/woocommerce/includes/class-wc-order.php
+++ b/plugins/woocommerce/includes/class-wc-order.php
@@ -1862,16 +1862,15 @@ class WC_Order extends WC_Abstract_Order {
 	 * Orders which only contain virtual, downloadable items do not need admin
 	 * intervention.
 	 *
-	 * Uses a transient so these calls are not repeated multiple times, and because
-	 * once the order is processed this code/transient does not need to persist.
-	 *
+	 * @since 10.8.0 the method no longer uses transients.
 	 * @since 3.0.0
+	 *
 	 * @return bool
 	 */
 	public function needs_processing() {
-		$transient_name   = 'wc_order_' . $this->get_id() . '_needs_processing';
-		$needs_processing = get_transient( $transient_name );
-
+		$order_id         = $this->get_id();
+		$cache_key        = 'order-needs-processing-' . $order_id;
+		$needs_processing = wp_cache_get( $cache_key, 'orders' );
 		if ( false === $needs_processing ) {
 			$needs_processing = 0;

@@ -1880,22 +1879,30 @@ class WC_Order extends WC_Abstract_Order {
 				foreach ( $line_items as $item ) {
 					if ( $item->is_type( 'line_item' ) ) {
 						$product = $item->get_product();
-
-						if ( ! $product ) {
-							continue;
-						}
-
-						$virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual();
-
-						if ( apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) {
-							$needs_processing = 1;
-							break;
+						if ( $product ) {
+							$virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual();
+							/**
+							 * Filters whether an order line item requires processing. By default, only
+							 * virtual downloadable items do not require processing; all other items do.
+							 *
+							 * @since 2.7.0
+							 *
+							 * @param bool        $needs_processing
+							 * @param \WC_Product $product
+							 * @param int         $order_id
+							 * @return bool
+							 */
+							$custom_needs_processing = (bool) apply_filters( 'woocommerce_order_item_needs_processing', ! $virtual_downloadable_item, $product, $order_id );
+							if ( $custom_needs_processing ) {
+								$needs_processing = 1;
+								break;
+							}
 						}
 					}
 				}
 			}

-			set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS );
+			wp_cache_set( $cache_key, $needs_processing, 'orders', DAY_IN_SECONDS );
 		}

 		return 1 === absint( $needs_processing );
diff --git a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
index 5260753b308..accb74dc100 100644
--- a/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/abstract-wc-order-data-store-cpt.php
@@ -593,12 +593,7 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
 				return;
 			}
 		}
-		$cache_keys     = array_map(
-			function ( $order_id ) {
-				return 'order-items-' . $order_id;
-			},
-			$order_ids
-		);
+		$cache_keys     = array_map( static fn( $order_id ) => 'order-items-' . $order_id, $order_ids );
 		$cache_values   = wc_cache_get_multiple( $cache_keys, 'orders' );
 		$non_cached_ids = array();
 		foreach ( $order_ids as $order_id ) {
@@ -658,7 +653,35 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
 				$raw_meta_data_collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
 			}
 			\WC_Order_Item::prime_raw_meta_data_cache( $raw_meta_data_collection, 'order-items' );
+
+			$this->prime_product_post_caches_for_order_items( $order_items, $raw_meta_data_collection );
+		}
+	}
+
+	/**
+	 * Primes post caches for products which are referenced in line items with 'line_item' type.
+	 *
+	 * Although the product data store can be replaced, maintaining the posts table connection, as with HPOS, is necessary
+	 * for products to function properly. We can therefore prime the post cache directly without compromising store isolation.
+	 *
+	 * @since 10.8.0
+	 *
+	 * @param array<int,object{order_item_id:int, order_item_type:string}>    $line_items_all           Line item entries.
+	 * @param array<int,array<int,object{meta_key:string, meta_value:mixed}>> $raw_meta_data_collection Meta-entries grouped by line item id.
+	 * @return void
+	 */
+	private function prime_product_post_caches_for_order_items( array $line_items_all, array $raw_meta_data_collection ): void {
+		$product_ids = array();
+		foreach ( $line_items_all as $line_item ) {
+			if ( 'line_item' === $line_item->order_item_type ) {
+				foreach ( $raw_meta_data_collection[ $line_item->order_item_id ] ?? array() as $meta ) {
+					if ( ( '_variation_id' === $meta->meta_key || '_product_id' === $meta->meta_key ) && $meta->meta_value > 0 ) {
+						$product_ids[] = (int) $meta->meta_value;
+					}
+				}
+			}
 		}
+		_prime_post_caches( array_unique( $product_ids ) );
 	}

 	/**
@@ -736,19 +759,14 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
 	 * object cache for all transient option names in a single query, we
 	 * eliminate the N+1.
 	 *
+	 * @since 10.7.0
+	 * @deprecated 10.8.0 `\WC_Order::needs_processing` method no longer uses transients.
+	 *
 	 * @param array $order_ids  Order IDs to prime cache for.
 	 * @param array $query_vars Query vars for the query.
 	 * @return void
-	 * @since 10.7.0
 	 */
 	protected function prime_needs_processing_transients( $order_ids, $query_vars ) {
-		$option_names = array();
-		foreach ( $order_ids as $order_id ) {
-			$option_names[] = '_transient_wc_order_' . $order_id . '_needs_processing';
-			$option_names[] = '_transient_timeout_wc_order_' . $order_id . '_needs_processing';
-		}
-
-		wp_prime_option_caches( $option_names );
 	}

 	/**
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
index 3060c096854..975d40184f7 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-order-data-store-cpt.php
@@ -1181,7 +1181,6 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement

 		$this->prime_refund_caches_for_orders( $order_ids, $query_vars );
 		$this->prime_refund_total_caches_for_orders( $order_ids, $query_vars );
-		$this->prime_needs_processing_transients( $order_ids, $query_vars );
 	}

 	/**
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php
index 0f41e9ad347..b80c77501e8 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-order-item-data-store.php
@@ -184,6 +184,7 @@ class WC_Order_Item_Data_Store implements WC_Order_Item_Data_Store_Interface {
 		}
 		if ( $order_id ) {
 			wp_cache_delete( 'order-items-' . $order_id, 'orders' );
+			wp_cache_delete( 'order-needs-processing-' . $order_id, 'orders' );
 		}
 	}
 }
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
index 5da09df805d..058d7f0c3cc 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
@@ -3150,7 +3150,6 @@ FROM $order_meta_table

 		$this->prime_refund_caches_for_orders( $order_ids, $query_vars );
 		$this->prime_refund_total_caches_for_orders( $order_ids, $query_vars );
-		$this->prime_needs_processing_transients( $order_ids, $query_vars );
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
index 2e36b8b9054..4d09ff7d147 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -4110,4 +4110,36 @@ class OrdersTableDataStoreTests extends \HposTestCase {
 		$this->assertCount( 1, $fresh_order->get_items(), 'Order should have items from DB.' );
 		$this->assertTrue( $fresh_order->needs_processing(), 'Order with physical product should need processing.' );
 	}
+
+	/**
+	 * Verifies that needs_processing caching functions as intended.
+	 */
+	public function test_needs_processing_caching_correctness(): void {
+		$product = WC_Helper_Product::create_simple_product();
+		$product->save();
+
+		$order = new WC_Order();
+		$item  = new WC_Order_Item_Product();
+		$item->set_props(
+			array(
+				'product'  => $product,
+				'quantity' => 1,
+				'subtotal' => 10,
+				'total'    => 10,
+			)
+		);
+		$order->add_item( $item );
+		$order->save();
+		$order_id = $order->get_id();
+
+		$this->assertTrue( $order->needs_processing() );
+		$this->assertSame( 1, wp_cache_get( 'order-needs-processing-' . $order_id, 'orders' ) );
+
+		wp_cache_set( 'order-needs-processing-' . $order_id, 0, 'orders' );
+		$this->assertFalse( $order->needs_processing() );
+		$this->assertSame( 0, wp_cache_get( 'order-needs-processing-' . $order_id, 'orders' ) );
+
+		$order->delete();
+		$product->delete();
+	}
 }