Add HPOS CLI tool to compare an order between datastores (#43173)

* Add `wp wc hpos status` command

* Add helper method to build order from different datastores

* Add helper method `get_diff_for_order()` to compare orders between datastores

* Add CLI tool `wp wc hpos diff` to compare an order between datastores

* Add changelog

* PHPCS fixes

* Better format for dates
This commit is contained in:
Jorge A. Torres 2024-01-11 16:51:01 +00:00 committed by GitHub
parent d0d056c60e
commit 9ce508f47d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 245 additions and 0 deletions

View File

@ -0,0 +1,4 @@
Significance: minor
Type: add
Add CLI command `wp wc hpos diff` to compare an order between datastores.

View File

@ -65,6 +65,8 @@ class CLIRunner {
WP_CLI::add_command( 'wc cot enable', array( $this, 'enable' ) );
WP_CLI::add_command( 'wc cot disable', array( $this, 'disable' ) );
WP_CLI::add_command( 'wc hpos cleanup', array( $this, 'cleanup_post_data' ) );
WP_CLI::add_command( 'wc hpos status', array( $this, 'status' ) );
WP_CLI::add_command( 'wc hpos diff', array( $this, 'diff' ) );
}
/**
@ -955,4 +957,110 @@ ORDER BY $meta_table.order_id ASC, $meta_table.meta_key ASC;
);
}
/**
* Displays a summary of HPOS situation on this site.
*
* @since 8.6.0
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
*/
public function status( array $args = array(), array $assoc_args = array() ) {
$legacy_handler = wc_get_container()->get( LegacyDataHandler::class );
// translators: %s is either 'yes' or 'no'.
WP_CLI::log( sprintf( __( 'HPOS enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->controller->custom_orders_table_usage_is_enabled() ) ) );
// translators: %s is either 'yes' or 'no'.
WP_CLI::log( sprintf( __( 'Compatibility mode enabled?: %s', 'woocommerce' ), wc_bool_to_string( $this->synchronizer->data_sync_is_enabled() ) ) );
// translators: %d is an order count.
WP_CLI::log( sprintf( __( 'Unsynced orders: %d', 'woocommerce' ), $this->synchronizer->get_current_orders_pending_sync_count() ) );
WP_CLI::log(
sprintf(
/* translators: %d is an order count. */
__( 'Orders subject to cleanup: %d', 'woocommerce' ),
( $this->synchronizer->custom_orders_table_is_authoritative() && ! $this->synchronizer->data_sync_is_enabled() )
? $legacy_handler->count_orders_for_cleanup()
: 0
)
);
}
/**
* Displays differences for an order between the HPOS and post datastore.
*
* ## OPTIONS
*
* <id>
* :The ID of the order.
*
* [--format=<format>]
* : Render output in a particular format.
* ---
* default: table
* options:
* - table
* - csv
* - json
* - yaml
* ---
*
* ## EXAMPLES
*
* # Find differences between datastores for order 123.
* $ wp wc hpos diff 123
*
* # Find differences for order 123 and display as CSV.
* $ wp wc hpos diff 123 --format=csv
*
* @since 8.6.0
*
* @param array $args Positional arguments passed to the command.
* @param array $assoc_args Associative arguments (options) passed to the command.
*/
public function diff( array $args = array(), array $assoc_args = array() ) {
$id = absint( $args[0] );
try {
$diff = wc_get_container()->get( LegacyDataHandler::class )->get_diff_for_order( $id );
} catch ( \Exception $e ) {
// translators: %1$d is an order ID, %2$s is an error message.
WP_CLI::error( sprintf( __( 'An error occurred while computing a diff for order %1$d: %2$s', 'woocommerce' ), $id, $e->getMessage() ) );
}
if ( ! $diff ) {
WP_CLI::success( __( 'No differences found.', 'woocommerce' ) );
return;
}
// Format the diff array.
$diff = array_map(
function( $key, $hpos_value, $cpt_value ) {
// Format for dates.
$hpos_value = is_a( $hpos_value, \WC_DateTime::class ) ? $hpos_value->format( DATE_ATOM ) : $hpos_value;
$cpt_value = is_a( $cpt_value, \WC_DateTime::class ) ? $cpt_value->format( DATE_ATOM ) : $cpt_value;
return array(
'property' => $key,
'hpos' => $hpos_value,
'post' => $cpt_value,
);
},
array_keys( $diff ),
array_column( $diff, 0 ),
array_column( $diff, 1 ),
);
WP_CLI::warning(
// translators: %d is an order ID.
sprintf( __( 'Differences found for order %d:', 'woocommerce' ), $id )
);
WP_CLI\Utils\format_items(
$assoc_args['format'] ?? 'table',
$diff,
array( 'property', 'hpos', 'post' )
);
}
}

View File

@ -5,6 +5,8 @@
namespace Automattic\WooCommerce\Internal\DataStores\Orders;
use Automattic\WooCommerce\Utilities\ArrayUtil;
defined( 'ABSPATH' ) || exit;
/**
@ -195,6 +197,137 @@ class LegacyDataHandler {
return $order_modified_gmt >= $post_modified_gmt;
}
/**
* Builds an array with properties and metadata for which HPOS and post record have different values.
* Given it's mostly informative nature, it doesn't perform any deep or recursive searches and operates only on top-level properties/metadata.
*
* @since 8.6.0
*
* @param int $order_id Order ID.
* @return array Array of [HPOS value, post value] keyed by property, for all properties where HPOS and post value differ.
*/
public function get_diff_for_order( int $order_id ): array {
$diff = array();
$hpos_order = $this->get_order_from_datastore( $order_id, 'hpos' );
$cpt_order = $this->get_order_from_datastore( $order_id, 'cpt' );
if ( $hpos_order->get_type() !== $cpt_order->get_type() ) {
$diff['type'] = array( $hpos_order->get_type(), $cpt_order->get_type() );
}
$hpos_meta = $this->order_meta_to_array( $hpos_order );
$cpt_meta = $this->order_meta_to_array( $cpt_order );
// Consider only keys for which we actually have a corresponding HPOS column or are meta.
$all_keys = array_unique(
array_diff(
array_merge(
$this->get_order_base_props(),
array_keys( $hpos_meta ),
array_keys( $cpt_meta )
),
$this->data_synchronizer->get_ignored_order_props()
)
);
foreach ( $all_keys as $key ) {
$val1 = in_array( $key, $this->get_order_base_props(), true ) ? $hpos_order->{"get_$key"}() : ( $hpos_meta[ $key ] ?? null );
$val2 = in_array( $key, $this->get_order_base_props(), true ) ? $cpt_order->{"get_$key"}() : ( $cpt_meta[ $key ] ?? null );
// Workaround for https://github.com/woocommerce/woocommerce/issues/43126.
if ( ! $val2 && in_array( $key, array( '_billing_address_index', '_shipping_address_index' ), true ) ) {
$val2 = get_post_meta( $order_id, $key, true );
}
if ( $val1 != $val2 ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
$diff[ $key ] = array( $val1, $val2 );
}
}
return $diff;
}
/**
* Returns an order object as seen by either the HPOS or CPT datastores.
*
* @since 8.6.0
*
* @param int $order_id Order ID.
* @param string $data_store_id Datastore to use. Should be either 'hpos' or 'cpt'. Defaults to 'hpos'.
* @return \WC_Order Order instance.
*/
public function get_order_from_datastore( int $order_id, string $data_store_id = 'hpos' ) {
$data_store = ( 'hpos' === $data_store_id ) ? $this->data_store : $this->data_store->get_cpt_data_store_instance();
wp_cache_delete( \WC_Order::generate_meta_cache_key( $order_id, 'orders' ), 'orders' );
// Prime caches if we can.
if ( method_exists( $data_store, 'prime_caches_for_orders' ) ) {
$data_store->prime_caches_for_orders( array( $order_id ), array() );
}
$classname = wc_get_order_type( $data_store->get_order_type( $order_id ) )['class_name'];
$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.
$data_store->read( $order );
return $order;
}
/**
* Returns all metadata in an order object as an array.
*
* @param \WC_Order $order Order instance.
* @return array Array of metadata grouped by meta key.
*/
private function order_meta_to_array( \WC_Order &$order ): array {
$result = array();
foreach ( ArrayUtil::select( $order->get_meta_data(), 'get_data', ArrayUtil::SELECT_BY_OBJECT_METHOD ) as &$meta ) {
if ( array_key_exists( $meta['key'], $result ) ) {
$result[ $meta['key'] ] = array( $result[ $meta['key'] ] );
$result[ $meta['key'] ][] = $meta['value'];
} else {
$result[ $meta['key'] ] = $meta['value'];
}
}
return $result;
}
/**
* Returns names of all order base properties supported by HPOS.
*
* @return string[] Property names.
*/
private function get_order_base_props(): array {
return array_column(
call_user_func_array(
'array_merge',
array_values( $this->data_store->get_all_order_column_mappings() )
),
'name'
);
}
}