PluginProbe
ElasticPress / 4.3.0
ElasticPress v4.3.0
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 / Facets / Facets.php

Facets.php in ElasticPress 4.3.0, at includes/classes/Feature/Facets/Facets.php

550 lines 14.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Facets feature
4 *
5 * @since 2.5
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\Facets;
10
11 use ElasticPress\Feature as Feature;
12 use ElasticPress\Features as Features;
13 use ElasticPress\Utils as Utils;
14 use ElasticPress\Indexables as Indexables;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * Facets feature class
22 */
23 class Facets extends Feature {
24 /**
25 * Facet types (taxonomy, meta fields, etc.)
26 *
27 * @since 4.3.0
28 * @var array
29 */
30 public $types = [];
31
32 /**
33 * Initialize feature setting it's config
34 *
35 * @since 3.0
36 */
37 public function __construct() {
38 $this->slug = 'facets';
39
40 $this->title = esc_html__( 'Facets', 'elasticpress' );
41
42 $this->summary = __( 'Add controls to your website to filter content by one or more taxonomies.', 'elasticpress' );
43
44 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#facets', 'elasticpress' );
45
46 $this->requires_install_reindex = false;
47
48 $this->default_settings = [
49 'match_type' => 'all',
50 ];
51
52 $types = [
53 'taxonomy' => __NAMESPACE__ . '\Types\Taxonomy\FacetType',
54 ];
55
56 /**
57 * Filter the Facet types available.
58 *
59 * ```
60 * add_filter(
61 * 'ep_facet_types',
62 * function ( $types ) {
63 * $types['post_type'] = '\MyPlugin\PostType';
64 * return $types;
65 * }
66 * );
67 * ```
68 *
69 * @since 4.3.0
70 * @hook ep_facet_types
71 * @param {array} $types Array of types available. Keys are slugs, values are class names.
72 * @return {array} New array of types available
73 */
74 $types = apply_filters( 'ep_facet_types', $types );
75
76 foreach ( $types as $type => $class ) {
77 if ( is_a( $class, __NAMESPACE__ . '\FacetType', true ) ) {
78 $this->types[ $type ] = new $class();
79 }
80 }
81
82 parent::__construct();
83 }
84
85 /**
86 * Setup hooks and filters for feature
87 *
88 * @since 2.5
89 */
90 public function setup() {
91 global $pagenow;
92
93 // This feature should not run while in the editor.
94 if ( in_array( $pagenow, [ 'post-new.php', 'post.php' ], true ) ) {
95 return;
96 }
97
98 foreach ( $this->types as $type => $class ) {
99 $this->types[ $type ]->setup();
100 }
101
102 add_filter( 'widget_types_to_hide_from_legacy_widget_block', [ $this, 'hide_legacy_widget' ] );
103 add_action( 'ep_valid_response', [ $this, 'get_aggs' ], 10, 4 );
104 add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
105 add_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] );
106 add_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ], 10, 1 );
107 add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
108 add_action( 'pre_get_posts', [ $this, 'facet_query' ] );
109 }
110
111 /**
112 * Dashboard facet settings
113 *
114 * @since 2.5
115 */
116 public function output_feature_box_settings() {
117 $settings = $this->get_settings();
118
119 if ( ! $settings ) {
120 $settings = [];
121 }
122
123 $settings = wp_parse_args( $settings, $this->default_settings );
124 ?>
125 <div class="field">
126 <div class="field-name status"><?php esc_html_e( 'Match Type', 'elasticpress' ); ?></div>
127 <div class="input-wrap">
128 <label><input name="settings[match_type]" type="radio" <?php checked( $settings['match_type'], 'all' ); ?> value="all"><?php echo wp_kses_post( __( 'Show any content tagged to <strong>all</strong> selected terms', 'elasticpress' ) ); ?></label><br>
129 <label><input name="settings[match_type]" type="radio" <?php checked( $settings['match_type'], 'any' ); ?> value="any"><?php echo wp_kses_post( __( 'Show all content tagged to <strong>any</strong> selected term', 'elasticpress' ) ); ?></label>
130 <p class="field-description"><?php esc_html_e( '"All" will only show content that matches all facets. "Any" will show content that matches any facet.', 'elasticpress' ); ?></p>
131 </div>
132 </div>
133 <?php
134 }
135
136 /**
137 * If we are doing `or` matches, we need to remove filters from aggs.
138 *
139 * By default, the same filters applied to the main query are applied to aggregations.
140 * If doing `or` matches, those should be removed so we get a broader set of results.
141 *
142 * @param array $args ES arguments
143 * @param array $query_args Query arguments
144 * @param WP_Query $query WP Query instance
145 * @since 2.5
146 * @return array
147 */
148 public function set_agg_filters( $args, $query_args, $query ) {
149 // Not a facetable query
150 if ( empty( $query_args['ep_facet'] ) ) {
151 return $args;
152 }
153
154 /**
155 * Filter WP query arguments that will be used to build the aggregations filter.
156 *
157 * The returned `$query_args` will be used to build the aggregations filter passing
158 * it through `Indexable\Post\Post::format_args()`.
159 *
160 * @hook ep_facet_agg_filters
161 * @since 4.3.0
162 * @param {array} $query_args Query arguments
163 * @param {array} $args ES arguments
164 * @param {array} $query WP Query instance
165 * @return {array} New facets aggregations
166 */
167 $query_args = apply_filters( 'ep_facet_agg_filters', $query_args, $args, $query );
168
169 remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
170 $facet_formatted_args = Indexables::factory()->get( 'post' )->format_args( $query_args, $query );
171 add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
172
173 $args['aggs']['terms']['filter'] = $facet_formatted_args['post_filter'];
174
175 return $args;
176 }
177
178 /**
179 * Output scripts for widget admin
180 *
181 * @param string $hook WP hook
182 * @since 2.5
183 */
184 public function admin_scripts( $hook ) {
185 if ( 'widgets.php' !== $hook ) {
186 return;
187 }
188
189 wp_enqueue_style(
190 'elasticpress-facets-admin',
191 EP_URL . 'dist/css/facets-admin-styles.min.css',
192 Utils\get_asset_info( 'facets-admin-styles', 'dependencies' ),
193 Utils\get_asset_info( 'facets-admin-styles', 'version' )
194 );
195 }
196
197 /**
198 * Output front end facets styles
199 *
200 * @since 2.5
201 */
202 public function front_scripts() {
203 wp_register_script(
204 'elasticpress-facets',
205 EP_URL . 'dist/js/facets-script.min.js',
206 Utils\get_asset_info( 'facets-script', 'dependencies' ),
207 Utils\get_asset_info( 'facets-script', 'version' ),
208 true
209 );
210
211 wp_register_style(
212 'elasticpress-facets',
213 EP_URL . 'dist/css/facets-styles.min.css',
214 Utils\get_asset_info( 'facets-styles', 'dependencies' ),
215 Utils\get_asset_info( 'facets-styles', 'version' )
216 );
217 }
218
219 /**
220 * Figure out if we can/should facet the query
221 *
222 * @param WP_Query $query WP Query
223 * @since 2.5
224 * @return bool
225 */
226 public function is_facetable( $query ) {
227
228 /**
229 * Bypass the standard checks and set a query to be facetable
230 *
231 * @hook ep_is_facetable
232 * @param {bool} $bypass Defaults to false.
233 * @param {WP_Query} $query The current WP_Query.
234 * @return {bool} true to bypass, false to ignore
235 */
236 if ( \apply_filters( 'ep_is_facetable', false, $query ) ) {
237 return true;
238 }
239
240 if ( is_admin() ) {
241 return false;
242 }
243
244 if ( defined( 'WP_CLI' ) && WP_CLI ) {
245 return false;
246 }
247
248 if ( ! $query->is_main_query() ) {
249 return false;
250 }
251
252 $ep_integrate = $query->get( 'ep_integrate', null );
253
254 if ( false === $ep_integrate ) {
255 return false;
256 }
257
258 $woocommerce = Features::factory()->get_registered_feature( 'woocommerce' );
259
260 if ( ! $woocommerce->is_active() && ( function_exists( 'is_product_category' ) && is_product_category() ) ) {
261 return false;
262 }
263
264 if ( ! $this->is_facetable_page( $query ) ) {
265 return false;
266 }
267
268 return true;
269 }
270
271 /**
272 * We enable ElasticPress facet on all archive/search queries as well as non-static home pages. There is no way to know
273 * when a facet widget is used before the main query is executed so we enable EP
274 * everywhere where a facet widget could be used.
275 *
276 * @param WP_Query $query WP Query
277 * @since 2.5
278 */
279 public function facet_query( $query ) {
280 $feature = Features::factory()->get_registered_feature( 'facets' );
281
282 if ( ! $feature->is_facetable( $query ) ) {
283 return;
284 }
285
286 /**
287 * Filter facet aggregations.
288 *
289 * This is used by facet types to add their own aggregations to the
290 * general facet.
291 *
292 * @hook ep_facet_wp_query_aggs_facet
293 * @since 4.3.0
294 * @param {array} $facets Facets aggregations
295 * @return {array} New facets aggregations
296 */
297 $facets = apply_filters( 'ep_facet_wp_query_aggs_facet', [] );
298
299 if ( empty( $facets ) ) {
300 return;
301 }
302
303 $query->set( 'ep_integrate', true );
304 $query->set( 'ep_facet', true );
305
306 $aggs = array(
307 'name' => 'terms',
308 'use-filter' => true,
309 'aggs' => $facets,
310 );
311
312 $query->set( 'aggs', $aggs );
313 }
314
315 /**
316 * Hacky. Save aggregation data for later in a global
317 *
318 * @param array $response ES response
319 * @param array $query Prepared Elasticsearch query
320 * @param array $query_args Current WP Query arguments
321 * @param mixed $query_object Could be WP_Query, WP_User_Query, etc.
322 * @since 2.5
323 */
324 public function get_aggs( $response, $query, $query_args, $query_object ) {
325 if ( empty( $query_object ) || 'WP_Query' !== get_class( $query_object ) || ! $this->is_facetable( $query_object ) ) {
326 return;
327 }
328
329 $GLOBALS['ep_facet_aggs'] = false;
330
331 if ( ! empty( $response['aggregations'] ) ) {
332 $GLOBALS['ep_facet_aggs'] = [];
333
334 if ( isset( $response['aggregations']['terms'] ) && is_array( $response['aggregations']['terms'] ) ) {
335 foreach ( $response['aggregations']['terms'] as $key => $agg ) {
336 if ( 'doc_count' === $key ) {
337 continue;
338 }
339
340 if ( ! is_array( $agg ) || empty( $agg['buckets'] ) ) {
341 continue;
342 }
343
344 $GLOBALS['ep_facet_aggs'][ $key ] = [];
345
346 foreach ( $agg['buckets'] as $bucket ) {
347 $GLOBALS['ep_facet_aggs'][ $key ][ $bucket['key'] ] = $bucket['doc_count'];
348 }
349 }
350 }
351 }
352 }
353
354 /**
355 * Get currently selected facets from query args
356 *
357 * @since 2.5
358 * @return array
359 */
360 public function get_selected() {
361 $allowed_args = $this->get_allowed_query_args();
362
363 $filters = [];
364 $filter_names = [];
365 foreach ( $this->types as $type_obj ) {
366 $filter_type = $type_obj->get_filter_type();
367
368 $filters[ $filter_type ] = [];
369 $filter_names[ $filter_type ] = $type_obj->get_filter_name();
370 }
371
372 foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification
373 $key = sanitize_key( $key );
374 if ( is_array( $value ) ) {
375 $value = array_map( 'sanitize_text_field', $value );
376 } else {
377 $value = sanitize_text_field( $value );
378 }
379
380 foreach ( $filter_names as $filter_type => $filter_name ) {
381 if ( 0 === strpos( $key, $filter_name ) ) {
382 $facet = str_replace( $filter_name, '', $key );
383
384 $filters[ $filter_type ][ $facet ] = array(
385 'terms' => array_fill_keys( array_map( 'trim', explode( ',', trim( $value, ',' ) ) ), true ),
386 );
387 }
388 }
389
390 if ( in_array( $key, $allowed_args, true ) ) {
391 $filters[ $key ] = $value;
392 }
393 }
394
395 return $filters;
396 }
397
398 /**
399 * Build query url
400 *
401 * @since 2.5
402 * @param array $filters Facet filters
403 * @return string
404 */
405 public function build_query_url( $filters ) {
406 $query_param = array();
407
408 foreach ( $this->types as $type_obj ) {
409 $filter_type = $type_obj->get_filter_type();
410
411 if ( ! empty( $filters[ $filter_type ] ) ) {
412 $type_filters = $filters[ $filter_type ];
413
414 foreach ( $type_filters as $facet => $filter ) {
415 if ( ! empty( $filter['terms'] ) ) {
416 $query_param[ $type_obj->get_filter_name() . $facet ] = implode( ',', array_keys( $filter['terms'] ) );
417 }
418 }
419 }
420 }
421
422 $feature = Features::factory()->get_registered_feature( 'facets' );
423 $allowed_args = $feature->get_allowed_query_args();
424
425 if ( ! empty( $filters ) ) {
426 foreach ( $filters as $filter => $value ) {
427 if ( ! empty( $value ) && in_array( $filter, $allowed_args, true ) ) {
428 $query_param[ $filter ] = $value;
429 }
430 }
431 }
432
433 $query_string = http_build_query( $query_param );
434
435 /**
436 * Filter facet query string
437 *
438 * @hook ep_facet_query_string
439 * @param {string} $query_string Current query string
440 * @param {array} $query_param Query parameters
441 * @return {string} New query string
442 */
443 $query_string = apply_filters( 'ep_facet_query_string', $query_string, $query_param );
444
445 $url = $_SERVER['REQUEST_URI'];
446 $pagination = strpos( $url, '/page' );
447 if ( false !== $pagination ) {
448 $url = substr( $url, 0, $pagination );
449 }
450
451 return strtok( trailingslashit( $url ), '?' ) . ( ( ! empty( $query_string ) ) ? '?' . $query_string : '' );
452 }
453
454 /**
455 * Register facet widget(s)
456 *
457 * @since 2.5, deprecated in 4.3.0
458 */
459 public function register_widgets() {
460 _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types[ \$type ]->register_widgets()" );
461 }
462
463 /**
464 * Hide the legacy widget.
465 *
466 * Hides the legacy widget in favor of the Block when the block editor
467 * is in use and the legacy widget has not been used.
468 *
469 * @since 4.3
470 * @param array $widgets An array of excluded widget-type IDs.
471 * @return array array of excluded widget-type IDs to hide.
472 */
473 public function hide_legacy_widget( $widgets ) {
474 $widgets[] = 'ep-facet';
475
476 return $widgets;
477 }
478
479 /**
480 * Output feature box long
481 *
482 * @since 2.5
483 */
484 public function output_feature_box_long() {
485 ?>
486 <p>
487 <?php
488 // translators: URL
489 echo wp_kses_post( sprintf( __( "Adds a <a href='%s'>Facet widget</a> that administrators can add to the website's sidebars (widgetized areas), so that visitors can filter applicable content and search results by one or more taxonomy terms.", 'elasticpress' ), esc_url( admin_url( 'widgets.php' ) ) ) );
490 ?>
491 </p>
492 <?php
493 }
494
495 /**
496 * Returns allowed query args for facets
497 *
498 * @return mixed|void
499 * @since 3.6.0
500 */
501 public function get_allowed_query_args() {
502 $args = array( 's', 'post_type' );
503
504 /**
505 * Filter allowed query args
506 *
507 * @hook ep_facet_allowed_query_args
508 * @since 3.6.0
509 * @param {array} $args Post types
510 * @return {array} New post types
511 */
512 return apply_filters( 'ep_facet_allowed_query_args', $args );
513 }
514
515 /**
516 * Get the facet filter name.
517 *
518 * @return string The filter name.
519 */
520 protected function get_filter_name() {
521 _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_filter_name()" );
522
523 return $this->types['taxonomy']->get_filter_name();
524 }
525
526 /**
527 * Get all taxonomies that could be selected for a facet.
528 *
529 * @since 4.2.0, deprecated in 4.3.0
530 * @return array
531 */
532 public function get_facetable_taxonomies() {
533 _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_facetable_taxonomies()" );
534
535 return $this->types['taxonomy']->get_filter_name();
536
537 }
538
539 /**
540 * Figure out if Facet widget can display on page.
541 *
542 * @param WP_Query $query WP Query
543 * @since 4.2.1
544 * @return bool
545 */
546 protected function is_facetable_page( $query ) {
547 return $query->is_home() || $query->is_search() || $query->is_tax() || $query->is_tag() || $query->is_category() || $query->is_post_type_archive();
548 }
549 }
550