eate 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 {
$data_store->read( $order );
} finally {
remove_filter( 'woocommerce_hpos_enable_sync_on_read', $prevent_sync_on_read, 999 );
}
return $order;
}
/**
* Backfills an order from/to the CPT or HPOS datastore.
*
* @since 8.7.0
*
* @param int $order_id Order ID.
* @param string $source_data_store Datastore to use as source. Should be either 'hpos' or 'posts'.
* @param string $destination_data_store Datastore to use as destination. Should be either 'hpos' or 'posts'.
* @param array $fields List of metakeys or order properties to limit the backfill to.
* @return void
* @throws \Exception When an error occurs.
*/
public function backfill_order_to_datastore( int $order_id, string $source_data_store, string $destination_data_store, array $fields = array() ) {
$valid_data_stores = array( 'posts', 'hpos' );
if ( ! in_array( $source_data_store, $valid_data_stores, true ) || ! in_array( $destination_data_store, $valid_data_stores, true ) || $destination_data_store === $source_data_store ) {
throw new \Exception( esc_html( sprintf( 'Invalid datastore arguments: %1$s -> %2$s.', $source_data_store, $destination_data_store ) ) );
}
$fields = array_filter( $fields );
$src_order = $this->get_order_from_datastore( $order_id, $source_data_store );
// Backfill entire orders.
if ( ! $fields ) {
if ( 'posts' === $destination_data_store ) {
$src_order->get_data_store()->backfill_post_record( $src_order );
} elseif ( 'hpos' === $destination_data_store ) {
$this->posts_to_cot_migrator->migrate_orders( array( $src_order->get_id() ) );
}
return;
}
$this->validate_backfill_fields( $fields, $src_order );
$dest_order = $this->get_order_from_datastore( $src_order->get_id(), $destination_data_store );
if ( 'posts' === $destination_data_store ) {
$datastore = $this->data_store->get_cpt_data_store_instance();
} elseif ( 'hpos' === $destination_data_store ) {
$datastore = $this->data_store;
}
if ( ! $datastore || ! method_exists( $datastore, 'update_order_from_object' ) ) {
throw new \Exception( esc_html__( 'The backup datastore does not support updating orders.', 'woocommerce' ) );
}
// Backfill meta.
if ( ! empty( $fields['meta_keys'] ) ) {
foreach ( $fields['meta_keys'] as $meta_key ) {
$dest_order->delete_meta_data( $meta_key );
foreach ( $src_order->get_meta( $meta_key, false, 'edit' ) as $meta ) {
$dest_order->add_meta_data( $meta_key, $meta->value );
}
}
}
// Backfill props.
if ( ! empty( $fields['props'] ) ) {
$new_values = array_combine(
$fields['props'],
array_map(
fn( $prop_name ) => $src_order->{"get_{$prop_name}"}(),
$fields['props']
)
);
$dest_order->set_props( $new_values );
if ( 'hpos' === $destination_data_store ) {
$dest_order->apply_changes();
$limit_cb = function ( $rows, $order ) use ( $dest_order, $fields ) {
if ( $dest_order->get_id() === $order->get_id() ) {
$rows = $this->limit_hpos_update_to_props( $rows, $fields['props'] );
}
return $rows;
};
add_filter( 'woocommerce_orders_table_datastore_db_rows_for_order', $limit_cb, 10, 2 );
}
}
$datastore->update_order_from_object( $dest_order );
if ( 'hpos' === $destination_data_store && isset( $limit_cb ) ) {
remove_filter( 'woocommerce_orders_table_datastore_db_rows_for_order', $limit_cb );
}
}
/**
* 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 {
$base_props = array();
foreach ( $this->data_store->get_all_order_column_mappings() as $mapping ) {
$base_props = array_merge( $base_props, array_column( $mapping, 'name' ) );
}
return $base_props;
}
/**
* Filters a set of HPOS row updates to those matching a specific set of order properties.
* Called via the `woocommerce_orders_table_datastore_db_rows_for_order` filter in `backfill_order_to_datastore`.
*
* @param array $rows Details for the db update.
* @param string[] $props Order property names.
* @return array
* @see OrdersTableDataStore::get_db_rows_for_order()
*/
private function limit_hpos_update_to_props( array $rows, array $props ) {
// Determine HPOS columns corresponding to the props in the $props array.
$allowed_columns = array();
foreach ( $this->data_store->get_all_order_column_mappings() as &$mapping ) {
foreach ( $mapping as $column_name => &$column_data ) {
if ( ! isset( $column_data['name'] ) || ! in_array( $column_data['name'], $props, true ) ) {
continue;
}
$allowed_columns[ $column_data['name'] ] = $column_name;
}
}
foreach ( $rows as $i => &$db_update ) {
// Prevent accidental update of another prop by limiting columns to explicitly requested props.
if ( ! array_intersect_key( $db_update['data'], array_flip( $allowed_columns ) ) ) {
unset( $rows[ $i ] );
continue;
}
$allowed_column_names_with_ids = array_merge(
$allowed_columns,
array( 'id', 'order_id', 'address_type' )
);
$db_update['data'] = array_intersect_key( $db_update['data'], array_flip( $allowed_column_names_with_ids ) );
$db_update['format'] = array_intersect_key( $db_update['format'], array_flip( $allowed_column_names_with_ids ) );
}
return $rows;
}
/**
* Validates meta_keys and property names for a partial order backfill.
*
* @param array $fields An array possibly having entries with index 'meta_keys' and/or 'props',
* corresponding to an array of order meta keys and/or order properties.
* @param \WC_Abstract_Order $order The order being validated.
* @throws \Exception When a validation error occurs.
* @return void
*/
private function validate_backfill_fields( array $fields, \WC_Abstract_Order $order ) {
if ( ! $fields ) {
return;
}
if ( ! empty( $fields['meta_keys'] ) ) {
$internal_meta_keys = array_unique(
array_merge(
$this->data_store->get_internal_meta_keys(),
$this->data_store->get_cpt_data_store_instance()->get_internal_meta_keys()
)
);
$possibly_internal_keys = array_intersect( $internal_meta_keys, $fields['meta_keys'] );
if ( ! empty( $possibly_internal_keys ) ) {
throw new \Exception(
esc_html(
sprintf(
// translators: %s is a comma separated list of metakey names.
_n(
'%s is an internal meta key. Use --props to set it.',
'%s are internal meta keys. Use --props to set them.',
count( $possibly_internal_keys ),
'woocommerce'
),
implode( ', ', $possibly_internal_keys )
)
)
);
}
}
if ( ! empty( $fields['props'] ) ) {
$invalid_props = array_filter(
$fields['props'],
function ( $prop_name ) use ( $order ) {
return ! method_exists( $order, "get_{$prop_name}" );
}
);
if ( ! empty( $invalid_props ) ) {
throw new \Exception(
esc_html(
sprintf(
// translators: %s is a list of order property names.
_n(
'%s is not a valid order property.',
'%s are not valid order properties.',
count( $invalid_props ),
'woocommerce'
),
implode( ', ', $invalid_props )
)
)
);
}
}
}
}