Commit 621bb08c9fd for woocommerce

commit 621bb08c9fddf432565de20315c5ef902883f4f3
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date:   Thu Jun 4 06:17:56 2026 +0200

    [Performance] Actualize line item product object caching. (#65486)

    Partial revert of #64418:
    - aligns with Enable product object caching by default for new installs #65445
    - addresses a potential regression
    - removes entities that haven't made it to the stable release yet

diff --git a/plugins/woocommerce/changelog/update-64418-integration-with-product-object-caching b/plugins/woocommerce/changelog/update-64418-integration-with-product-object-caching
new file mode 100644
index 00000000000..fd83cb625e7
--- /dev/null
+++ b/plugins/woocommerce/changelog/update-64418-integration-with-product-object-caching
@@ -0,0 +1,4 @@
+Significance: minor
+Type: performance
+
+Updated order line item product caching to leverage the product object caching feature.
diff --git a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
index c27fb97a784..86dbf930778 100644
--- a/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
+++ b/plugins/woocommerce/includes/abstracts/abstract-wc-product.php
@@ -14,7 +14,6 @@ use Automattic\WooCommerce\Enums\ProductStockStatus;
 use Automattic\WooCommerce\Enums\ProductTaxStatus;
 use Automattic\WooCommerce\Enums\ProductType;
 use Automattic\WooCommerce\Enums\CatalogVisibility;
-use Automattic\WooCommerce\Internal\Caches\StaleObjectAttribution;
 use Automattic\WooCommerce\Internal\CostOfGoodsSold\CogsAwareTrait;
 use Automattic\WooCommerce\Internal\ProductAttributesLookup\LookupDataStore as ProductAttributesLookupDataStore;

@@ -34,7 +33,6 @@ require_once WC_ABSPATH . 'includes/legacy/abstract-wc-legacy-product.php';
  */
 class WC_Product extends WC_Abstract_Legacy_Product {
 	use CogsAwareTrait;
-	use StaleObjectAttribution;

 	/**
 	 * This is the name of this object type.
@@ -148,8 +146,6 @@ class WC_Product extends WC_Abstract_Legacy_Product {
 		if ( $this->get_id() > 0 ) {
 			$this->data_store->read( $this );
 		}
-
-		$this->_woocommerce_object_instantiated();
 	}

 	/**
diff --git a/plugins/woocommerce/includes/class-wc-checkout.php b/plugins/woocommerce/includes/class-wc-checkout.php
index dbbb004e34c..6d476bf7869 100644
--- a/plugins/woocommerce/includes/class-wc-checkout.php
+++ b/plugins/woocommerce/includes/class-wc-checkout.php
@@ -559,8 +559,6 @@ class WC_Checkout {
 						'tax_class'    => $product->get_tax_class(),
 						'product_id'   => $product->is_type( ProductType::VARIATION ) ? $product->get_parent_id() : $product->get_id(),
 						'variation_id' => $product->is_type( ProductType::VARIATION ) ? $product->get_id() : 0,
-						// Order model compositions: set the product instance.
-						'product'      => $product,
 					)
 				);
 			}
diff --git a/plugins/woocommerce/includes/class-wc-order-item-product.php b/plugins/woocommerce/includes/class-wc-order-item-product.php
index c8abf5d6701..9a0158eee54 100644
--- a/plugins/woocommerce/includes/class-wc-order-item-product.php
+++ b/plugins/woocommerce/includes/class-wc-order-item-product.php
@@ -56,14 +56,6 @@ class WC_Order_Item_Product extends WC_Order_Item {
 		),
 	);

-	/**
-	 * The product object is set using set_product or is automatically populated by get_product. This property was introduced
-	 * to reduce the number of wc_get_product calls when working with this class in core and extension workflows.
-	 *
-	 * @var null|false|WC_Product
-	 */
-	private $product;
-
 	/*
 	|--------------------------------------------------------------------------
 	| Setters
@@ -100,19 +92,7 @@ class WC_Order_Item_Product extends WC_Order_Item {
 		if ( $value > 0 && 'product' !== get_post_type( absint( $value ) ) ) {
 			$this->error( 'order_item_product_invalid_product_id', __( 'Invalid product ID', 'woocommerce' ) );
 		}
-		$product_id = absint( $value );
-		$this->set_prop( 'product_id', $product_id );
-
-		if ( null !== $this->product ) {
-			// Cached instance invalidation: match the cached product identity against the incoming ID.
-			$cached_product_id = null;
-			if ( $this->product ) {
-				$cached_product_id = $this->product->is_type( ProductType::VARIATION ) ? $this->product->get_parent_id() : $this->product->get_id();
-			}
-			if ( ! $this->product || $cached_product_id !== $product_id ) {
-				$this->product = null;
-			}
-		}
+		$this->set_prop( 'product_id', absint( $value ) );
 	}

 	/**
@@ -129,19 +109,7 @@ class WC_Order_Item_Product extends WC_Order_Item {
 				array( 'variation_id' => $value )
 			);
 		}
-		$variation_id = absint( $value );
-		$this->set_prop( 'variation_id', $variation_id );
-
-		if ( null !== $this->product ) {
-			// Cached instance invalidation: match the cached product identity against the incoming ID.
-			$cached_variation_id = null;
-			if ( $this->product ) {
-				$cached_variation_id = $this->product->is_type( ProductType::VARIATION ) ? $this->product->get_id() : 0;
-			}
-			if ( ! $this->product || $cached_variation_id !== $variation_id ) {
-				$this->product = null;
-			}
-		}
+		$this->set_prop( 'variation_id', absint( $value ) );
 	}

 	/**
@@ -294,7 +262,6 @@ class WC_Order_Item_Product extends WC_Order_Item {
 		}
 		$this->set_name( $product->get_name() );
 		$this->set_tax_class( $product->get_tax_class() );
-		$this->product = $product;
 	}

 	/**
@@ -417,31 +384,18 @@ class WC_Order_Item_Product extends WC_Order_Item {
 	/**
 	 * Get the associated product.
 	 *
-	 * @since 10.9.0 returns the same product instance; if the product is updated or deleted, the instance will be re-instantiated.
 	 * @return WC_Product|bool
 	 */
 	public function get_product() {
-		if ( $this->product ) {
-			$reinstantiate = ! ( $this->product instanceof WC_Product ) ||
-				$this->product->_woocommerce_object_is_stale() ||
-				$this->product->_woocommerce_object_is_dirty();
-			if ( $reinstantiate ) {
-				$this->product = null;
-			}
-		}
-		if ( ! $this->product ) {
-			$product_id    = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id();
-			$this->product = wc_get_product( $product_id );
-		}
+		$product_id = $this->get_variation_id() ? $this->get_variation_id() : $this->get_product_id();
+		$product    = wc_get_product( $product_id );

 		// Backwards compatible filter from WC_Order::get_product_from_item().
 		/** @var WC_Product|false $product */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort
-		$product = $this->product;
 		if ( has_filter( 'woocommerce_get_product_from_item' ) ) {
 			/**
 			 * Modifies the product object returned by \WC_Order_Item_Product::get_product.
 			 *
-			 * @since 10.9.0 the method is caching the product object.
 			 * @since 2.7.0 filter introduction.
 			 *
 			 * @param WC_Product|false      $product   Product instance.
@@ -453,7 +407,6 @@ class WC_Order_Item_Product extends WC_Order_Item {
 		/**
 		 * Modifies the product object returned by \WC_Order_Item_Product::get_product.
 		 *
-		 * @since 10.9.0 the method is caching the product object.
 		 * @since 2.7.0 filter introduction.
 		 *
 		 * @param WC_Product|false      $product   Product instance.
diff --git a/plugins/woocommerce/includes/class-wc-post-data.php b/plugins/woocommerce/includes/class-wc-post-data.php
index cf13876717a..3326045eed1 100644
--- a/plugins/woocommerce/includes/class-wc-post-data.php
+++ b/plugins/woocommerce/includes/class-wc-post-data.php
@@ -39,7 +39,6 @@ class WC_Post_Data {
 	public static function init() {
 		add_action( 'clean_post_cache', array( __CLASS__, 'invalidate_products_last_modified' ), 10, 2 );
 		add_action( 'clean_post_cache', array( __CLASS__, 'invalidate_db_block_templates_cache' ), 10, 2 );
-		add_action( 'clean_post_cache', array( __CLASS__, 'update_stale_product_objects_tracking_cache' ), 10, 2 );
 		add_filter( 'post_type_link', array( __CLASS__, 'variation_post_link' ), 10, 2 );
 		add_action( 'shutdown', array( __CLASS__, 'do_deferred_product_sync' ), 10 );
 		add_action( 'set_object_terms', array( __CLASS__, 'force_default_term' ), 10, 5 );
@@ -209,24 +208,6 @@ class WC_Post_Data {
 		}
 	}

-	/**
-	 * Updates product save/delete operation timestamp as part of stale objects trackings.
-	 *
-	 * @param int      $post_id Post ID.
-	 * @param \WP_Post $post    Post object.
-	 *
-	 * @internal
-	 * @since 10.9.0
-	 *
-	 * @return void
-	 */
-	public static function update_stale_product_objects_tracking_cache( $post_id, $post ): void {
-		if ( $post instanceof \WP_Post && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
-			// Not the greatest design, but we need access to non-static 'object_type' property of the product object.
-			( new WC_Product() )->_woocommerce_entity_persisted( $post_id );
-		}
-	}
-
 	/**
 	 * Handle type changes.
 	 *
diff --git a/plugins/woocommerce/src/Internal/Caches/StaleObjectAttribution.php b/plugins/woocommerce/src/Internal/Caches/StaleObjectAttribution.php
deleted file mode 100644
index 47df90f8aa8..00000000000
--- a/plugins/woocommerce/src/Internal/Caches/StaleObjectAttribution.php
+++ /dev/null
@@ -1,78 +0,0 @@
-<?php declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\Caches;
-
-/**
- * Tracks stale object instances. The decentralized hooks architecture allows multiple modification routes in both
- * WooCommerce core and its extensions. This feature enables the identification of stale objects under those constraints.
- */
-trait StaleObjectAttribution {
-	/**
-	 * Object instantiation timestamp.
-	 *
-	 * @var float|null
-	 */
-	private ?float $_woocommerce_instance_created_at = null; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
-
-	/**
-	 * Registry of persistence (update/delete) operations on the ID-level.
-	 *
-	 * @var array<string,float>
-	 */
-	private static array $_woocommerce_entity_persisted_at = array(); // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
-
-	/**
-	 * Tracks object instantiation timestamp.
-	 *
-	 * @return void
-	 */
-	private function _woocommerce_object_instantiated(): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
-		$this->_woocommerce_instance_created_at = microtime( true );
-	}
-
-	/**
-	 * Tracks object persistence (update/delete) timestamp.
-	 *
-	 * Do not use this method for customization. Although it is public due to architectural constraints, improper use
-	 * may result in stale instance usage, which is critical for functions such as product inventory management.
-	 *
-	 * @param int $entity_id Object ID.
-	 * @return void
-	 */
-	public function _woocommerce_entity_persisted( int $entity_id ): void { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
-		$object_type = $this instanceof \WC_Product ? 'product' : $this->object_type;
-		self::$_woocommerce_entity_persisted_at[ $object_type . ':' . $entity_id ] = microtime( true );
-	}
-
-	/**
-	 * Enables verification of whether the object instance is stale. If it is stale, the object must be re-created,
-	 * as the current architecture does not support refreshing object attributes without replacing the object.
-	 *
-	 * @return bool
-	 */
-	public function _woocommerce_object_is_stale(): bool { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
-		// Unknown instantiation timestamp means freshness cannot be determined — treat as stale to fail safe.
-		if ( null !== $this->_woocommerce_instance_created_at ) {
-			$instance_timestamp  = (float) ( $this->_woocommerce_instance_created_at );
-			$object_type         = $this instanceof \WC_Product ? 'product' : $this->object_type;
-			$entity_persisted_at = (float) ( self::$_woocommerce_entity_persisted_at[ $object_type . ':' . $this->get_id() ] ?? null );
-
-			return $instance_timestamp <= $entity_persisted_at;
-		}
-
-		return true;
-	}
-
-	/**
-	 * Enables verification of whether the object instance is modified.
-	 *
-	 * @return bool
-	 */
-	public function _woocommerce_object_is_dirty(): bool { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
-		$meta_data = $this->meta_data ?? array();
-		$is_dirty  = ! empty( $this->changes );
-		$is_dirty  = $is_dirty || ! empty( array_filter( $meta_data, static fn( $meta ) => ! $meta->id || ! empty( $meta->get_changes() ) ) );
-
-		return $is_dirty;
-	}
-}
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php b/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php
index ed0b890742d..7b433ff8e38 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/checkout/checkout.php
@@ -5,6 +5,8 @@
  * @package WooCommerce\Tests\Checkout
  */

+declare(strict_types=1);
+
 use Automattic\WooCommerce\Enums\OrderStatus;

 /**
@@ -313,4 +315,53 @@ class WC_Tests_Checkout extends WC_Unit_Test_Case {

 		$this->assertEquals( false, WC()->cart->check_cart_items() );
 	}
+
+	/**
+	 * Test that a customer-chosen value for an "any" variation attribute is preserved on the order line item.
+	 */
+	public function test_create_order_preserves_customer_chosen_any_attribute_value(): void {
+		$parent_product = WC_Helper_Product::create_variation_product();
+
+		// Find the first variation that has pa_number as "any" (stored as '').
+		$any_number_variation_id = 0;
+		$any_number_variation    = null;
+		foreach ( $parent_product->get_children() as $child_id ) {
+			$child = wc_get_product( $child_id );
+			if ( '' === ( $child->get_variation_attributes()['attribute_pa_number'] ?? null ) ) {
+				$any_number_variation_id = $child_id;
+				$any_number_variation    = $child;
+				break;
+			}
+		}
+		$this->assertGreaterThan( 0, $any_number_variation_id, 'Expected a variation with an "any" pa_number attribute.' );
+
+		// Build cart attributes: use the variation's fixed values for non-number attributes, supply an arbitrary value for
+		// any remaining "any" attributes, and the customer's chosen value (1) for the "any" pa_number attribute under test.
+		$cart_variation = array(
+			'attribute_pa_size'   => 'small',
+			'attribute_pa_colour' => 'red',
+			'attribute_pa_number' => '1',
+		);
+		WC()->cart->add_to_cart( $parent_product->get_id(), 1, $any_number_variation_id, $cart_variation );
+
+		$order_id = WC_Checkout::instance()->create_order(
+			array(
+				'payment_method' => WC_Gateway_COD::ID,
+				'billing_email'  => 'a@b.com',
+			)
+		);
+		$this->assertNotWPError( $order_id );
+
+		// Re-read from storage to assert the persisted value.
+		/** @var WC_Order_Item_Product[] $items */
+		$items = wc_get_order( $order_id )->get_items();
+		$this->assertCount( 1, $items );
+
+		// The chosen value must survive; premature set_product() overwrites it with '' (empty).
+		$this->assertSame(
+			'1',
+			array_values( $items )[0]->get_meta( 'pa_number' ),
+			'The customer-chosen value for an "Any" variation attribute must be persisted on the order line item.'
+		);
+	}
 }
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/order-items/class-wc-tests-order-item-product.php b/plugins/woocommerce/tests/legacy/unit-tests/order-items/class-wc-tests-order-item-product.php
index 1e04bccbd5c..13c354e512b 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/order-items/class-wc-tests-order-item-product.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/order-items/class-wc-tests-order-item-product.php
@@ -147,62 +147,6 @@ class WC_Tests_Order_Item_Product extends WC_Unit_Test_Case {
 		$this->assertEquals( $variation_product->get_id(), $retrieved->get_id() );
 	}

-	/**
-	 * Validates the product instance caching behaviour.
-	 */
-	public function test_get_get_product_caching(): void {
-		$product_item = new WC_Order_Item_Product();
-		$product      = new WC_Product_Simple();
-		$product->save();
-
-		// Verify: cached instance unchanged after the initial injection.
-		$instance = wc_get_product( $product->get_id() );
-		$product_item->set_product( $instance );
-		$this->assertSame( $product->get_id(), $product_item->get_product_id() );
-		$this->assertSame( $instance, $product_item->get_product() );
-
-		// Verify: cached instance refresh when product is saved.
-		$instance = $product_item->get_product();
-		$instance->set_stock_quantity( 10 );
-		$instance->save();
-		$this->assertNotSame( $instance, $product_item->get_product() );
-
-		// Verify: cached instance refresh after unsaved property mutation.
-		$instance = $product_item->get_product();
-		$instance->set_price( 10.99 );
-		$this->assertNotSame( $instance, $product_item->get_product() );
-
-		// Verify: cached instance refresh after unsaved meta addition.
-		$instance = $product_item->get_product();
-		$instance->add_meta_data( 'key', 'add' );
-		$this->assertNotSame( $instance, $product_item->get_product() );
-		$instance->save();
-
-		// Verify: cached instance refresh after unsaved meta update.
-		$instance = $product_item->get_product();
-		$instance->update_meta_data( 'key', 'update' );
-		$this->assertNotSame( $instance, $product_item->get_product() );
-		$instance->save();
-
-		// Verify: cached instance refresh after unsaved meta delete.
-		$instance = $product_item->get_product();
-		$instance->delete_meta_data( 'key' );
-		$this->assertNotSame( $instance, $product_item->get_product() );
-
-		// Verify: cached instance refresh when product is deleted.
-		$product->delete( true );
-		$this->assertFalse( $product_item->get_product() );
-
-		$replacement = new WC_Product_Simple();
-		$replacement->save();
-
-		// Verify: cached instance invalidated when updating product ID.
-		$product_item->set_product_id( $replacement->get_id() );
-		$this->assertSame( $replacement->get_id(), $product_item->get_product()->get_id() );
-
-		$replacement->delete();
-	}
-
 	/**
 	 * Test get_item_download_url method for WC_Order_Item_Product.
 	 *