From b9850d00fefffd4451a214a9ee82ff6a8017a894 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Tue, 10 Dec 2024 19:20:30 -0300 Subject: [PATCH 1/2] Initial exploration of compatibility with WC HPOS --- .../classes/Feature/WooCommerce/Orders.php | 40 +++++- .../Feature/WooCommerce/OrdersHPOS.php | 131 ++++++++++++++++++ .../WooCommerce/TestWooCommerceOrders.php | 34 ++++- 3 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 includes/classes/Feature/WooCommerce/OrdersHPOS.php diff --git a/includes/classes/Feature/WooCommerce/Orders.php b/includes/classes/Feature/WooCommerce/Orders.php index 8c705acdc..f6ee9f4fc 100644 --- a/includes/classes/Feature/WooCommerce/Orders.php +++ b/includes/classes/Feature/WooCommerce/Orders.php @@ -25,6 +25,13 @@ class Orders { */ protected $woocommerce; + /** + * Receive the OrdersHPOS object instance + * + * @var OrdersHPOS + */ + protected $orders_hpos; + /** * Class constructor * @@ -32,6 +39,7 @@ class Orders { */ public function __construct( WooCommerce $woocommerce ) { $this->woocommerce = $woocommerce; + $this->orders_hpos = new OrdersHPOS( $this ); } /** @@ -46,6 +54,10 @@ public function setup() { add_action( 'parse_query', [ $this, 'search_order' ], 11 ); add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 ); add_filter( 'ep_admin_notices', [ $this, 'hpos_compatibility_notice' ] ); + + if ( $this->is_hpos_enabled() ) { + $this->orders_hpos->setup(); + } } /** @@ -61,6 +73,10 @@ public function tear_down() { remove_action( 'parse_query', [ $this, 'maybe_hook_woocommerce_search_fields' ], 1 ); remove_action( 'parse_query', [ $this, 'search_order' ], 11 ); remove_action( 'pre_get_posts', [ $this, 'translate_args' ], 11 ); + + if ( $this->is_hpos_enabled() ) { + $this->orders_hpos->tear_down(); + } } /** @@ -359,13 +375,7 @@ public function hpos_compatibility_notice( array $notices ) : array { return $notices; } - if ( - ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) - || ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) { - return $notices; - } - - if ( ! \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) { + if ( ! $this->is_hpos_enabled() ) { return $notices; } @@ -480,6 +490,22 @@ public function translate_args( $query ) { $this->maybe_set_search_fields( $query ); } + /** + * Whether WooCommerce HPOS is enabled or not + * + * @since 5.3.0 + * @return boolean Whether WooCommerce HPOS is enabled or not + */ + public function is_hpos_enabled() : bool { + if ( + ! class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) + || ! method_exists( '\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled' ) ) { + return false; + } + + return \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); + } + /** * Handle calls to OrdersAutosuggest methods * diff --git a/includes/classes/Feature/WooCommerce/OrdersHPOS.php b/includes/classes/Feature/WooCommerce/OrdersHPOS.php new file mode 100644 index 000000000..8c2404c60 --- /dev/null +++ b/includes/classes/Feature/WooCommerce/OrdersHPOS.php @@ -0,0 +1,131 @@ +orders = $orders; + } + + /** + * Setup order HPOS related hooks + */ + public function setup() { + add_action( 'woocommerce_new_order', [ $this, 'sync_order' ] ); + add_action( 'woocommerce_refund_created', [ $this, 'sync_order' ] ); + add_action( 'woocommerce_update_order', [ $this, 'sync_order' ] ); + add_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'set_order_data' ], 10, 2 ); + } + + /** + * Unsetup order HPOS related hooks + */ + public function tear_down() { + remove_action( 'woocommerce_new_order', [ $this, 'sync_order' ], 10, 2 ); + remove_action( 'woocommerce_refund_created', [ $this, 'sync_order' ], 10, 2 ); + remove_action( 'woocommerce_update_order', [ $this, 'sync_order' ], 10, 2 ); + remove_filter( 'ep_post_sync_args_post_prepare_meta', [ $this, 'set_order_data' ], 10, 2 ); + } + + /** + * Add orders to the sync queue + * + * @param int $order_id Order ID. + */ + public function sync_order( $order_id ) { + Indexables::factory()->get( 'post' )->sync_manager->add_to_queue( $order_id ); + } + + /** + * Add order data to ES document args + * + * @param array $post_args Post arguments + * @param int $post_id Post ID + * @return array + */ + public function set_order_data( $post_args, $post_id ) { + if ( \Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE !== $post_args['post_type'] ) { + return $post_args; + } + + /** + * Post Indexable instance. + * + * @var \ElasticPress\Indexable\Post\Post + */ + $post_indexable = Indexables::factory()->get( 'post' ); + $order = wc_get_order( $post_id ); + $order_class = get_class( $order ); + $post_order = new $order_class(); + + $post_args['post_type'] = $order->get_type(); + $post_args['post_status'] = $order->get_status( 'edit' ); + $post_args['post_parent'] = $order->get_changes()['parent_id'] ?? $order->get_data()['parent_id'] ?? 0; + $post_args['post_date'] = gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getOffsetTimestamp() ); + $post_args['post_date_gmt'] = gmdate( 'Y-m-d H:i:s', $order->get_date_created( 'edit' )->getTimestamp() ); + $post_args['edit_date'] = true; + $post_args['post_excerpt'] = method_exists( $order, 'get_customer_note' ) ? $order->get_customer_note() : ''; + + $post_order = new $order_class(); + $post_order->set_id( $order->get_id() ); + $post_order->set_props( $order->get_data() ); + + error_log( var_export( $post_order, true ) ); + + add_filter( 'ep_prepare_meta_data', [ $this, 'prepare_meta_data' ], 10, 2 ); + $post_args['meta'] = $post_indexable->prepare_meta_types( $post_indexable->prepare_meta( $post_order ) ); + remove_filter( 'ep_prepare_meta_data', [ $this, 'prepare_meta_data' ] ); + + return $post_args; + } + + /** + * Format meta data + * + * @param array $order_meta Meta data + * @param WP_Post $order_post Order object + * @return array + */ + public function prepare_meta_data( $order_meta, $order_post ) { + $order = wc_get_order( $order_post->get_id() ); + + if ( is_null( $order->get_meta() ) ) { + return $order_meta; + } + + foreach ( $order->get_meta_data() as $meta_data ) { + $order_meta[ $meta_data->key ] = ( is_object( $meta_data->value ) && '__PHP_Incomplete_Class' === get_class( $meta_data->value ) ) + ? maybe_serialize( $meta_data->value ) + : $meta_data->value; + } + + return $order_meta; + } +} diff --git a/tests/php/features/WooCommerce/TestWooCommerceOrders.php b/tests/php/features/WooCommerce/TestWooCommerceOrders.php index 2f5a3257c..fbcd65fcc 100644 --- a/tests/php/features/WooCommerce/TestWooCommerceOrders.php +++ b/tests/php/features/WooCommerce/TestWooCommerceOrders.php @@ -348,11 +348,7 @@ public function test_hpos_compatibility_notice() { ElasticPress\Features::factory()->activate_feature( 'protected_content' ); $this->assertCount( 1, $this->orders->hpos_compatibility_notice( $notices ) ); - $option_name = \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION; - $change_value = function() { - return 'yes'; - }; - add_filter( 'pre_option_' . $option_name, $change_value ); + $this->enable_hpos(); $new_notices = $this->orders->hpos_compatibility_notice( $notices ); $this->assertCount( 2, $new_notices ); @@ -370,4 +366,32 @@ public function test_hpos_compatibility_notice() { $this->assertCount( 1, $new_notices ); $this->assertArrayNotHasKey( 'wc_orders_incompatible', $new_notices ); } + + /** + * Test the `is_hpos_enabled` method + * + * @since 5.3.0 + * @group woocommerce + * @group woocommerce-orders + */ + public function test_is_hpos_enabled() { + $this->assertFalse( $this->orders->is_hpos_enabled() ); + + $this->enable_hpos(); + + $this->assertTrue( $this->orders->is_hpos_enabled() ); + } + + /** + * Utilitary function to enable WooCommerce HPOS + * + * @since 5.3.0 + */ + protected function enable_hpos() { + $option_name = \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::CUSTOM_ORDERS_TABLE_USAGE_ENABLED_OPTION; + $change_value = function() { + return 'yes'; + }; + add_filter( 'pre_option_' . $option_name, $change_value ); + } } From de0ec89cf5265f3a5c34df4b69289c715271cbcd Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Wed, 5 Feb 2025 08:01:05 -0300 Subject: [PATCH 2/2] Index orders --- .../Feature/WooCommerce/OrdersHPOS.php | 122 ++++++++++++++---- 1 file changed, 99 insertions(+), 23 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/OrdersHPOS.php b/includes/classes/Feature/WooCommerce/OrdersHPOS.php index 8c2404c60..366c0b876 100644 --- a/includes/classes/Feature/WooCommerce/OrdersHPOS.php +++ b/includes/classes/Feature/WooCommerce/OrdersHPOS.php @@ -71,19 +71,14 @@ public function sync_order( $order_id ) { * @return array */ public function set_order_data( $post_args, $post_id ) { - if ( \Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE !== $post_args['post_type'] ) { + if ( + \Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer::PLACEHOLDER_ORDER_POST_TYPE !== $post_args['post_type'] + && ! in_array( $post_args['post_type'], $this->orders->get_supported_post_types(), true ) ) { return $post_args; } - /** - * Post Indexable instance. - * - * @var \ElasticPress\Indexable\Post\Post - */ $post_indexable = Indexables::factory()->get( 'post' ); $order = wc_get_order( $post_id ); - $order_class = get_class( $order ); - $post_order = new $order_class(); $post_args['post_type'] = $order->get_type(); $post_args['post_status'] = $order->get_status( 'edit' ); @@ -93,11 +88,7 @@ public function set_order_data( $post_args, $post_id ) { $post_args['edit_date'] = true; $post_args['post_excerpt'] = method_exists( $order, 'get_customer_note' ) ? $order->get_customer_note() : ''; - $post_order = new $order_class(); - $post_order->set_id( $order->get_id() ); - $post_order->set_props( $order->get_data() ); - - error_log( var_export( $post_order, true ) ); + $post_order = new \WP_Post( (object) $post_args ); add_filter( 'ep_prepare_meta_data', [ $this, 'prepare_meta_data' ], 10, 2 ); $post_args['meta'] = $post_indexable->prepare_meta_types( $post_indexable->prepare_meta( $post_order ) ); @@ -107,25 +98,110 @@ public function set_order_data( $post_args, $post_id ) { } /** - * Format meta data + * Get meta data from an order as it would be stored in the post_meta table. + * + * This method is a copy of WC_Order_Data_Store_CPT::update_post_meta() with some simplifications and returning data as an array, + * instead of actually storing it in the database. * * @param array $order_meta Meta data * @param WP_Post $order_post Order object * @return array */ public function prepare_meta_data( $order_meta, $order_post ) { - $order = wc_get_order( $order_post->get_id() ); - - if ( is_null( $order->get_meta() ) ) { - return $order_meta; + $data_store = new \WC_Order_Data_Store_CPT(); + $order = wc_get_order( $order_post->ID ); + + $meta_data = []; + $meta_key_to_props = [ + '_order_key' => 'order_key', + '_customer_user' => 'customer_id', + '_payment_method' => 'payment_method', + '_payment_method_title' => 'payment_method_title', + '_transaction_id' => 'transaction_id', + '_customer_ip_address' => 'customer_ip_address', + '_customer_user_agent' => 'customer_user_agent', + '_created_via' => 'created_via', + '_date_completed' => 'date_completed', + '_date_paid' => 'date_paid', + '_cart_hash' => 'cart_hash', + '_download_permissions_granted' => 'download_permissions_granted', + '_recorded_sales' => 'recorded_sales', + '_recorded_coupon_usage_counts' => 'recorded_coupon_usage_counts', + '_new_order_email_sent' => 'new_order_email_sent', + '_order_stock_reduced' => 'order_stock_reduced', + ]; + + foreach ( $meta_key_to_props as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + switch ( $prop ) { + case 'date_paid': + case 'date_completed': + $value = ! is_null( $value ) ? $value->getTimestamp() : ''; + break; + case 'download_permissions_granted': + case 'recorded_sales': + case 'recorded_coupon_usage_counts': + case 'order_stock_reduced': + if ( is_null( $value ) || '' === $value ) { + break; + } + $value = is_bool( $value ) ? wc_bool_to_string( $value ) : $value; + break; + case 'new_order_email_sent': + if ( is_null( $value ) || '' === $value ) { + break; + } + $value = is_bool( $value ) ? wc_bool_to_string( $value ) : $value; + $value = 'yes' === $value ? 'true' : 'false'; // For backward compatibility, we store as true/false in DB. + break; + } + + // We want to persist internal data store keys as 'yes' or 'no' if they are boolean to maintain compatibility. + if ( is_bool( $value ) && in_array( $prop, array_values( $data_store->get_internal_data_store_key_getters() ), true ) ) { + $value = wc_bool_to_string( $value ); + } + + $meta_data[ $meta_key ] = [ $value ]; } - foreach ( $order->get_meta_data() as $meta_data ) { - $order_meta[ $meta_data->key ] = ( is_object( $meta_data->value ) && '__PHP_Incomplete_Class' === get_class( $meta_data->value ) ) - ? maybe_serialize( $meta_data->value ) - : $meta_data->value; + $address_props = array( + 'billing' => array( + '_billing_first_name' => 'billing_first_name', + '_billing_last_name' => 'billing_last_name', + '_billing_company' => 'billing_company', + '_billing_address_1' => 'billing_address_1', + '_billing_address_2' => 'billing_address_2', + '_billing_city' => 'billing_city', + '_billing_state' => 'billing_state', + '_billing_postcode' => 'billing_postcode', + '_billing_country' => 'billing_country', + '_billing_email' => 'billing_email', + '_billing_phone' => 'billing_phone', + ), + 'shipping' => array( + '_shipping_first_name' => 'shipping_first_name', + '_shipping_last_name' => 'shipping_last_name', + '_shipping_company' => 'shipping_company', + '_shipping_address_1' => 'shipping_address_1', + '_shipping_address_2' => 'shipping_address_2', + '_shipping_city' => 'shipping_city', + '_shipping_state' => 'shipping_state', + '_shipping_postcode' => 'shipping_postcode', + '_shipping_country' => 'shipping_country', + '_shipping_phone' => 'shipping_phone', + ), + ); + + foreach ( $address_props as $props ) { + foreach ( $props as $meta_key => $prop ) { + $value = $order->{"get_$prop"}( 'edit' ); + $value = is_string( $value ) ? wp_slash( $value ) : $value; + + $meta_data[ $meta_key ] = [ $value ]; + } } - return $order_meta; + return $meta_data; } }