From 22796f3ea7b4c209351f637b604c0b339a593f20 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Thu, 12 May 2022 09:10:41 -0300 Subject: [PATCH 1/5] Bypass default WooCommerce behavior in the Product Admin List View --- .../Feature/WooCommerce/WooCommerce.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/includes/classes/Feature/WooCommerce/WooCommerce.php b/includes/classes/Feature/WooCommerce/WooCommerce.php index be680dfeb3..a7b7a83517 100644 --- a/includes/classes/Feature/WooCommerce/WooCommerce.php +++ b/includes/classes/Feature/WooCommerce/WooCommerce.php @@ -805,6 +805,8 @@ public function setup() { add_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] ); add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ], 10, 2 ); add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ], 10, 2 ); + + add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 ); } /** @@ -935,6 +937,27 @@ public function keep_order_fields( $skip, $post_args ) { return $skip; } + /** + * Bypass default WooCommerce behavior in the Product Admin List View. + * + * @todo This is just a proof of concept. Should NOT be merged as is. + * + * @param array $query_vars Query vars for the main WP_Query. + * @return array + */ + public function admin_product_list_request_query( $query_vars ) { + global $typenow, $wc_list_table; + + if ( 'product' !== $typenow ) { + return $query_vars; + } + + add_filter( 'ep_is_integrated_request', '__return_true' ); + remove_filter( 'request', [ $wc_list_table, 'request_query' ] ); + + return $query_vars; + } + /** * Determines whether or not ES should be integrating with the provided query * From 623ae6bee9aa5c786cd4d79de538ffd7870aa595 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Tue, 17 May 2022 19:13:04 -0300 Subject: [PATCH 2/5] Integrate ElasticPress with WooCommerce's Product List in the Admin --- .../Feature/WooCommerce/WooCommerce.php | 132 ++++++++++++++---- 1 file changed, 108 insertions(+), 24 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/WooCommerce.php b/includes/classes/Feature/WooCommerce/WooCommerce.php index 5058149add..5247a76b76 100644 --- a/includes/classes/Feature/WooCommerce/WooCommerce.php +++ b/includes/classes/Feature/WooCommerce/WooCommerce.php @@ -518,7 +518,7 @@ public function translate_args( $query ) { $query->set( 'order', 'DESC' ); break; case 'price': - $query->set( 'order', 'ASC' ); + $query->set( 'order', $query->get( 'order', 'ASC' ) ); $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) ); break; case 'price-desc': @@ -535,6 +535,9 @@ public function translate_args( $query ) { case 'ID': $query->set( 'orderby', $this->get_orderby_meta_mapping( 'ID' ) ); break; + case 'sku': + $query->set( 'orderby', $this->get_orderby_meta_mapping( '_sku' ) ); + break; default: $query->set( 'orderby', $this->get_orderby_meta_mapping( 'menu_order' ) ); // Order by menu and title. } @@ -565,6 +568,7 @@ public function get_orderby_meta_mapping( $meta_key ) { 'total_sales' => 'meta.total_sales.double date', '_wc_average_rating' => 'meta._wc_average_rating.double date', '_price' => 'meta._price.double date', + '_sku' => 'meta._sku.value.sortable date', ) ); @@ -806,9 +810,8 @@ public function setup() { add_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] ); add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ], 10, 2 ); add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ], 10, 2 ); - - add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 ); add_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ], 10, 2 ); + add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 ); } /** @@ -939,27 +942,6 @@ public function keep_order_fields( $skip, $post_args ) { return $skip; } - /** - * Bypass default WooCommerce behavior in the Product Admin List View. - * - * @todo This is just a proof of concept. Should NOT be merged as is. - * - * @param array $query_vars Query vars for the main WP_Query. - * @return array - */ - public function admin_product_list_request_query( $query_vars ) { - global $typenow, $wc_list_table; - - if ( 'product' !== $typenow ) { - return $query_vars; - } - - add_filter( 'ep_is_integrated_request', '__return_true' ); - remove_filter( 'request', [ $wc_list_table, 'request_query' ] ); - - return $query_vars; - } - /** * Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch. * @@ -996,6 +978,108 @@ function ( $variations_skus, $current_id ) { return $post_meta; } + /** + * Integrate ElasticPress with the WooCommerce Admin Product List. + * + * WooCommerce uses its `WC_Admin_List_Table_Products` class to control that screen. This + * function adds all necessary hooks to bypass the default behavior and integrate with ElasticPress. + * By default, WC runs a SQL query to get the Product IDs that match the list criteria and passes + * that list of IDs to the main WP_Query. This integration changes that process to a single query, run + * by ElasticPress. + * + * @since 4.2.0 + * @param array $query_vars Query vars. + * @return array + */ + public function admin_product_list_request_query( $query_vars ) { + global $typenow, $wc_list_table; + + // Return if not in the correct screen. + if ( ! is_a( $wc_list_table, 'WC_Admin_List_Table_Products' ) || 'product' !== $typenow ) { + return $query_vars; + } + + // Return if admin WP_Query integration is not turned on, i.e., Protect Content is not enabled. + if ( ! has_filter( 'ep_admin_wp_query_integration', '__return_true' ) ) { + return $query_vars; + } + + /** + * Filter to skip integration with WooCommerce Admin Product List. + * + * @hook ep_woocommerce_integrate_admin_products_list + * @since 4.2.0 + * @param {bool} $integrate True to integrate, false to preserve original behavior. Defaults to true. + * @param {array} $query_vars Query vars. + * @return {bool} New integrate value + */ + if ( ! apply_filters( 'ep_woocommerce_integrate_admin_products_list', true, $query_vars ) ) { + return $query_vars; + } + + add_action( 'pre_get_posts', [ $this, 'translate_args_admin_products_list' ], 12 ); + + // This short-circuits WooCommerce search for product IDs. + add_filter( 'woocommerce_product_pre_search_products', '__return_empty_array' ); + + return $query_vars; + } + + /** + * Apply the necessary changes to WP_Query in WooCommerce Admin Product List. + * + * @param WP_Query $query The WP Query being executed. + */ + public function translate_args_admin_products_list( $query ) { + // The `translate_args()` method sets it to `true` if we should integrate it. + if ( ! $query->get( 'ep_integrate', false ) ) { + return; + } + + // WooCommerce unsets the search term right after using it to fetch product IDs. Here we add it back. + $search_term = ! empty( $_GET['s'] ) ? sanitize_text_field( $_GET['s'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification + if ( ! empty( $search_term ) ) { + $query->set( 's', sanitize_text_field( $search_term ) ); // phpcs:ignore WordPress.Security.NonceVerification + + /** + * Filter to skip integration with WooCommerce Admin Product List. + * + * @hook ep_woocommerce_admin_products_list_search_fields + * @since 4.2.0 + * @param {bool} $integrate True to integrate, false to preserve original behavior. Defaults to true. + * @param {array} $query_vars Query vars. + * @return {bool} New integrate value + */ + $search_fields = apply_filters( + 'ep_woocommerce_admin_products_list_search_fields', + [ + 'post_title', + 'post_content', + 'post_excerpt', + 'meta' => [ + '_sku', + '_variations_skus', + ], + ] + ); + + $query->set( 'search_fields', $search_fields ); + } + + // Sets the meta query for `product_type` if needed. Also removed from the WP_Query by WC in `WC_Admin_List_Table_Products::query_filters()`. + $product_type_query = $query->get( 'product_type', '' ); + $product_type_url = ! empty( $_GET['product_type'] ) ? sanitize_text_field( $_GET['product_type'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification + $allowed_prod_types = [ 'virtual', 'downloadable' ]; + if ( empty( $product_type_query ) && ! empty( $product_type_url ) && in_array( $product_type_url, $allowed_prod_types, true ) ) { + $meta_query = $query->get( 'meta_query', [] ); + $meta_query[] = [ + 'key' => "_{$product_type_url}", + 'value' => 'yes', + ]; + $query->set( 'meta_query', $meta_query ); + } + } + /** * Determines whether or not ES should be integrating with the provided query * From 44d00ebc3603e978232a79542a4cba11a191c847 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Tue, 17 May 2022 19:59:48 -0300 Subject: [PATCH 3/5] PHP Unit tests --- tests/php/features/TestWooCommerce.php | 74 ++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/php/features/TestWooCommerce.php b/tests/php/features/TestWooCommerce.php index 807e9271aa..c682344bb3 100644 --- a/tests/php/features/TestWooCommerce.php +++ b/tests/php/features/TestWooCommerce.php @@ -284,4 +284,78 @@ public function testAddVariationsSkusMeta() { $this->assertContains( 'child-sku-1', $product_meta_to_index['_variations_skus'] ); $this->assertContains( 'child-sku-2', $product_meta_to_index['_variations_skus'] ); } + + /** + * Test the translate_args_admin_products_list method + * + * @since 4.2.0 + * @group woocommerce + */ + public function testTranslateArgsAdminProductsList() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + parse_str( 'post_type=product&s=product&product_type=downloadable', $_GET ); + + $query_args = [ + 'ep_integrate' => true, + ]; + + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + add_action( 'pre_get_posts', [ $woocommerce_feature, 'translate_args_admin_products_list' ] ); + + $query = new \WP_Query( $query_args ); + + $this->assertTrue( $query->elasticsearch_success ); + $this->assertEquals( $query->query_vars['s'], 'product' ); + $this->assertEquals( $query->query_vars['meta_query'][0]['key'], '_downloadable' ); + $this->assertEquals( $query->query_vars['meta_query'][0]['value'], 'yes' ); + $this->assertEquals( + $query->query_vars['search_fields'], + [ + 'post_title', + 'post_content', + 'post_excerpt', + 'meta' => [ + '_sku', + '_variations_skus', + ], + ] + ); + } + + /** + * Test the ep_woocommerce_admin_products_list_search_fields filter + * + * @since 4.2.0 + * @group woocommerce + */ + public function testEPWoocommerceAdminProductsListSearchFields() { + ElasticPress\Features::factory()->activate_feature( 'protected_content' ); + ElasticPress\Features::factory()->activate_feature( 'woocommerce' ); + ElasticPress\Features::factory()->setup_features(); + + parse_str( 'post_type=product&s=product&product_type=downloadable', $_GET ); + + $query_args = [ + 'ep_integrate' => true, + ]; + + $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); + add_action( 'pre_get_posts', [ $woocommerce_feature, 'translate_args_admin_products_list' ] ); + + $search_fields_function = function() { + return [ 'post_title', 'post_content' ]; + }; + add_filter( 'ep_woocommerce_admin_products_list_search_fields', $search_fields_function ); + + $query = new \WP_Query( $query_args ); + $this->assertEquals( + $query->query_vars['search_fields'], + [ 'post_title', 'post_content' ] + ); + + remove_filter( 'ep_woocommerce_admin_products_list_search_fields', $search_fields_function ); + } } From 11b9cc070057c65422c3d8fcfa9b35955754bae3 Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Tue, 17 May 2022 20:07:02 -0300 Subject: [PATCH 4/5] Remove unnecessary \t --- tests/php/features/TestWooCommerce.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/php/features/TestWooCommerce.php b/tests/php/features/TestWooCommerce.php index c682344bb3..37ae7fe2dc 100644 --- a/tests/php/features/TestWooCommerce.php +++ b/tests/php/features/TestWooCommerce.php @@ -344,7 +344,7 @@ public function testEPWoocommerceAdminProductsListSearchFields() { $woocommerce_feature = ElasticPress\Features::factory()->get_registered_feature( 'woocommerce' ); add_action( 'pre_get_posts', [ $woocommerce_feature, 'translate_args_admin_products_list' ] ); - + $search_fields_function = function() { return [ 'post_title', 'post_content' ]; }; From c1f23091189c95e1010d3daafffdf4f1edb2208a Mon Sep 17 00:00:00 2001 From: Felipe Elia Date: Tue, 17 May 2022 20:14:38 -0300 Subject: [PATCH 5/5] Add orderby=title support --- includes/classes/Feature/WooCommerce/WooCommerce.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/includes/classes/Feature/WooCommerce/WooCommerce.php b/includes/classes/Feature/WooCommerce/WooCommerce.php index 5247a76b76..c33cc29faa 100644 --- a/includes/classes/Feature/WooCommerce/WooCommerce.php +++ b/includes/classes/Feature/WooCommerce/WooCommerce.php @@ -511,8 +511,8 @@ public function translate_args( $query ) { * Also make sure the orderby param affects only the main query */ if ( ! empty( $_GET['orderby'] ) && $query->is_main_query() ) { // phpcs:ignore WordPress.Security.NonceVerification - - switch ( $_GET['orderby'] ) { // phpcs:ignore WordPress.Security.NonceVerification + $orderby = sanitize_text_field( $_GET['orderby'] ); // phpcs:ignore WordPress.Security.NonceVerification + switch ( $orderby ) { // phpcs:ignore WordPress.Security.NonceVerification case 'popularity': $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) ); $query->set( 'order', 'DESC' ); @@ -530,10 +530,9 @@ public function translate_args( $query ) { $query->set( 'order', 'DESC' ); break; case 'date': - $query->set( 'orderby', $this->get_orderby_meta_mapping( 'date' ) ); - break; + case 'title': case 'ID': - $query->set( 'orderby', $this->get_orderby_meta_mapping( 'ID' ) ); + $query->set( 'orderby', $this->get_orderby_meta_mapping( $orderby ) ); break; case 'sku': $query->set( 'orderby', $this->get_orderby_meta_mapping( '_sku' ) ); @@ -563,6 +562,7 @@ public function get_orderby_meta_mapping( $meta_key ) { 'orderby_meta_mapping', array( 'ID' => 'ID', + 'title' => 'title date', 'menu_order' => 'menu_order title date', 'menu_order title' => 'menu_order title date', 'total_sales' => 'meta.total_sales.double date',