PluginProbe
ElasticPress / 5.0.2
ElasticPress v5.0.2
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / WooCommerce / Products.php

Products.php in ElasticPress 5.0.2, at includes/classes/Feature/WooCommerce/Products.php

1,093 lines 34.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Products
4 *
5 * @since 4.7.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\WooCommerce;
10
11 use ElasticPress\Indexables;
12 use ElasticPress\IndexHelper;
13 use ElasticPress\Utils;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * WooCommerce Products
21 */
22 class Products {
23 /**
24 * WooCommerce feature object instance
25 *
26 * @var WooCommerce
27 */
28 protected $woocommerce;
29
30 /**
31 * Class constructor
32 *
33 * @param WooCommerce $woocommerce WooCommerce feature object instance
34 */
35 public function __construct( WooCommerce $woocommerce ) {
36 $this->woocommerce = $woocommerce;
37 }
38
39 /**
40 * Setup product related hooks
41 */
42 public function setup() {
43 add_action( 'ep_formatted_args', [ $this, 'price_filter' ], 10, 3 );
44 add_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ], 10, 2 );
45 add_filter( 'ep_sync_taxonomies', [ $this, 'sync_taxonomies' ] );
46 add_filter( 'ep_term_suggest_post_type', [ $this, 'suggest_wc_add_post_type' ] );
47 add_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] );
48 add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ], 10, 2 );
49 add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ], 10, 2 );
50 add_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ], 10, 2 );
51 add_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 );
52 add_action( 'pre_get_posts', [ $this, 'translate_args' ], 11, 1 );
53 add_filter( 'ep_facet_tax_special_slug_taxonomies', [ $this, 'add_taxonomy_attributes' ] );
54
55 // Custom product ordering
56 add_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_about_product_ordering' ] );
57 add_action( 'woocommerce_after_product_ordering', [ $this, 'action_sync_on_woocommerce_sort_single' ], 10, 2 );
58
59 // Settings for Weight results by date
60 add_action( 'ep_weight_settings_after_search', [ $this, 'add_weight_settings_search' ] );
61 add_filter( 'ep_feature_settings_schema', [ $this, 'add_weight_settings_search_schema' ], 10, 2 );
62 add_filter( 'ep_is_decaying_enabled', [ $this, 'maybe_disable_decaying' ], 10, 3 );
63 }
64
65 /**
66 * Un-setup product related hooks
67 *
68 * @since 5.0.0
69 */
70 public function tear_down() {
71 remove_action( 'ep_formatted_args', [ $this, 'price_filter' ] );
72 remove_filter( 'ep_prepare_meta_allowed_protected_keys', [ $this, 'allow_meta_keys' ] );
73 remove_filter( 'ep_sync_taxonomies', [ $this, 'sync_taxonomies' ] );
74 remove_filter( 'ep_term_suggest_post_type', [ $this, 'suggest_wc_add_post_type' ] );
75 remove_filter( 'ep_facet_include_taxonomies', [ $this, 'add_product_attributes' ] );
76 remove_filter( 'ep_weighting_fields_for_post_type', [ $this, 'add_product_attributes_to_weighting' ] );
77 remove_filter( 'ep_weighting_default_post_type_weights', [ $this, 'add_product_default_post_type_weights' ] );
78 remove_filter( 'ep_prepare_meta_data', [ $this, 'add_variations_skus_meta' ] );
79 remove_filter( 'request', [ $this, 'admin_product_list_request_query' ], 9 );
80 remove_action( 'pre_get_posts', [ $this, 'translate_args' ], 11 );
81 remove_filter( 'ep_facet_tax_special_slug_taxonomies', [ $this, 'add_taxonomy_attributes' ] );
82
83 // Custom product ordering
84 remove_action( 'ep_admin_notices', [ $this, 'maybe_display_notice_about_product_ordering' ] );
85 remove_action( 'woocommerce_after_product_ordering', [ $this, 'action_sync_on_woocommerce_sort_single' ] );
86
87 // Settings for Weight results by date
88 remove_action( 'ep_weight_settings_after_search', [ $this, 'add_weight_settings_search' ] );
89 remove_filter( 'ep_feature_settings_schema', [ $this, 'add_weight_settings_search_schema' ] );
90 remove_filter( 'ep_is_decaying_enabled', [ $this, 'maybe_disable_decaying' ] );
91 }
92
93 /**
94 * Modifies main query to allow filtering by price with WooCommerce "Filter by price" widget.
95 *
96 * @param array $args ES args
97 * @param array $query_args WP_Query args
98 * @param WP_Query $query WP_Query object
99 * @return array
100 */
101 public function price_filter( $args, $query_args, $query ) {
102 // Only can use widget on main query
103 if ( ! $query->is_main_query() ) {
104 return $args;
105 }
106
107 // Only can use widget on shop, product taxonomy, or search
108 if ( ! is_shop() && ! is_product_taxonomy() && ! is_search() ) {
109 return $args;
110 }
111
112 // phpcs:disable WordPress.Security.NonceVerification
113 if ( empty( $_GET['min_price'] ) && empty( $_GET['max_price'] ) ) {
114 return $args;
115 }
116
117 $min_price = ! empty( $_GET['min_price'] ) ? sanitize_text_field( wp_unslash( $_GET['min_price'] ) ) : null;
118 $max_price = ! empty( $_GET['max_price'] ) ? sanitize_text_field( wp_unslash( $_GET['max_price'] ) ) : null;
119 // phpcs:enable WordPress.Security.NonceVerification
120
121 if ( $query->is_search() ) {
122 /**
123 * This logic is iffy but the WC price filter widget is not intended for use with search anyway
124 */
125 $old_query = $args['query']['bool'];
126 unset( $args['query']['bool']['should'] );
127
128 if ( ! empty( $min_price ) ) {
129 $args['query']['bool']['must'][0]['range']['meta._price.long']['gte'] = $min_price;
130 }
131
132 if ( ! empty( $max_price ) ) {
133 $args['query']['bool']['must'][0]['range']['meta._price.long']['lte'] = $max_price;
134 }
135
136 $args['query']['bool']['must'][0]['range']['meta._price.long']['boost'] = 2.0;
137 $args['query']['bool']['must'][1]['bool'] = $old_query;
138 } else {
139 unset( $args['query']['match_all'] );
140
141 $args['query']['range']['meta._price.long']['gte'] = ! empty( $min_price ) ? $min_price : 0;
142
143 if ( ! empty( $min_price ) ) {
144 $args['query']['range']['meta._price.long']['gte'] = $min_price;
145 }
146
147 if ( ! empty( $max_price ) ) {
148 $args['query']['range']['meta._price.long']['lte'] = $max_price;
149 }
150
151 $args['query']['range']['meta._price.long']['boost'] = 2.0;
152 }
153
154 return $args;
155 }
156
157 /**
158 * Index WooCommerce products meta fields
159 *
160 * @param array $meta Existing post meta
161 * @param \WP_Post $post Post object.
162 * @return array
163 */
164 public function allow_meta_keys( $meta, $post ) {
165 if ( 'product' !== $post->post_type ) {
166 return $meta;
167 }
168
169 return array_unique(
170 array_merge(
171 $meta,
172 array(
173 '_thumbnail_id',
174 '_product_attributes',
175 '_wpb_vc_js_status',
176 '_swatch_type',
177 'total_sales',
178 '_downloadable',
179 '_virtual',
180 '_regular_price',
181 '_sale_price',
182 '_tax_status',
183 '_tax_class',
184 '_purchase_note',
185 '_featured',
186 '_weight',
187 '_length',
188 '_width',
189 '_height',
190 '_visibility',
191 '_sku',
192 '_sale_price_dates_from',
193 '_sale_price_dates_to',
194 '_price',
195 '_sold_individually',
196 '_manage_stock',
197 '_backorders',
198 '_stock',
199 '_upsell_ids',
200 '_crosssell_ids',
201 '_stock_status',
202 '_product_version',
203 '_product_tabs',
204 '_override_tab_layout',
205 '_suggested_price',
206 '_min_price',
207 '_variable_billing',
208 '_wc_average_rating',
209 '_product_image_gallery',
210 '_bj_lazy_load_skip_post',
211 '_min_variation_price',
212 '_max_variation_price',
213 '_min_price_variation_id',
214 '_max_price_variation_id',
215 '_min_variation_regular_price',
216 '_max_variation_regular_price',
217 '_min_regular_price_variation_id',
218 '_max_regular_price_variation_id',
219 '_min_variation_sale_price',
220 '_max_variation_sale_price',
221 '_min_sale_price_variation_id',
222 '_max_sale_price_variation_id',
223 '_default_attributes',
224 '_swatch_type_options',
225 '_variations_skus',
226 )
227 )
228 );
229 }
230
231 /**
232 * Index WooCommerce taxonomies
233 *
234 * @param array $taxonomies Index taxonomies array
235 * @return array
236 */
237 public function sync_taxonomies( $taxonomies ) {
238 $woo_taxonomies = [];
239
240 $product_type = get_taxonomy( 'product_type' );
241 if ( false !== $product_type ) {
242 $woo_taxonomies[] = $product_type;
243 }
244
245 $product_visibility = get_taxonomy( 'product_visibility' );
246 if ( false !== $product_visibility ) {
247 $woo_taxonomies[] = $product_visibility;
248 }
249
250 /**
251 * Note product_shipping_class, product_cat, and product_tag are already public. Make
252 * sure to index non-attribute taxonomies.
253 */
254 $attribute_taxonomies = wc_get_attribute_taxonomies();
255
256 if ( ! empty( $attribute_taxonomies ) ) {
257 foreach ( $attribute_taxonomies as $tax ) {
258 $name = wc_attribute_taxonomy_name( $tax->attribute_name );
259
260 if ( ! empty( $name ) ) {
261 if ( empty( $tax->attribute_ ) ) {
262 $woo_taxonomies[] = get_taxonomy( $name );
263 }
264 }
265 }
266 }
267
268 return array_merge( $taxonomies, $woo_taxonomies );
269 }
270
271 /**
272 * Add WC product post type to autosuggest
273 *
274 * @param array $post_types Array of post types (e.g. post, page)
275 * @return array
276 */
277 public function suggest_wc_add_post_type( $post_types ) {
278 if ( ! in_array( 'product', $post_types, true ) ) {
279 $post_types[] = 'product';
280 }
281
282 return $post_types;
283 }
284
285 /**
286 * Add WooCommerce Product Attributes to EP Facets.
287 *
288 * @param array $taxonomies Taxonomies array
289 * @return array
290 */
291 public function add_product_attributes( $taxonomies = [] ) {
292 $attribute_names = wc_get_attribute_taxonomy_names();
293
294 foreach ( $attribute_names as $name ) {
295 if ( ! taxonomy_exists( $name ) ) {
296 continue;
297 }
298 $taxonomies[ $name ] = get_taxonomy( $name );
299 }
300
301 return $taxonomies;
302 }
303
304 /**
305 * Add WooCommerce Fields to the Weighting Dashboard.
306 *
307 * @param array $fields Current weighting fields.
308 * @param string $post_type Current post type.
309 * @return array New fields.
310 */
311 public function add_product_attributes_to_weighting( $fields, $post_type ) {
312 if ( 'product' !== $post_type ) {
313 return $fields;
314 }
315
316 if ( ! empty( $fields['attributes']['children']['author_name'] ) ) {
317 unset( $fields['attributes']['children']['author_name'] );
318 }
319
320 $sku_key = 'meta._sku.value';
321
322 unset( $fields['ep_metadata']['children'][ $sku_key ] );
323
324 $fields['attributes']['children'][ $sku_key ] = array(
325 'key' => $sku_key,
326 'label' => __( 'SKU', 'elasticpress' ),
327 );
328
329 $variations_skus_key = 'meta._variations_skus.value';
330
331 unset( $fields['ep_metadata']['children'][ $variations_skus_key ] );
332
333 $fields['attributes']['children'][ $variations_skus_key ] = array(
334 'key' => $variations_skus_key,
335 'label' => __( 'Variations SKUs', 'elasticpress' ),
336 );
337
338 return $fields;
339 }
340
341 /**
342 * Add WooCommerce Fields to the default values of the Weighting Dashboard.
343 *
344 * @param array $defaults Default values for the post type.
345 * @param string $post_type Current post type.
346 * @return array
347 */
348 public function add_product_default_post_type_weights( $defaults, $post_type ) {
349 if ( 'product' !== $post_type ) {
350 return $defaults;
351 }
352
353 if ( ! empty( $defaults['author_name'] ) ) {
354 unset( $defaults['author_name'] );
355 }
356
357 $defaults['meta._sku.value'] = array(
358 'enabled' => true,
359 'weight' => 1,
360 );
361
362 $defaults['meta._variations_skus.value'] = array(
363 'enabled' => true,
364 'weight' => 1,
365 );
366
367 return $defaults;
368 }
369
370 /**
371 * Add a new `_variations_skus` meta field to the product to be indexed in Elasticsearch.
372 *
373 * @param array $post_meta Post meta
374 * @param WP_Post $post Post object
375 * @return array
376 */
377 public function add_variations_skus_meta( $post_meta, $post ) {
378 if ( 'product' !== $post->post_type ) {
379 return $post_meta;
380 }
381
382 $product = wc_get_product( $post );
383 if ( ! $product ) {
384 return $post_meta;
385 }
386
387 $variations_ids = $product->get_children();
388
389 $post_meta['_variations_skus'] = array_reduce(
390 $variations_ids,
391 function ( $variations_skus, $current_id ) {
392 $variation = wc_get_product( $current_id );
393 if ( ! $variation || ! $variation->exists() ) {
394 return $variations_skus;
395 }
396 $variation_sku = $variation->get_sku();
397 if ( ! $variation_sku ) {
398 return $variations_skus;
399 }
400 $variations_skus[] = $variation_sku;
401 return $variations_skus;
402 },
403 []
404 );
405
406 return $post_meta;
407 }
408
409 /**
410 * Integrate ElasticPress with the WooCommerce Admin Product List.
411 *
412 * WooCommerce uses its `WC_Admin_List_Table_Products` class to control that screen. This
413 * function adds all necessary hooks to bypass the default behavior and integrate with ElasticPress.
414 * By default, WC runs a SQL query to get the Product IDs that match the list criteria and passes
415 * that list of IDs to the main WP_Query. This integration changes that process to a single query, run
416 * by ElasticPress.
417 *
418 * @param array $query_vars Query vars.
419 * @return array
420 */
421 public function admin_product_list_request_query( $query_vars ) {
422 global $typenow, $wc_list_table;
423
424 // Return if not in the correct screen.
425 if ( ! is_a( $wc_list_table, 'WC_Admin_List_Table_Products' ) || 'product' !== $typenow ) {
426 return $query_vars;
427 }
428
429 // Return if admin WP_Query integration is not turned on, i.e., Protect Content is not enabled.
430 if ( ! has_filter( 'ep_admin_wp_query_integration', '__return_true' ) ) {
431 return $query_vars;
432 }
433
434 /**
435 * Filter to skip integration with WooCommerce Admin Product List.
436 *
437 * @hook ep_woocommerce_integrate_admin_products_list
438 * @since 4.2.0
439 * @param {bool} $integrate True to integrate, false to preserve original behavior. Defaults to true.
440 * @param {array} $query_vars Query vars.
441 * @return {bool} New integrate value
442 */
443 if ( ! apply_filters( 'ep_woocommerce_integrate_admin_products_list', true, $query_vars ) ) {
444 return $query_vars;
445 }
446
447 add_action( 'pre_get_posts', [ $this, 'translate_args_admin_products_list' ], 12 );
448
449 // This short-circuits WooCommerce search for product IDs.
450 add_filter( 'woocommerce_product_pre_search_products', '__return_empty_array' );
451
452 return $query_vars;
453 }
454
455 /**
456 * Apply the necessary changes to WP_Query in WooCommerce Admin Product List.
457 *
458 * @param WP_Query $query The WP Query being executed.
459 */
460 public function translate_args_admin_products_list( $query ) {
461 // The `translate_args()` method sets it to `true` if we should integrate it.
462 if ( ! $query->get( 'ep_integrate', false ) ) {
463 return;
464 }
465
466 // WooCommerce unsets the search term right after using it to fetch product IDs. Here we add it back.
467 $search_term = ! empty( $_GET['s'] ) ? sanitize_text_field( wp_unslash( $_GET['s'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
468 if ( ! empty( $search_term ) ) {
469 $query->set( 's', sanitize_text_field( $search_term ) ); // phpcs:ignore WordPress.Security.NonceVerification
470
471 /**
472 * Filter the fields used in WooCommerce Admin Product Search.
473 *
474 * ```
475 * add_filter(
476 * 'ep_woocommerce_admin_products_list_search_fields',
477 * function ( $wc_admin_search_fields ) {
478 * $wc_admin_search_fields['meta'][] = 'custom_field';
479 * return $wc_admin_search_fields;
480 * }
481 * );
482 * ```
483 *
484 * @hook ep_woocommerce_admin_products_list_search_fields
485 * @since 4.2.0
486 * @param {array} $wc_admin_search_fields Fields to be used in the WooCommerce Admin Product Search
487 * @return {array} New fields
488 */
489 $search_fields = apply_filters(
490 'ep_woocommerce_admin_products_list_search_fields',
491 [
492 'post_title',
493 'post_content',
494 'post_excerpt',
495 'meta' => [
496 '_sku',
497 '_variations_skus',
498 ],
499 ]
500 );
501
502 $query->set( 'search_fields', $search_fields );
503 }
504
505 // 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()`.
506 $product_type_query = $query->get( 'product_type', '' );
507 $product_type_url = ! empty( $_GET['product_type'] ) ? sanitize_text_field( wp_unslash( $_GET['product_type'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
508 $allowed_prod_types = [ 'virtual', 'downloadable' ];
509 if ( empty( $product_type_query ) && ! empty( $product_type_url ) && in_array( $product_type_url, $allowed_prod_types, true ) ) {
510 $meta_query = $query->get( 'meta_query', [] );
511 $meta_query[] = [
512 'key' => "_{$product_type_url}",
513 'value' => 'yes',
514 ];
515 $query->set( 'meta_query', $meta_query );
516 }
517
518 // Sets the meta query for `stock_status` if needed.
519 $stock_status_query = $query->get( 'stock_status', '' );
520 $stock_status_url = ! empty( $_GET['stock_status'] ) ? sanitize_text_field( wp_unslash( $_GET['stock_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
521 $allowed_stock_status = [ 'instock', 'outofstock', 'onbackorder' ];
522 if ( empty( $stock_status_query ) && ! empty( $stock_status_url ) && in_array( $stock_status_url, $allowed_stock_status, true ) ) {
523 $meta_query = $query->get( 'meta_query', [] );
524 $meta_query[] = [
525 'key' => '_stock_status',
526 'value' => $stock_status_url,
527 ];
528 $query->set( 'meta_query', $meta_query );
529 }
530 }
531
532 /**
533 * Depending on the number of products display an admin notice in the custom sort screen for WooCommerce Products
534 *
535 * @param array $notices Current ElasticPress admin notices
536 * @return array
537 */
538 public function maybe_display_notice_about_product_ordering( $notices ) {
539 global $pagenow, $wp_query;
540
541 /**
542 * Make sure we're on edit.php in admin dashboard.
543 */
544 if ( ! is_admin() || 'edit.php' !== $pagenow || empty( $wp_query->query['orderby'] ) || 'menu_order title' !== $wp_query->query['orderby'] ) {
545 return $notices;
546 }
547
548 $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page();
549 if ( $documents_per_page_sync >= $wp_query->found_posts ) {
550 return $notices;
551 }
552
553 $notices['woocommerce_custom_sort'] = [
554 'html' => sprintf(
555 /* translators: Sync Page URL */
556 __( 'Due to the number of products in the site, you will need to <a href="%s">resync</a> after applying a custom sort order.', 'elasticpress' ),
557 Utils\get_sync_url()
558 ),
559 'type' => 'warning',
560 'dismiss' => true,
561 ];
562
563 return $notices;
564 }
565
566 /**
567 * Conditionally resync products after applying a custom order.
568 *
569 * @param int $sorting_id ID of post dragged and dropped
570 * @param array $menu_orders Post IDs and their new menu_order value
571 */
572 public function action_sync_on_woocommerce_sort_single( $sorting_id, $menu_orders ) {
573 $documents_per_page_sync = IndexHelper::factory()->get_index_default_per_page();
574 if ( $documents_per_page_sync < count( $menu_orders ) ) {
575 return;
576 }
577
578 $sync_manager = Indexables::factory()->get( 'post' )->sync_manager;
579 foreach ( $menu_orders as $post_id => $order ) {
580 $sync_manager->add_to_queue( $post_id );
581 }
582 }
583
584 /**
585 * Add weight by date settings related to WooCommerce
586 *
587 * @param array $settings Current settings.
588 */
589 public function add_weight_settings_search( $settings ) {
590 ?>
591 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( $settings['decaying_enabled'], 'disabled_only_products' ); ?> value="disabled_only_products"><?php esc_html_e( 'Disabled for product only queries', 'elasticpress' ); ?></label><br>
592 <label><input name="settings[decaying_enabled]" type="radio" <?php checked( $settings['decaying_enabled'], 'disabled_includes_products' ); ?> value="disabled_includes_products"><?php esc_html_e( 'Disabled for any query that includes products', 'elasticpress' ); ?></label>
593 <?php
594 }
595
596 /**
597 * Conditionally disable decaying by date based on WooCommerce Decay settings.
598 *
599 * @param bool $is_decaying_enabled Whether decay by date is enabled or not
600 * @param array $settings Settings
601 * @param array $args WP_Query args
602 * @return bool
603 */
604 public function maybe_disable_decaying( $is_decaying_enabled, $settings, $args ) {
605 // If the decay setting isn't a WooCommerce related option, return
606 if ( ! in_array( $settings['decaying_enabled'], [ 'disabled_only_products', 'disabled_includes_products' ], true ) ) {
607 return $is_decaying_enabled;
608 }
609
610 // If the query is not dealing with products, return
611 if ( ! isset( $args['post_type'] ) || ! in_array( 'product', (array) $args['post_type'], true ) ) {
612 return $is_decaying_enabled;
613 }
614
615 $post_types = (array) $args['post_type'];
616
617 // If set to disable decay on product-only queries and have more than one post type, return
618 if ( 'disabled_only_products' === $settings['decaying_enabled'] && count( $post_types ) > 1 ) {
619 return $is_decaying_enabled;
620 }
621
622 return false;
623 }
624
625 /**
626 * Translate args to ElasticPress compat format. This is the meat of what the feature does
627 *
628 * @param \WP_Query $query WP Query
629 */
630 public function translate_args( $query ) {
631 if ( ! $this->woocommerce->should_integrate_with_query( $query ) ) {
632 return;
633 }
634
635 if ( ! $this->should_integrate_with_query( $query ) ) {
636 return;
637 }
638
639 /**
640 * Make sure filters are suppressed
641 */
642 $query->query['suppress_filters'] = false;
643 $query->set( 'suppress_filters', false );
644
645 $query->set( 'ep_integrate', true );
646
647 $this->maybe_update_tax_query( $query );
648 $this->maybe_update_post_type( $query );
649 $this->maybe_update_meta_query( $query );
650
651 $this->maybe_handle_top_rated( $query );
652
653 $this->maybe_set_search_fields( $query );
654 $this->maybe_set_orderby( $query );
655 }
656
657 /**
658 * Determines whether or not ES should be integrating with the provided query.
659 *
660 * A product-related query will be integrated if:
661 * * Is the main query OR is a search OR has `ep_integrate` set as true
662 * * Is querying a supported taxonomy like product attributes
663 * * Is querying a supported post type like `product`
664 *
665 * @param \WP_Query $query Query we might integrate with
666 * @return bool
667 */
668 public function should_integrate_with_query( \WP_Query $query ) : bool {
669 $has_ep_integrate = isset( $query->query_vars['ep_integrate'] ) && filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN );
670 $is_search = '' !== $this->woocommerce->get_search_term( $query );
671
672 if ( ! $query->is_main_query() && ! $is_search && ! $has_ep_integrate ) {
673 return false;
674 }
675
676 /**
677 * Check for taxonomies
678 */
679 $supported_taxonomies = $this->get_supported_taxonomies();
680 $tax_query = $query->get( 'tax_query', [] );
681 $taxonomies_queried = array_merge(
682 array_column( $tax_query, 'taxonomy' ),
683 array_keys( $query->query_vars )
684 );
685 if ( ! empty( array_intersect( $supported_taxonomies, $taxonomies_queried ) ) ) {
686 return true;
687 }
688
689 /**
690 * Check the post type
691 */
692 $supported_post_types = $this->get_supported_post_types( $query );
693 $post_type = $query->get( 'post_type', false );
694 if ( ! empty( $post_type ) && ( in_array( $post_type, $supported_post_types, true ) || ( is_array( $post_type ) && ! array_diff( $post_type, $supported_post_types ) ) ) ) {
695 return true;
696 }
697
698 return false;
699 }
700
701 /**
702 * Get the WooCommerce supported taxonomies (related to products.)
703 *
704 * @return array
705 */
706 public function get_supported_taxonomies() : array {
707 $supported_taxonomies = array(
708 'product_cat',
709 'product_tag',
710 'product_type',
711 'product_visibility',
712 'product_shipping_class',
713 );
714
715 // Add in any attribute taxonomies that exist
716 $attribute_taxonomies = wc_get_attribute_taxonomy_names();
717
718 $supported_taxonomies = array_merge( $supported_taxonomies, $attribute_taxonomies );
719
720 /**
721 * DEPRECATED. Filter supported custom taxonomies for WooCommerce integration.
722 *
723 * @param {array} $supported_taxonomies An array of default taxonomies.
724 * @hook ep_woocommerce_supported_taxonomies
725 * @since 2.3.0
726 * @return {array} New taxonomies
727 */
728 $supported_taxonomies = apply_filters_deprecated(
729 'ep_woocommerce_supported_taxonomies',
730 [ $supported_taxonomies ],
731 '4.7.0',
732 'ep_woocommerce_products_supported_taxonomies'
733 );
734
735 /**
736 * Filter supported custom taxonomies for WooCommerce product queries integration
737 *
738 * @param {array} $supported_taxonomies An array of default taxonomies.
739 * @hook ep_woocommerce_products_supported_taxonomies
740 * @since 4.7.0
741 * @return {array} New taxonomies
742 */
743 return apply_filters( 'ep_woocommerce_products_supported_taxonomies', $supported_taxonomies );
744 }
745
746 /**
747 * Get the WooCommerce supported post types (related to products.)
748 *
749 * @param \WP_Query $query The WP_Query object
750 * @return array
751 */
752 public function get_supported_post_types( \WP_Query $query ) : array {
753 $post_types = [ 'product_variation' ];
754
755 $is_main_post_type_archive = $query->is_main_query() && $query->is_post_type_archive( 'product' );
756 $has_ep_integrate_set_true = isset( $query->query_vars['ep_integrate'] ) && filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN );
757 if ( $is_main_post_type_archive || $has_ep_integrate_set_true ) {
758 $post_types[] = 'product';
759 }
760
761 /**
762 * DEPRECATED. Expands or contracts the post_types eligible for indexing.
763 *
764 * @hook ep_woocommerce_default_supported_post_types
765 * @since 4.4.0
766 * @param {array} $post_types Post types
767 * @return {array} New post types
768 */
769 $supported_post_types = apply_filters_deprecated(
770 'ep_woocommerce_default_supported_post_types',
771 [ $post_types ],
772 '4.7.0',
773 'ep_woocommerce_products_supported_post_types'
774 );
775
776 /**
777 * Expands or contracts the post_types related to products eligible for indexing.
778 *
779 * @hook ep_woocommerce_products_supported_post_types
780 * @since 4.7.0
781 * @param {array} $supported_post_types Post types
782 * @param {WP_Query} $query The WP_Query object
783 * @return {array} New post types
784 */
785 $supported_post_types = apply_filters( 'ep_woocommerce_products_supported_post_types', $supported_post_types, $query );
786
787 $supported_post_types = array_intersect(
788 $supported_post_types,
789 Indexables::factory()->get( 'post' )->get_indexable_post_types()
790 );
791
792 return $supported_post_types;
793 }
794
795 /**
796 * If needed, update the `'tax_query'` parameter
797 *
798 * If a supported taxonomy was added in the root of the args array,
799 * this method moves it to the `'tax_query'`
800 *
801 * @param \WP_Query $query The WP_Query object
802 */
803 protected function maybe_update_tax_query( \WP_Query $query ) {
804 $supported_taxonomies = $this->get_supported_taxonomies();
805 $tax_query = $query->get( 'tax_query', [] );
806
807 foreach ( $supported_taxonomies as $taxonomy ) {
808 $term = $query->get( $taxonomy, false );
809
810 if ( ! empty( $term ) ) {
811 $tax_query[] = array(
812 'taxonomy' => $taxonomy,
813 'field' => 'slug',
814 'terms' => (array) $term,
815 );
816 }
817 }
818
819 $query->set( 'tax_query', $tax_query );
820 }
821
822 /**
823 * Set the post_type to product if empty
824 *
825 * @param \WP_Query $query The WP_Query object
826 */
827 protected function maybe_update_post_type( \WP_Query $query ) {
828 $post_type = $query->get( 'post_type', false );
829
830 if ( empty( $post_type ) ) {
831 $query->set( 'post_type', 'product' );
832 }
833 }
834
835 /**
836 * If the `'meta_key'` or `'meta_value'` parameters were set,
837 * move them to `'meta_query'`
838 *
839 * @param \WP_Query $query The WP_Query object
840 */
841 protected function maybe_update_meta_query( \WP_Query $query ) {
842 /**
843 * Handle meta queries
844 */
845 $meta_query = $query->get( 'meta_query', [] );
846 $meta_key = $query->get( 'meta_key', false );
847 $meta_value = $query->get( 'meta_value', false );
848
849 if ( ! empty( $meta_key ) && ! empty( $meta_value ) ) {
850 $meta_query[] = array(
851 'key' => $meta_key,
852 'value' => $meta_value,
853 );
854
855 $query->set( 'meta_query', $meta_query );
856 }
857 }
858
859 /**
860 * Handle the WC Top Rated Widget
861 *
862 * @param \WP_Query $query The WP_Query object
863 * @return void
864 */
865 protected function maybe_handle_top_rated( \WP_Query $query ) {
866 if ( ! has_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) ) ) {
867 return;
868 }
869
870 remove_filter( 'posts_clauses', array( WC()->query, 'order_by_rating_post_clauses' ) );
871 $query->set( 'orderby', 'meta_value_num' );
872 $query->set( 'meta_key', '_wc_average_rating' );
873 }
874
875 /**
876 * If the query has a search term and the weighting dashboard is not
877 * available, add the needed fields
878 *
879 * @param \WP_Query $query The WP_Query
880 * @return \WP_Query
881 */
882 protected function maybe_set_search_fields( \WP_Query $query ) {
883 $search_term = $this->woocommerce->get_search_term( $query );
884 if ( empty( $search_term ) ) {
885 return $query;
886 }
887
888 $post_type = $query->get( 'post_type', false );
889 if ( 'product' !== $post_type || ! defined( 'EP_IS_NETWORK' ) || ! EP_IS_NETWORK ) {
890 return;
891 }
892
893 $search_fields = $query->get( 'search_fields', array( 'post_title', 'post_content', 'post_excerpt' ) );
894
895 // Remove author_name from this search.
896 $search_fields = $this->remove_author( $search_fields );
897
898 $search_fields['meta'] = ( ! empty( $search_fields['meta'] ) ) ? $search_fields['meta'] : [];
899 $search_fields['taxonomies'] = ( ! empty( $search_fields['taxonomies'] ) ) ? $search_fields['taxonomies'] : [];
900
901 $search_fields['meta'] = array_merge( $search_fields['meta'], array( '_sku' ) );
902 $search_fields['taxonomies'] = array_merge( $search_fields['taxonomies'], array( 'category', 'post_tag', 'product_tag', 'product_cat' ) );
903
904 $query->set( 'search_fields', $search_fields );
905 }
906
907 /**
908 * Remove the author_name from search fields.
909 *
910 * @param array $search_fields Array of search fields.
911 * @return array
912 */
913 public function remove_author( array $search_fields ) : array {
914 foreach ( $search_fields as $field_key => $field ) {
915 if ( 'author_name' === $field ) {
916 unset( $search_fields[ $field_key ] );
917 }
918 }
919
920 return $search_fields;
921 }
922
923 /**
924 * If needed, set the `'order'` and `'orderby'` parameters
925 *
926 * @param \WP_Query $query The WP_Query object
927 */
928 protected function maybe_set_orderby( \WP_Query $query ) {
929 $search_term = $this->woocommerce->get_search_term( $query );
930
931 if ( empty( $search_term ) ) {
932 /**
933 * For default sorting by popularity (total_sales) and rating
934 * WooCommerce doesn't set the orderby correctly.
935 * These lines will check the meta_key and correct the orderby based on that.
936 * And this won't run in search result and only run in main query
937 */
938 $meta_key = $query->get( 'meta_key', false );
939 if ( $meta_key && $query->is_main_query() ) {
940 switch ( $meta_key ) {
941 case 'total_sales':
942 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) );
943 $query->set( 'order', 'DESC' );
944 break;
945 case '_wc_average_rating':
946 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) );
947 $query->set( 'order', 'DESC' );
948 break;
949 }
950 }
951 }
952
953 /**
954 * Set orderby and order for price/popularity when GET param not set
955 */
956 $orderby = $query->get( 'orderby', null );
957 if ( $orderby && in_array( $orderby, [ 'price', 'popularity' ], true ) ) {
958 switch ( $orderby ) {
959 case 'price':
960 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) );
961 $query->set( 'order', $query->get( 'order' ) );
962 break;
963 case 'popularity':
964 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) );
965 $query->set( 'order', 'DESC' );
966 break;
967 }
968 }
969
970 /**
971 * Set orderby from GET param
972 * Also make sure the orderby param affects only the main query
973 */
974 if ( ! empty( $_GET['orderby'] ) && $query->is_main_query() ) { // phpcs:ignore WordPress.Security.NonceVerification
975 $orderby = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
976 switch ( $orderby ) { // phpcs:ignore WordPress.Security.NonceVerification
977 case 'popularity':
978 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'total_sales' ) );
979 $query->set( 'order', 'DESC' );
980 break;
981 case 'price':
982 $query->set( 'order', $query->get( 'order', 'ASC' ) );
983 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) );
984 break;
985 case 'price-desc':
986 $query->set( 'order', 'DESC' );
987 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_price' ) );
988 break;
989 case 'rating':
990 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_wc_average_rating' ) );
991 $query->set( 'order', 'DESC' );
992 break;
993 case 'date':
994 case 'title':
995 case 'ID':
996 $query->set( 'orderby', $this->get_orderby_meta_mapping( $orderby ) );
997 break;
998 case 'sku':
999 $query->set( 'orderby', $this->get_orderby_meta_mapping( '_sku' ) );
1000 break;
1001 default:
1002 $query->set( 'orderby', $this->get_orderby_meta_mapping( 'menu_order' ) ); // Order by menu and title.
1003 }
1004 }
1005 }
1006
1007 /**
1008 * Fetch the ES related meta mapping for orderby
1009 *
1010 * @param array $meta_key The meta key to get the mapping for.
1011 * @return string The mapped meta key.
1012 */
1013 public function get_orderby_meta_mapping( $meta_key ) : string {
1014 /**
1015 * Filter WooCommerce to Elasticsearch meta mapping
1016 *
1017 * @hook orderby_meta_mapping
1018 * @param {array} $mapping Meta mapping
1019 * @return {array} New mapping
1020 */
1021 $mapping = apply_filters(
1022 'orderby_meta_mapping',
1023 array(
1024 'ID' => 'ID',
1025 'title' => 'title date',
1026 'menu_order' => 'menu_order title date',
1027 'menu_order title' => 'menu_order title date',
1028 'total_sales' => 'meta.total_sales.double date',
1029 '_wc_average_rating' => 'meta._wc_average_rating.double date',
1030 '_price' => 'meta._price.double date',
1031 '_sku' => 'meta._sku.value.sortable date',
1032 )
1033 );
1034
1035 if ( isset( $mapping[ $meta_key ] ) ) {
1036 return $mapping[ $meta_key ];
1037 }
1038
1039 return 'date';
1040 }
1041
1042 /**
1043 * Add taxonomies that should be woocommerce attributes.
1044 *
1045 * @param array $attribute_taxonomies Attribute taxonomies.
1046 * @return array $attribute_taxonomies Attribute taxonomies.
1047 */
1048 public function add_taxonomy_attributes( array $attribute_taxonomies ) : array {
1049 $all_attr_taxonomies = wc_get_attribute_taxonomies();
1050
1051 foreach ( $all_attr_taxonomies as $attr_taxonomy ) {
1052 $attribute_taxonomies[ $attr_taxonomy->attribute_name ] = wc_attribute_taxonomy_name( $attr_taxonomy->attribute_name );
1053 }
1054 return $attribute_taxonomies;
1055 }
1056
1057 /**
1058 * Add weight by date settings related to WooCommerce
1059 *
1060 * @since 5.0.0
1061 * @param array $settings_schema Settings schema
1062 * @param string $feature_slug Feature slug
1063 * @return array New settings schema
1064 */
1065 public function add_weight_settings_search_schema( $settings_schema, $feature_slug ) {
1066 if ( 'search' !== $feature_slug ) {
1067 return $settings_schema;
1068 }
1069
1070 foreach ( $settings_schema as &$setting_schema ) {
1071 if ( 'decaying_enabled' !== $setting_schema['key'] ) {
1072 continue;
1073 }
1074
1075 $setting_schema['options'] = array_merge(
1076 $setting_schema['options'],
1077 [
1078 [
1079 'label' => __( 'Weight results by date, except for product-only queries', 'elasticpress' ),
1080 'value' => 'disabled_only_products',
1081 ],
1082 [
1083 'label' => __( 'Weight results by date, except for any query that includes products', 'elasticpress' ),
1084 'value' => 'disabled_includes_products',
1085 ],
1086 ]
1087 );
1088 }
1089
1090 return $settings_schema;
1091 }
1092 }
1093