Commit b5422fe135 for woocommerce
commit b5422fe135f0c853f8bf8ab3f1802e881fa83f1d
Author: Jorge A. Torres <jorge.torres@automattic.com>
Date: Thu Dec 11 17:42:13 2025 +0000
Revert "Fully read orders from the CPT datastore (including meta) for sync-on-read" (#62402)
diff --git a/plugins/woocommerce/changelog/revert-61293-fix-43126 b/plugins/woocommerce/changelog/revert-61293-fix-43126
new file mode 100644
index 0000000000..6db8e0cc9b
--- /dev/null
+++ b/plugins/woocommerce/changelog/revert-61293-fix-43126
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Revert to previous sync-on-read logic to prevent unnecessary writes when HPOS and compat mode are enabled.
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 f74c258b51..d0acc398eb 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
@@ -8,7 +8,6 @@
use Automattic\Jetpack\Constants;
use Automattic\WooCommerce\Caches\OrderCache;
use Automattic\WooCommerce\Enums\OrderStatus;
-use Automattic\WooCommerce\Internal\Utilities\PostMetaUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Utilities\OrderUtil;
@@ -779,8 +778,23 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
}
}
}
-
- PostMetaUtil::add_post_meta_safe( $order->get_id(), $meta_data->key, $meta_data->value, false );
+ if ( is_object( $meta_data->value ) && '__PHP_Incomplete_Class' === get_class( $meta_data->value ) ) {
+ $meta_value = maybe_serialize( $meta_data->value );
+ $result = $wpdb->insert(
+ _get_meta_table( 'post' ),
+ array(
+ 'post_id' => $order->get_id(),
+ 'meta_key' => $meta_data->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+ ),
+ array( '%d', '%s', '%s' )
+ );
+ wp_cache_delete( $order->get_id(), 'post_meta' );
+ $logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
+ $logger->warning( sprintf( 'encountered an order meta value of type __PHP_Incomplete_Class during `update_order_meta_from_object` in order with ID %d: "%s"', $order->get_id(), var_export( $meta_value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
+ } else {
+ add_post_meta( $order->get_id(), $meta_data->key, $meta_data->value, false );
+ }
}
// Find remaining meta that was deleted from the order but still present in the associated post.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index e1dbb1f5fa..f078f14461 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -99276,18 +99276,6 @@ parameters:
count: 1
path: src/Internal/Utilities/PluginInstaller.php
- -
- message: '#^Cannot call method warning\(\) on mixed\.$#'
- identifier: method.nonObject
- count: 3
- path: src/Internal/Utilities/PostMetaUtil.php
-
- -
- message: '#^Parameter \#1 \$data of function maybe_serialize expects array\|object\|string, mixed given\.$#'
- identifier: argument.type
- count: 3
- path: src/Internal/Utilities/PostMetaUtil.php
-
-
message: '#^Binary operation "\." between ''wc_child_has…''\|''wc_child_has_weight_''\|''wc_product_children_''\|''wc_related_''\|''wc_var_prices_'' and Automattic\\WooCommerce\\Internal\\Utilities\\WC_Product\|int results in an error\.$#'
identifier: binaryOp.invalid
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/LegacyDataHandler.php b/plugins/woocommerce/src/Internal/DataStores/Orders/LegacyDataHandler.php
index c9cc817231..79eac797df 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/LegacyDataHandler.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/LegacyDataHandler.php
@@ -305,13 +305,30 @@ class LegacyDataHandler {
$order = new $classname();
$order->set_id( $order_id );
+ // Switch datastore if necessary.
+ $update_data_store_func = function ( $data_store ) {
+ // Each order object contains a reference to its data store, but this reference is itself
+ // held inside of an instance of WC_Data_Store, so we create that first.
+ $data_store_wrapper = \WC_Data_Store::load( 'order' );
+
+ // Bind $data_store to our WC_Data_Store.
+ ( function ( $data_store ) {
+ $this->current_class_name = get_class( $data_store );
+ $this->instance = $data_store;
+ } )->call( $data_store_wrapper, $data_store );
+
+ // Finally, update the $order object with our WC_Data_Store( $data_store ) instance.
+ $this->data_store = $data_store_wrapper;
+ };
+ $update_data_store_func->call( $order, $data_store );
+
// Read order (without triggering sync) -- we create our own callback instead of using `__return_false` to
// prevent `remove_filter()` from removing it in cases where it was already hooked by 3rd party code.
$prevent_sync_on_read = fn() => false;
add_filter( 'woocommerce_hpos_enable_sync_on_read', $prevent_sync_on_read, 999 );
try {
- $this->read_order_from_datastore( $order, $data_store );
+ $data_store->read( $order );
} finally {
remove_filter( 'woocommerce_hpos_enable_sync_on_read', $prevent_sync_on_read, 999 );
}
@@ -319,30 +336,6 @@ class LegacyDataHandler {
return $order;
}
- /**
- * Reads an order from a datastore fully changing the instance of the datastore inside the order object.
- *
- * @since 10.4.0
- *
- * @param \WC_Abstract_Order &$order The order object to read from the datastore.
- * @param object &$data_store The datastore to read from.
- */
- public function read_order_from_datastore( \WC_Abstract_Order &$order, &$data_store ) {
- // Change the instance of the datastore inside the datastore object.
- ( function () use ( $data_store ) {
- if ( $this->data_store instanceof \WC_Data_Store ) {
- ( function () use ( $data_store ) {
- $this->current_class_name = get_class( $data_store );
- $this->instance = $data_store;
- } )->call( $this->data_store );
- } else {
- $this->data_store = $data_store;
- }
- } )->call( $order );
-
- $data_store->read( $order );
- }
-
/**
* Backfills an order from/to the CPT or HPOS datastore.
*
diff --git a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
index 58808e496e..6e73c1e1c9 100644
--- a/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
+++ b/plugins/woocommerce/src/Internal/DataStores/Orders/OrdersTableDataStore.php
@@ -20,7 +20,6 @@ use WC_Abstract_Order;
use WC_Data;
use WC_Order;
use Automattic\WooCommerce\Internal\Fulfillments\FulfillmentUtils;
-use Automattic\WooCommerce\Internal\Utilities\PostMetaUtil;
defined( 'ABSPATH' ) || exit;
@@ -1541,8 +1540,6 @@ WHERE
* @throws \Exception If no CPT data store is found for an order.
*/
private function get_post_orders_for_ids( array $orders ): array {
- $legacy_handler = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\LegacyDataHandler::class );
-
$order_ids = array_keys( $orders );
foreach ( $order_ids as $order_id ) {
// Exclude orders where the CPT version is a placeholder post.
@@ -1587,7 +1584,7 @@ WHERE
try {
$cpt_order->set_id( $order_id );
- $legacy_handler->read_order_from_datastore( $cpt_order, $cpt_store );
+ $cpt_store->read( $cpt_order );
$cpt_orders[ $order_id ] = $cpt_order;
} catch ( Exception $e ) {
// If the post record has been deleted (for instance, by direct query) then an exception may be thrown.
@@ -1649,25 +1646,15 @@ WHERE
*
* Also provides an option to sync the metadata as well, since we are already computing the diff.
*
- * @param \WC_Abstract_Order $order1 Order object read from COT.
- * @param \WC_Abstract_Order $order2 Order object read from posts.
+ * @param \WC_Abstract_Order $order1 Order object read from posts.
+ * @param \WC_Abstract_Order $order2 Order object read from COT.
* @param bool $sync Whether to also sync the meta data.
*
* @return array Difference between post and COT meta data.
*/
private function get_diff_meta_data_between_orders( \WC_Abstract_Order &$order1, \WC_Abstract_Order $order2, $sync = false ): array {
- $order1_meta = ArrayUtil::select( $order1->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
- $order2_meta = ArrayUtil::select( $order2->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
-
- // Canonicalize metadata by converting scalar to string for comparison purposes.
- if ( ! $sync ) {
- $maybe_convert_to_string = function ( &$m ) {
- $m['value'] = is_scalar( $m['value'] ) ? (string) $m['value'] : $m['value'];
- };
- array_walk( $order1_meta, $maybe_convert_to_string );
- array_walk( $order2_meta, $maybe_convert_to_string );
- }
-
+ $order1_meta = ArrayUtil::select( $order1->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
+ $order2_meta = ArrayUtil::select( $order2->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD );
$order1_meta_by_key = ArrayUtil::select_as_assoc( $order1_meta, 'key', ArrayUtil::SELECT_BY_ARRAY_KEY );
$order2_meta_by_key = ArrayUtil::select_as_assoc( $order2_meta, 'key', ArrayUtil::SELECT_BY_ARRAY_KEY );
@@ -1687,7 +1674,7 @@ WHERE
$order2_values = ArrayUtil::select( $order2_meta_by_key[ $key ], 'value', ArrayUtil::SELECT_BY_ARRAY_KEY );
$new_diff = ArrayUtil::deep_assoc_array_diff( $order1_values, $order2_values );
- if ( ! empty( $new_diff ) ) {
+ if ( ! empty( $new_diff ) && $sync ) {
if ( count( $order2_values ) > 1 ) {
$sync && $order1->delete_meta_data( $key );
foreach ( $order2_values as $post_order_value ) {
@@ -3308,6 +3295,8 @@ CREATE TABLE $meta_table (
* @return bool
*/
public function delete_meta( &$object, $meta ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
+ global $wpdb;
+
if ( $this->should_backfill_post_record() && isset( $meta->id ) ) {
// Let's get the actual meta key before its deleted for backfilling. We cannot delete just by ID because meta IDs are different in HPOS and posts tables.
$db_meta = $this->data_store_meta->get_metadata_by_id( $meta->id );
@@ -3322,7 +3311,23 @@ CREATE TABLE $meta_table (
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() && isset( $meta->key ) ) {
self::$backfilling_order_ids[] = $object->get_id();
- PostMetaUtil::delete_post_meta_safe( $object->get_id(), $meta->key, $meta->value );
+ if ( is_object( $meta->value ) && '__PHP_Incomplete_Class' === get_class( $meta->value ) ) {
+ $meta_value = maybe_serialize( $meta->value );
+ $wpdb->delete(
+ _get_meta_table( 'post' ),
+ array(
+ 'post_id' => $object->get_id(),
+ 'meta_key' => $meta->key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
+ 'meta_value' => $meta_value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
+ ),
+ array( '%d', '%s', '%s' )
+ );
+ wp_cache_delete( $object->get_id(), 'post_meta' );
+ $logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
+ $logger->warning( sprintf( 'encountered an order meta value of type __PHP_Incomplete_Class during `delete_meta` in order with ID %d: "%s"', $object->get_id(), var_export( $meta_value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
+ } else {
+ delete_post_meta( $object->get_id(), $meta->key, $meta->value );
+ }
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
}
@@ -3365,7 +3370,7 @@ CREATE TABLE $meta_table (
if ( ! $changes_applied && $object instanceof WC_Abstract_Order && $this->should_backfill_post_record() ) {
self::$backfilling_order_ids[] = $object->get_id();
- PostMetaUtil::update_post_meta_safe( $object->get_id(), $meta->key, $meta->value );
+ update_post_meta( $object->get_id(), $meta->key, $meta->value );
self::$backfilling_order_ids = array_diff( self::$backfilling_order_ids, array( $object->get_id() ) );
}
diff --git a/plugins/woocommerce/src/Internal/Utilities/PostMetaUtil.php b/plugins/woocommerce/src/Internal/Utilities/PostMetaUtil.php
deleted file mode 100644
index bcead96f2f..0000000000
--- a/plugins/woocommerce/src/Internal/Utilities/PostMetaUtil.php
+++ /dev/null
@@ -1,133 +0,0 @@
-<?php
-declare( strict_types=1 );
-
-namespace Automattic\WooCommerce\Internal\Utilities;
-
-use Automattic\WooCommerce\Proxies\LegacyProxy;
-
-/**
- * A class of utilities for dealing with post meta.
- *
- * @since 10.4.0
- */
-class PostMetaUtil {
-
- /**
- * Check if a value is an incomplete object.
- *
- * @param mixed $value The value to check.
- * @return bool TRUE if the value is an incomplete object, FALSE otherwise.
- */
- private static function is_incomplete_object( $value ): bool {
- return is_object( $value ) && '__PHP_Incomplete_Class' === get_class( $value );
- }
-
- /**
- * Add a post meta value safely, ensuring incomplete objects are handled gracefully.
- *
- * @param int $post_id The post ID.
- * @param string $key The meta key.
- * @param mixed $value The meta value.
- * @param bool $unique Whether the meta value should be unique.
- * @return bool
- */
- public static function add_post_meta_safe( int $post_id, string $key, $value, bool $unique = false ): bool {
- global $wpdb;
-
- if ( ! self::is_incomplete_object( $value ) ) {
- return (bool) add_post_meta( $post_id, $key, $value, $unique );
- }
-
- if ( $unique && metadata_exists( 'post', $post_id, $key ) ) {
- return false;
- }
-
- $value = maybe_serialize( $value );
- $result = $wpdb->insert(
- _get_meta_table( 'post' ),
- array(
- 'post_id' => $post_id,
- 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
- 'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
- ),
- array( '%d', '%s', '%s' )
- );
- wp_cache_delete( $post_id, 'post_meta' );
-
- $logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
- $logger->warning( sprintf( 'encountered a post meta value of type __PHP_Incomplete_Class during `add_post_meta_safe` in post with ID %d and key %s: "%s"', $post_id, $key, var_export( $value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
-
- return (bool) $result;
- }
-
- /**
- * Delete a post meta value safely, ensuring incomplete objects are handled gracefully.
- *
- * @param int $post_id The post ID.
- * @param string $key The meta key.
- * @param mixed $value The meta value.
- * @return bool
- */
- public static function delete_post_meta_safe( int $post_id, string $key, $value ): bool {
- global $wpdb;
-
- if ( ! self::is_incomplete_object( $value ) ) {
- return delete_post_meta( $post_id, $key, $value );
- }
-
- $value = maybe_serialize( $value );
-
- $result = $wpdb->delete(
- _get_meta_table( 'post' ),
- array(
- 'post_id' => $post_id,
- 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
- 'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
- ),
- array( '%d', '%s', '%s' )
- );
- wp_cache_delete( $post_id, 'post_meta' );
-
- $logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
- $logger->warning( sprintf( 'encountered a post meta value of type __PHP_Incomplete_Class during `delete_post_meta_safe` in post with ID %d and key %s: "%s"', $post_id, $key, var_export( $value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
-
- return (bool) $result;
- }
-
- /**
- * Update a post meta value safely, ensuring incomplete objects are handled gracefully.
- *
- * @param int $post_id The post ID.
- * @param string $key The meta key.
- * @param mixed $value The meta value.
- * @return bool
- */
- public static function update_post_meta_safe( int $post_id, string $key, $value ): bool {
- global $wpdb;
-
- if ( ! self::is_incomplete_object( $value ) ) {
- return (bool) update_post_meta( $post_id, $key, $value );
- }
-
- $value = maybe_serialize( $value );
-
- $result = $wpdb->update(
- _get_meta_table( 'post' ),
- array(
- 'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
- ),
- array(
- 'post_id' => $post_id,
- 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
- ),
- array( '%s' ),
- array( '%d', '%s' )
- );
- wp_cache_delete( $post_id, 'post_meta' );
-
- $logger = wc_get_container()->get( LegacyProxy::class )->call_function( 'wc_get_logger' );
- $logger->warning( sprintf( 'encountered a post meta value of type __PHP_Incomplete_Class during `update_post_meta_safe` in post with ID %d and key %s: "%s"', $post_id, $key, var_export( $value, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_var_export
-
- return (bool) $result;
- }
-}
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 a951bac96e..cce4e759ff 100644
--- a/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
+++ b/plugins/woocommerce/tests/php/src/Internal/DataStores/Orders/OrdersTableDataStoreTests.php
@@ -3494,11 +3494,7 @@ class OrdersTableDataStoreTests extends \HposTestCase {
$this->assertEquals( 'Europe/Brussels', $meta_object_vars['timezone'] );
// Check that the log entry was created.
- $serialized_meta_value = '"\'O:11:"geoiprecord":14:{s:12:"country_code";s:2:"BE";s:13:"country_code3";s:3:"BEL";s:12:"country_name";s:7:"Belgium";s:6:"region";s:3:"BRU";s:4:"city";s:8:"Brussels";s:11:"postal_code";s:4:"1000";s:8:"latitude";d:50.8333;s:9:"longitude";d:4.3333;s:9:"area_code";N;s:8:"dma_code";N;s:10:"metro_code";N;s:14:"continent_code";s:2:"EU";s:11:"region_name";s:16:"Brussels Capital";s:8:"timezone";s:15:"Europe/Brussels";}\'"';
-
- $log_message = end( $fake_logger->warnings )['message'];
- $this->assertStringContainsString( 'encountered a post meta value of type __PHP_Incomplete_Class during', $log_message );
- $this->assertStringContainsString( $serialized_meta_value, $log_message );
+ $this->assertEquals( 'encountered an order meta value of type __PHP_Incomplete_Class during `update_order_meta_from_object` in order with ID ' . $order->get_id() . ': "\'O:11:"geoiprecord":14:{s:12:"country_code";s:2:"BE";s:13:"country_code3";s:3:"BEL";s:12:"country_name";s:7:"Belgium";s:6:"region";s:3:"BRU";s:4:"city";s:8:"Brussels";s:11:"postal_code";s:4:"1000";s:8:"latitude";d:50.8333;s:9:"longitude";d:4.3333;s:9:"area_code";N;s:8:"dma_code";N;s:10:"metro_code";N;s:14:"continent_code";s:2:"EU";s:11:"region_name";s:16:"Brussels Capital";s:8:"timezone";s:15:"Europe/Brussels";}\'"', end( $fake_logger->warnings )['message'] );
// Test deleting meta data containing an object of a non-existent class.
$meta_data = $this->sut->read_meta( $order );
@@ -3511,9 +3507,7 @@ class OrdersTableDataStoreTests extends \HposTestCase {
$this->assertEquals( '', get_post_meta( $order->get_id(), $meta_key, true ) );
// Check that the log entry was created.
- $log_message = end( $fake_logger->warnings )['message'];
- $this->assertStringContainsString( 'encountered a post meta value of type __PHP_Incomplete_Class during', $log_message );
- $this->assertStringContainsString( $serialized_meta_value, $log_message );
+ $this->assertEquals( 'encountered an order meta value of type __PHP_Incomplete_Class during `delete_meta` in order with ID ' . $order->get_id() . ': "\'O:11:"geoiprecord":14:{s:12:"country_code";s:2:"BE";s:13:"country_code3";s:3:"BEL";s:12:"country_name";s:7:"Belgium";s:6:"region";s:3:"BRU";s:4:"city";s:8:"Brussels";s:11:"postal_code";s:4:"1000";s:8:"latitude";d:50.8333;s:9:"longitude";d:4.3333;s:9:"area_code";N;s:8:"dma_code";N;s:10:"metro_code";N;s:14:"continent_code";s:2:"EU";s:11:"region_name";s:16:"Brussels Capital";s:8:"timezone";s:15:"Europe/Brussels";}\'"', end( $fake_logger->warnings )['message'] );
}
/**
@@ -3880,26 +3874,6 @@ class OrdersTableDataStoreTests extends \HposTestCase {
$this->assertEquals( $wpdb->get_var( $query ), 2 ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- query has already been prepared.
}
- /**
- * @testdox Sync-on-read should update metadata as well.
- */
- public function test_sync_on_read_updates_metadata() {
- $this->toggle_cot_feature_and_usage( true );
- $this->enable_cot_sync();
-
- $order = OrderHelper::create_order();
- $order->add_meta_data( 'foo', 'bar' );
- $order->save();
-
- // Update the meta data on the post.
- update_post_meta( $order->get_id(), 'foo', 'baz' );
-
- $fresh_order = wc_get_order( $order->get_id() );
-
- $this->assertEquals( 'baz', get_post_meta( $order->get_id(), 'foo', true ) );
- $this->assertEquals( 'baz', $fresh_order->get_meta( 'foo', true, 'edit' ) );
- }
-
/**
* @testdox An order deleted from the posts table while sync was off is deleted from the orders table when sync runs.
*/