PluginProbe
ElasticPress / 4.7.2
ElasticPress v4.7.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 4.7.2, at includes/classes/Feature/WooCommerce/Products.php

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