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.
*