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:
parent
d0d056c60e
commit
9ce508f47d
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: add
|
||||
|
||||
Add CLI command `wp wc hpos diff` to compare an order between datastores.
|
|
@ -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' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue