PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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
← All changes | includes/classes/Feature/Facets/Facets.php +439 -231 4.2.1 → 5.3.5 View file →
@@ -7,13 +7,13 @@
7 7 */
8 8
9 9 namespace ElasticPress\Feature\Facets;
10 10
11 -use ElasticPress\Feature as Feature;
12 -use ElasticPress\Features as Features;
13 -use ElasticPress\Utils as Utils;
14 -use ElasticPress\FeatureRequirementsStatus as FeatureRequirementsStatus;
15 -use ElasticPress\Indexables as Indexables;
11 +use ElasticPress\Feature;
12 +use ElasticPress\Features;
13 +use ElasticPress\Indexables;
14 +use ElasticPress\REST;
15 +use ElasticPress\Utils;
16 16
17 17 if ( ! defined( 'ABSPATH' ) ) {
18 18 exit; // Exit if accessed directly.
19 19 }
@@ -21,16 +21,15 @@
21 21 /**
22 22 * Facets feature class
23 23 */
24 24 class Facets extends Feature {
25 -
26 25 /**
27 - * Block instance.
26 + * Facet types (taxonomy, meta fields, etc.)
28 27 *
29 - * @since 4.2.0
30 - * @var Block
28 + * @since 4.3.0
29 + * @var array
31 30 */
32 - public $block;
31 + public $types = [];
33 32
34 33 /**
35 34 * Initialize feature setting it's config
36 35 *
@@ -38,14 +37,10 @@
38 37 */
39 38 public function __construct() {
40 39 $this->slug = 'facets';
41 40
42 - $this->title = esc_html__( 'Facets', 'elasticpress' );
41 + $this->group = 'core-search';
43 42
44 - $this->summary = __( 'Add controls to your website to filter content by one or more taxonomies.', 'elasticpress' );
45 -
46 - $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#facets', 'elasticpress' );
47 -
48 43 $this->requires_install_reindex = false;
49 44
50 45 $this->default_settings = [
51 46 'match_type' => 'all',
@@ -50,64 +45,119 @@
50 45 $this->default_settings = [
51 46 'match_type' => 'all',
52 47 ];
53 48
49 + $types = [
50 + 'taxonomy' => __NAMESPACE__ . '\Types\Taxonomy\FacetType',
51 + ];
52 +
53 + if ( version_compare( get_bloginfo( 'version' ), '5.8', '>=' ) ) {
54 + $types['meta'] = __NAMESPACE__ . '\Types\Meta\FacetType';
55 + $types['meta-range'] = __NAMESPACE__ . '\Types\MetaRange\FacetType';
56 + $types['post-type'] = __NAMESPACE__ . '\Types\PostType\FacetType';
57 + $types['date'] = __NAMESPACE__ . '\Types\Date\FacetType';
58 +
59 + }
60 +
61 + /**
62 + * Filter the Facet types available.
63 + *
64 + * ```
65 + * add_filter(
66 + * 'ep_facet_types',
67 + * function ( $types ) {
68 + * $types['post_type'] = '\MyPlugin\PostType';
69 + * return $types;
70 + * }
71 + * );
72 + * ```
73 + *
74 + * @since 4.3.0
75 + * @hook ep_facet_types
76 + * @param {array} $types Array of types available. Keys are slugs, values are class names.
77 + * @return {array} New array of types available
78 + */
79 + $types = apply_filters( 'ep_facet_types', $types );
80 +
81 + foreach ( $types as $type => $class ) {
82 + if ( is_a( $class, __NAMESPACE__ . '\FacetType', true ) ) {
83 + $this->types[ $type ] = new $class();
84 + }
85 + }
86 +
54 87 parent::__construct();
55 88 }
56 89
57 90 /**
91 + * Sets i18n strings.
92 + *
93 + * @return void
94 + * @since 5.2.0
95 + */
96 + public function set_i18n_strings(): void {
97 + $this->title = esc_html__( 'Filters', 'elasticpress' );
98 +
99 + $this->summary = '<p>' .
100 + ( wp_is_block_theme()
101 + ? sprintf(
102 + /* translators: Site Editor URL */
103 + __( 'Adds <a href="%s">filter blocks</a> that administrators can add to the website’s templates and template parts, so that visitors can filter applicable content and search results by one or more taxonomy terms, metafields, and date ranges.', 'elasticpress' ),
104 + esc_url( admin_url( 'site-editor.php' ) )
105 + )
106 + : sprintf(
107 + /* translators: Widgets Edit Screen URL */
108 + __( 'Adds <a href="%s">filter widgets</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, metafields, and date ranges.', 'elasticpress' ),
109 + esc_url( admin_url( 'widgets.php' ) )
110 + )
111 + ) . '</p>';
112 +
113 + $this->docs_url = __( 'https://www.elasticpress.io/resources/articles/configuring-elasticpress-via-the-plugin-dashboard/#filters', 'elasticpress' );
114 + }
115 +
116 + /**
58 117 * Setup hooks and filters for feature
59 118 *
60 119 * @since 2.5
61 120 */
62 121 public function setup() {
63 - global $pagenow;
64 -
65 - // This feature should not run while in the editor.
66 - if ( in_array( $pagenow, [ 'post-new.php', 'post.php' ], true ) ) {
67 - return;
122 + foreach ( $this->types as $type => $class ) {
123 + $this->types[ $type ]->setup();
68 124 }
69 125
70 - add_action( 'widgets_init', [ $this, 'register_widgets' ] );
126 + add_filter( 'widget_types_to_hide_from_legacy_widget_block', [ $this, 'hide_legacy_widget' ] );
71 127 add_action( 'ep_valid_response', [ $this, 'get_aggs' ], 10, 4 );
128 + add_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] );
129 + add_action( 'enqueue_block_editor_assets', [ $this, 'front_scripts' ] );
130 + add_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ], 10, 1 );
72 131 add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
73 132 add_action( 'pre_get_posts', [ $this, 'facet_query' ] );
74 - add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] );
75 - add_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] );
76 - add_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ], 10, 1 );
77 -
78 - $this->block = new Block();
79 - $this->block->setup();
133 + add_filter( 'ep_post_filters', [ $this, 'apply_facets_filters' ], 10, 3 );
134 + add_action( 'rest_api_init', [ $this, 'setup_endpoints' ] );
80 135 }
81 136
82 137 /**
83 - * Dashboard facet settings
138 + * Unsetup Facets related hooks
84 139 *
85 - * @since 2.5
140 + * @since 5.1.0
86 141 */
87 - public function output_feature_box_settings() {
88 - $settings = $this->get_settings();
89 -
90 - if ( ! $settings ) {
91 - $settings = [];
92 - }
93 -
94 - $settings = wp_parse_args( $settings, $this->default_settings );
95 - ?>
96 - <div class="field">
97 - <div class="field-name status"><?php esc_html_e( 'Match Type', 'elasticpress' ); ?></div>
98 - <div class="input-wrap">
99 - <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>
100 - <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>
101 - <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>
102 - </div>
103 - </div>
104 - <?php
142 + public function tear_down() {
143 + remove_filter( 'widget_types_to_hide_from_legacy_widget_block', [ $this, 'hide_legacy_widget' ] );
144 + remove_action( 'ep_valid_response', [ $this, 'get_aggs' ] );
145 + remove_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] );
146 + remove_action( 'enqueue_block_editor_assets', [ $this, 'front_scripts' ] );
147 + remove_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ] );
148 + remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ] );
149 + remove_action( 'pre_get_posts', [ $this, 'facet_query' ] );
150 + remove_filter( 'ep_post_filters', [ $this, 'apply_facets_filters' ] );
151 + remove_action( 'rest_api_init', [ $this, 'setup_endpoints' ] );
105 152 }
106 153
107 154 /**
108 - * If we are doing or matches, we need to remove filters from aggs
155 + * If we are doing `or` matches, we need to remove filters from aggs.
109 156 *
157 + * By default, the same filters applied to the main query are applied to aggregations.
158 + * If doing `or` matches, those should be removed so we get a broader set of results.
159 + *
110 160 * @param array $args ES arguments
111 161 * @param array $query_args Query arguments
112 162 * @param WP_Query $query WP Query instance
113 163 * @since 2.5
@@ -113,49 +163,45 @@
113 163 * @since 2.5
114 164 * @return array
115 165 */
116 166 public function set_agg_filters( $args, $query_args, $query ) {
167 + // Not a facetable query
117 168 if ( empty( $query_args['ep_facet'] ) ) {
118 169 return $args;
119 170 }
120 171
121 - // @todo For some reason these are appearing in the query args, need to investigate
122 - unset( $query_args['category_name'] );
123 - unset( $query_args['cat'] );
124 - unset( $query_args['tag'] );
125 - unset( $query_args['tag_id'] );
126 - unset( $query_args['taxonomy'] );
127 - unset( $query_args['term'] );
172 + if ( 'any' === $this->get_match_type() ) {
173 + add_filter( 'ep_post_filters', [ $this, 'remove_facets_filter' ], 11 );
174 + }
128 175
129 - $facet_query_args = $query_args;
176 + /**
177 + * This flag is used to differentiate filters being applied to the query and to its aggregations.
178 + */
179 + $query_args['ep_facet_adding_agg_filters'] = true;
130 180
131 - $settings = $this->get_settings();
181 + /**
182 + * Filter WP query arguments that will be used to build the aggregations filter.
183 + *
184 + * The returned `$query_args` will be used to build the aggregations filter passing
185 + * it through `Indexable\Post\Post::format_args()`.
186 + *
187 + * @hook ep_facet_agg_filters
188 + * @since 4.3.0
189 + * @param {array} $query_args Query arguments
190 + * @param {array} $args ES arguments
191 + * @param {array} $query WP Query instance
192 + * @return {array} New facets aggregations
193 + */
194 + $query_args = apply_filters( 'ep_facet_agg_filters', $query_args, $args, $query );
132 195
133 - $settings = wp_parse_args(
134 - $settings,
135 - array(
136 - 'match_type' => 'all',
137 - )
138 - );
196 + remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
197 + $facet_formatted_args = Indexables::factory()->get( 'post' )->format_args( $query_args, $query );
198 + add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
139 199
140 - if ( ! empty( $facet_query_args['tax_query'] ) ) {
141 - remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
200 + remove_filter( 'ep_post_filters', [ $this, 'remove_facets_filter' ], 11 );
142 201
143 - foreach ( $facet_query_args['tax_query'] as $key => $taxonomy ) {
144 - if ( is_array( $taxonomy ) ) {
145 - if ( 'any' === $settings['match_type'] ) {
146 - unset( $facet_query_args['tax_query'][ $key ] );
147 - }
148 - }
149 - }
202 + $args['aggs']['terms']['filter'] = $facet_formatted_args['post_filter'];
150 203
151 - $facet_formatted_args = Indexables::factory()->get( 'post' )->format_args( $facet_query_args, $query );
152 -
153 - $args['aggs']['terms']['filter'] = $facet_formatted_args['post_filter'];
154 -
155 - add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 );
156 - }
157 -
158 204 return $args;
159 205 }
160 206
161 207 /**
@@ -164,17 +210,12 @@
164 210 * @param string $hook WP hook
165 211 * @since 2.5
166 212 */
167 213 public function admin_scripts( $hook ) {
168 - if ( 'widgets.php' !== $hook ) {
169 - return;
170 - }
171 -
172 - wp_enqueue_style(
173 - 'elasticpress-facets-admin',
174 - EP_URL . 'dist/css/facets-admin-styles.min.css',
175 - Utils\get_asset_info( 'facets-admin-styles', 'dependencies' ),
176 - Utils\get_asset_info( 'facets-admin-styles', 'version' )
214 + _doing_it_wrong(
215 + __METHOD__,
216 + esc_html__( 'Facets no longer require admin styles.', 'elasticpress' ),
217 + '4.7.0'
177 218 );
178 219 }
179 220
180 221 /**
@@ -184,17 +225,19 @@
184 225 */
185 226 public function front_scripts() {
186 227 wp_register_script(
187 228 'elasticpress-facets',
188 - EP_URL . 'dist/js/facets-script.min.js',
229 + EP_URL . 'dist/js/facets-script.js',
189 230 Utils\get_asset_info( 'facets-script', 'dependencies' ),
190 231 Utils\get_asset_info( 'facets-script', 'version' ),
191 232 true
192 233 );
193 234
235 + wp_set_script_translations( 'elasticpress-facets', 'elasticpress' );
236 +
194 237 wp_register_style(
195 238 'elasticpress-facets',
196 - EP_URL . 'dist/css/facets-styles.min.css',
239 + EP_URL . 'dist/css/facets-styles.css',
197 240 Utils\get_asset_info( 'facets-styles', 'dependencies' ),
198 241 Utils\get_asset_info( 'facets-styles', 'version' )
199 242 );
200 243 }
@@ -208,20 +251,32 @@
208 251 */
209 252 public function is_facetable( $query ) {
210 253
211 254 /**
212 - * Bypass the standard checks and set a query to be facetable
255 + * Bypass the standard checks and set a query to be facetable.
213 256 *
257 + * @deprecated 5.3.0 Use the 'ep_is_facetable' argument in WP_Query instead.
258 + *
214 259 * @hook ep_is_facetable
215 260 * @param {bool} $bypass Defaults to false.
216 261 * @param {WP_Query} $query The current WP_Query.
217 262 * @return {bool} true to bypass, false to ignore
218 263 */
219 - if ( \apply_filters( 'ep_is_facetable', false, $query ) ) {
264 + if ( apply_filters_deprecated(
265 + 'ep_is_facetable',
266 + [ false, $query ],
267 + 'ElasticPress 5.3.0',
268 + 'WP_Query->ep_is_facetable argument'
269 + ) ) {
220 270 return true;
221 271 }
222 272
223 - if ( is_admin() ) {
273 + $ep_is_facetable = $query->get( 'ep_is_facetable', null );
274 + if ( null !== $ep_is_facetable ) {
275 + return (bool) $ep_is_facetable;
276 + }
277 +
278 + if ( is_admin() || is_feed() ) {
224 279 return false;
225 280 }
226 281
227 282 if ( defined( 'WP_CLI' ) && WP_CLI ) {
@@ -263,20 +318,28 @@
263 318 if ( ! $this->is_facetable( $query ) ) {
264 319 return;
265 320 }
266 321
267 - $taxonomies = get_taxonomies( array( 'public' => true ), 'object' );
322 + // If any filter was selected, there is no reason to prepend the list with sticky posts.
323 + $selected_filters = $this->get_selected();
324 + if ( ! empty( array_filter( $selected_filters ) ) ) {
325 + $query->set( 'ignore_sticky_posts', true );
326 + }
268 327
269 328 /**
270 - * Filter taxonomies made available for faceting
329 + * Filter facet aggregations.
271 330 *
272 - * @hook ep_facet_include_taxonomies
273 - * @param {array} $taxonomies Taxonomies
274 - * @return {array} New taxonomies
331 + * This is used by facet types to add their own aggregations to the
332 + * general facet.
333 + *
334 + * @hook ep_facet_wp_query_aggs_facet
335 + * @since 4.3.0
336 + * @param {array} $facets Facets aggregations
337 + * @return {array} New facets aggregations
275 338 */
276 - $taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies );
339 + $facets = apply_filters( 'ep_facet_wp_query_aggs_facet', [] );
277 340
278 - if ( empty( $taxonomies ) ) {
341 + if ( empty( $facets ) ) {
279 342 return;
280 343 }
281 344
282 345 $query->set( 'ep_integrate', true );
@@ -281,37 +344,8 @@
281 344
282 345 $query->set( 'ep_integrate', true );
283 346 $query->set( 'ep_facet', true );
284 347
285 - $facets = [];
286 -
287 - /**
288 - * Retrieve aggregations based on a custom field. This field must exist on the mapping.
289 - * Values available out-of-the-box are:
290 - * - slug (default)
291 - * - term_id
292 - * - name
293 - * - parent
294 - * - term_taxonomy_id
295 - * - term_order
296 - * - facet (retrieves a JSON representation of the term object)
297 - *
298 - * @since 3.6.0
299 - * @hook ep_facet_use_field
300 - * @param {string} $field The term field to use
301 - * @return {string} The chosen term field
302 - */
303 - $facet_field = apply_filters( 'ep_facet_use_field', 'slug' );
304 -
305 - foreach ( $taxonomies as $slug => $taxonomy ) {
306 - $facets[ $slug ] = array(
307 - 'terms' => array(
308 - 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ),
309 - 'field' => 'terms.' . $slug . '.' . $facet_field,
310 - ),
311 - );
312 - }
313 -
314 348 $aggs = array(
315 349 'name' => 'terms',
316 350 'use-filter' => true,
317 351 'aggs' => $facets,
@@ -317,50 +351,12 @@
317 351 'aggs' => $facets,
318 352 );
319 353
320 354 $query->set( 'aggs', $aggs );
321 -
322 - $selected_filters = $this->get_selected();
323 -
324 - $settings = $this->get_settings();
325 -
326 - $settings = wp_parse_args(
327 - $settings,
328 - array(
329 - 'match_type' => 'all',
330 - )
331 - );
332 -
333 - $tax_query = $query->get( 'tax_query', [] );
334 -
335 - // Account for taxonomies that should be woocommerce attributes, if WC is enabled
336 - $attribute_taxonomies = [];
337 - if ( function_exists( 'wc_attribute_taxonomy_name' ) ) {
338 - $all_attr_taxonomies = wc_get_attribute_taxonomies();
339 -
340 - foreach ( $all_attr_taxonomies as $attr_taxonomy ) {
341 - $attribute_taxonomies[ $attr_taxonomy->attribute_name ] = wc_attribute_taxonomy_name( $attr_taxonomy->attribute_name );
342 - }
343 - }
344 -
345 - foreach ( $selected_filters['taxonomies'] as $taxonomy => $filter ) {
346 - $tax_query[] = [
347 - 'taxonomy' => isset( $attribute_taxonomies[ $taxonomy ] ) ? $attribute_taxonomies[ $taxonomy ] : $taxonomy,
348 - 'field' => 'slug',
349 - 'terms' => array_keys( $filter['terms'] ),
350 - 'operator' => ( 'any' === $settings['match_type'] ) ? 'or' : 'and',
351 - ];
352 - }
353 -
354 - if ( ! empty( $selected_filters['taxonomies'] ) && 'any' === $settings['match_type'] ) {
355 - $tax_query['relation'] = 'or';
356 - }
357 -
358 - $query->set( 'tax_query', $tax_query );
359 355 }
360 356
361 357 /**
362 - * Hacky. Save aggregation data for later in a global
358 + * Get aggregations from Elasticsearch response and store on query object
363 359 *
364 360 * @param array $response ES response
365 361 * @param array $query Prepared Elasticsearch query
366 362 * @param array $query_args Current WP Query arguments
@@ -367,16 +363,14 @@
367 363 * @param mixed $query_object Could be WP_Query, WP_User_Query, etc.
368 364 * @since 2.5
369 365 */
370 366 public function get_aggs( $response, $query, $query_args, $query_object ) {
371 - if ( empty( $query_object ) || 'WP_Query' !== get_class( $query_object ) || ! $this->is_facetable( $query_object ) ) {
367 + if ( empty( $query_object ) || ! $query_object instanceof \WP_Query || ! $this->is_facetable( $query_object ) ) {
372 368 return;
373 369 }
374 370
375 - $GLOBALS['ep_facet_aggs'] = false;
376 -
377 371 if ( ! empty( $response['aggregations'] ) ) {
378 - $GLOBALS['ep_facet_aggs'] = [];
372 + $processed_aggs = [];
379 373
380 374 if ( isset( $response['aggregations']['terms'] ) && is_array( $response['aggregations']['terms'] ) ) {
381 375 foreach ( $response['aggregations']['terms'] as $key => $agg ) {
382 376 if ( 'doc_count' === $key ) {
@@ -382,23 +376,105 @@
382 376 if ( 'doc_count' === $key ) {
383 377 continue;
384 378 }
385 379
386 - if ( ! is_array( $agg ) || empty( $agg['buckets'] ) ) {
380 + if ( ! is_array( $agg ) || ( empty( $agg['buckets'] ) && empty( $agg['value'] ) ) ) {
387 381 continue;
388 382 }
389 383
390 - $GLOBALS['ep_facet_aggs'][ $key ] = [];
384 + $processed_aggs[ $key ] = [];
391 385
386 + if ( ! empty( $agg['value'] ) ) {
387 + $processed_aggs[ $key ] = $agg['value'];
388 + continue;
389 + }
390 +
392 391 foreach ( $agg['buckets'] as $bucket ) {
393 - $GLOBALS['ep_facet_aggs'][ $key ][ $bucket['key'] ] = $bucket['doc_count'];
392 + $processed_aggs[ $key ][ $bucket['key'] ] = $bucket['doc_count'];
394 393 }
395 394 }
396 395 }
396 +
397 + $this->set_query_aggregations( $query_object, $processed_aggs );
397 398 }
398 399 }
399 400
400 401 /**
402 + * Set aggregation data for a query.
403 + *
404 + * @since 5.3.0
405 + * @param \WP_Query $query The WP_Query object to store data for
406 + * @param array $aggregations The aggregation data to store
407 + */
408 + public function set_query_aggregations( $query, $aggregations ): void {
409 + // Store aggregations on the query object
410 + $query->ep_aggregations = $aggregations;
411 +
412 + if ( $query->is_main_query() && $this->should_sync_to_global( $query ) ) {
413 + _doing_it_wrong(
414 + __METHOD__,
415 + esc_html__( 'The global variable $GLOBALS[\'ep_facet_aggs\'] is deprecated. Access aggregation data directly from the query object using $query->ep_aggregations or the Facets feature methods get_query_aggregations() and get_facet_aggregation().', 'elasticpress' ),
416 + 'ElasticPress 5.3.0'
417 + );
418 +
419 + $GLOBALS['ep_facet_aggs'] = $aggregations;
420 + }
421 + }
422 +
423 + /**
424 + * Get aggregation data for a specific query
425 + *
426 + * @since 5.3.0
427 + * @param \WP_Query $query The WP_Query object
428 + * @return array|false Aggregation data or false if not found
429 + */
430 + public function get_query_aggregations( $query ) {
431 + if ( $query instanceof \WP_Query && isset( $query->ep_aggregations ) && false !== $query->ep_aggregations ) {
432 + return $query->ep_aggregations;
433 + }
434 + // Fallback to global variable
435 + return $GLOBALS['ep_facet_aggs'] ?? false;
436 + }
437 +
438 + /**
439 + * Get aggregation data for a specific facet within a query
440 + *
441 + * @since 5.3.0
442 + * @param \WP_Query $query The WP_Query object
443 + * @param string $facet_name The name of the facet to retrieve
444 + * @return array|false Facet aggregation data or false if not found
445 + */
446 + public function get_facet_aggregation( $query, $facet_name ) {
447 + $aggregations = $this->get_query_aggregations( $query );
448 +
449 + if ( false === $aggregations || ! isset( $aggregations[ $facet_name ] ) ) {
450 + return false;
451 + }
452 +
453 + return $aggregations[ $facet_name ];
454 + }
455 +
456 + /**
457 + * Determine if aggregations should be synced to global variable
458 + *
459 + * @since 5.3.0
460 + * @param \WP_Query $query The query to evaluate
461 + * @return bool True if should sync to global, false otherwise
462 + */
463 + protected function should_sync_to_global( $query ) {
464 + /**
465 + * Filter whether to sync query aggregations to global variable
466 + *
467 + * @since 5.3.0
468 + * @hook ep_facet_sync_aggregations_to_global
469 + * @param {bool} $sync Whether to sync (default: false)
470 + * @param {WP_Query} $query The query object
471 + * @return {bool} Whether to sync aggregations to global variable
472 + */
473 + return apply_filters( 'ep_facet_sync_aggregations_to_global', false, $query );
474 + }
475 +
476 + /**
401 477 * Get currently selected facets from query args
402 478 *
403 479 * @since 2.5
404 480 * @return array
@@ -403,22 +479,28 @@
403 479 * @since 2.5
404 480 * @return array
405 481 */
406 482 public function get_selected() {
407 - $filters = array(
408 - 'taxonomies' => [],
409 - );
483 + $allowed_args = $this->get_allowed_query_args();
410 484
411 - $allowed_args = $this->get_allowed_query_args();
412 - $filter_name = $this->get_filter_name();
485 + $filters = [];
486 + $filter_names = [];
487 + foreach ( $this->types as $type_obj ) {
488 + $filter_names[ $type_obj->get_filter_name() ] = $type_obj;
489 + }
413 490
414 491 foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification
415 - if ( 0 === strpos( $key, $filter_name ) ) {
416 - $taxonomy = str_replace( $filter_name, '', $key );
492 + $key = sanitize_key( $key );
417 493
418 - $filters['taxonomies'][ $taxonomy ] = array(
419 - 'terms' => array_fill_keys( array_map( 'trim', explode( ',', trim( $value, ',' ) ) ), true ),
420 - );
494 + foreach ( $filter_names as $filter_name => $type_obj ) {
495 + if ( 0 === strpos( $key, $filter_name ) ) {
496 + if ( empty( $value ) ) {
497 + continue;
498 + }
499 + $facet = str_replace( $filter_name, '', $key );
500 +
501 + $filters = $type_obj->format_selected( $facet, $value, $filters );
502 + }
421 503 }
422 504
423 505 if ( in_array( $key, $allowed_args, true ) ) {
424 506 $filters[ $key ] = $value;
@@ -424,9 +506,17 @@
424 506 $filters[ $key ] = $value;
425 507 }
426 508 }
427 509
428 - return $filters;
510 + /**
511 + * Filter selected filters.
512 + *
513 + * @hook ep_facet_selected_filters
514 + * @since 5.1.4
515 + * @param {array} $filters Current filters
516 + * @return {array} New filters
517 + */
518 + return apply_filters( 'ep_facet_selected_filters', $filters );
429 519 }
430 520
431 521 /**
432 522 * Build query url
@@ -435,31 +525,29 @@
435 525 * @param array $filters Facet filters
436 526 * @return string
437 527 */
438 528 public function build_query_url( $filters ) {
439 - $query_param = array();
529 + $query_params = array();
440 530
441 - if ( ! empty( $filters['taxonomies'] ) ) {
442 - $tax_filters = $filters['taxonomies'];
443 -
444 - foreach ( $tax_filters as $taxonomy => $filter ) {
445 - if ( ! empty( $filter['terms'] ) ) {
446 - $query_param[ $this->get_filter_name() . $taxonomy ] = implode( ',', array_keys( $filter['terms'] ) );
447 - }
531 + foreach ( $this->types as $type_obj ) {
532 + if ( empty( $filters[ $type_obj->get_filter_type() ] ) ) {
533 + continue;
448 534 }
535 + $query_params = $type_obj->add_query_params( $query_params, $filters );
449 536 }
450 537
451 - $allowed_args = $this->get_allowed_query_args();
538 + $feature = Features::factory()->get_registered_feature( 'facets' );
539 + $allowed_args = $feature->get_allowed_query_args();
452 540
453 541 if ( ! empty( $filters ) ) {
454 542 foreach ( $filters as $filter => $value ) {
455 - if ( ! empty( $value ) && in_array( $filter, $allowed_args, true ) ) {
456 - $query_param[ $filter ] = $value;
543 + if ( in_array( $filter, $allowed_args, true ) ) {
544 + $query_params[ $filter ] = $value;
457 545 }
458 546 }
459 547 }
460 548
461 - $query_string = http_build_query( $query_param );
549 + $query_string = build_query( $query_params );
462 550
463 551 /**
464 552 * Filter facet query string
465 553 *
@@ -464,14 +552,14 @@
464 552 * Filter facet query string
465 553 *
466 554 * @hook ep_facet_query_string
467 555 * @param {string} $query_string Current query string
468 - * @param {array} $query_param Query parameters
556 + * @param {array} $query_params Query parameters
469 557 * @return {string} New query string
470 558 */
471 - $query_string = apply_filters( 'ep_facet_query_string', $query_string, $query_param );
559 + $query_string = apply_filters( 'ep_facet_query_string', $query_string, $query_params );
472 560
473 - $url = $_SERVER['REQUEST_URI'];
561 + $url = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
474 562 $pagination = strpos( $url, '/page' );
475 563 if ( false !== $pagination ) {
476 564 $url = substr( $url, 0, $pagination );
477 565 }
@@ -481,28 +569,31 @@
481 569
482 570 /**
483 571 * Register facet widget(s)
484 572 *
485 - * @since 2.5
573 + * @since 2.5, deprecated in 4.3.0
486 574 */
487 575 public function register_widgets() {
488 - register_widget( __NAMESPACE__ . '\Widget' );
576 + _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types[ \$type ]->register_widgets()" );
489 577 }
490 578
491 579 /**
492 - * Output feature box long
580 + * Hide the legacy widget.
493 581 *
494 - * @since 2.5
582 + * Hides the legacy widget in favor of the Block when the block editor
583 + * is in use and the legacy widget has not been used.
584 + *
585 + * @since 4.3
586 + * @param array $widgets An array of excluded widget-type IDs.
587 + * @return array array of excluded widget-type IDs to hide.
495 588 */
496 - public function output_feature_box_long() {
497 - ?>
498 - <p>
499 - <?php
500 - // translators: URL
501 - 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' ) ) ) );
502 - ?>
503 - </p>
504 - <?php
589 + public function hide_legacy_widget( $widgets ) {
590 + $widgets[] = 'ep-facet';
591 + $widgets[] = 'ep-facet-date';
592 + $widgets[] = 'ep-facet-meta';
593 + $widgets[] = 'ep-facet-meta-range';
594 +
595 + return $widgets;
505 596 }
506 597
507 598 /**
508 599 * Returns allowed query args for facets
@@ -510,11 +601,26 @@
510 601 * @return mixed|void
511 602 * @since 3.6.0
512 603 */
513 604 public function get_allowed_query_args() {
514 - $args = array( 's', 'post_type' );
605 + $args = array( 's', 'post_type', 'orderby' );
515 606
607 + // Retrieve all registered query variables for public taxonomies
608 + $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' );
609 + foreach ( $taxonomies as $taxonomy ) {
610 + if ( $taxonomy->query_var ) {
611 + $args[] = $taxonomy->query_var;
612 + }
613 + }
516 614 /**
615 + * To keep backward compatibility, WordPress uses `'cat'` for default categories.
616 + * It also allows access using the `?taxonomy=<tax>&term=<term>` format.
617 + *
618 + * @see get_term_link()
619 + */
620 + $args = array_merge( $args, [ 'cat', 'taxonomy', 'term' ] );
621 +
622 + /**
517 623 * Filter allowed query args
518 624 *
519 625 * @hook ep_facet_allowed_query_args
520 626 * @since 3.6.0
@@ -529,38 +635,124 @@
529 635 *
530 636 * @return string The filter name.
531 637 */
532 638 protected function get_filter_name() {
639 + _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_filter_name()" );
640 +
641 + return $this->types['taxonomy']->get_filter_name();
642 + }
643 +
644 + /**
645 + * Get all taxonomies that could be selected for a facet.
646 + *
647 + * @since 4.2.0, deprecated in 4.3.0
648 + * @return array
649 + */
650 + public function get_facetable_taxonomies() {
651 + _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_facetable_taxonomies()" );
652 +
653 + return $this->types['taxonomy']->get_filter_name();
654 + }
655 +
656 + /**
657 + * Add a new filter to the ES query with selected facets
658 + *
659 + * @since 4.4.0
660 + * @param array $filters Current filters
661 + * @param array $args WP Query args
662 + * @param WP_Query $query WP Query object
663 + * @return array
664 + */
665 + public function apply_facets_filters( $filters, $args, $query ) {
666 + if ( ! $this->is_facetable( $query ) ) {
667 + return $filters;
668 + }
669 +
533 670 /**
534 - * Filter the facet filter name that's added to the URL
671 + * Filter facet selection filters to be applied to the ES query
535 672 *
536 - * @hook ep_facet_filter_name
537 - * @since 4.0.0
538 - * @param {string} Facet filter name
539 - * @return {string} New facet filter name
673 + * @hook ep_facet_query_filters
674 + * @since 4.4.0
675 + * @param {array} $filters Current filters
676 + * @param {array} $args WP Query args
677 + * @param {WP_Query} $query WP Query object
678 + * @return {array} New filters
540 679 */
541 - return apply_filters( 'ep_facet_filter_name', 'ep_filter_' );
680 + $facets_filters = apply_filters( 'ep_facet_query_filters', [], $args, $query );
681 +
682 + if ( empty( $facets_filters ) ) {
683 + return $filters;
684 + }
685 +
686 + $es_operator = ( 'any' === $this->get_match_type() ) ? 'should' : 'must';
687 +
688 + $filters['facets'] = [
689 + 'bool' => [
690 + $es_operator => $facets_filters,
691 + ],
692 + ];
693 +
694 + return $filters;
542 695 }
543 696
544 697 /**
545 - * Get all taxonomies that could be selected for a facet.
698 + * Utility function to retrieve the match type selected by the user.
546 699 *
547 - * @since 4.2.0
548 - * @return array
700 + * @since 4.4.0
701 + * @return string
549 702 */
550 - public function get_facetable_taxonomies() {
551 - $taxonomies = get_taxonomies( array( 'public' => true ), 'object' );
703 + public function get_match_type() {
704 + $settings = $this->get_settings();
705 +
552 706 /**
553 - * Filter taxonomies made available for faceting
707 + * Filter the match type of all facets. Can be 'all' or 'any'.
554 708 *
555 - * @hook ep_facet_include_taxonomies
556 - * @param {array} $taxonomies Taxonomies
557 - * @return {array} New taxonomies
709 + * @hook ep_facet_match_type
710 + * @since 4.4.0
711 + * @param {string} $match_type Current selection
712 + * @return {string} New selection
558 713 */
559 - return apply_filters( 'ep_facet_include_taxonomies', $taxonomies );
714 + return apply_filters( 'ep_facet_match_type', $settings['match_type'] );
560 715 }
561 716
562 717 /**
718 + * Given an array of filters, remove the facets filter.
719 + *
720 + * This is used when the user wants posts matching ANY criteria, so aggregations should not restrict their results.
721 + *
722 + * @since 4.4.0
723 + * @param array $filters Filters to be applied to the ES query
724 + * @return array
725 + */
726 + public function remove_facets_filter( $filters ) {
727 + unset( $filters['facets'] );
728 + return $filters;
729 + }
730 +
731 + /**
732 + * Set the `settings_schema` attribute
733 + *
734 + * @since 5.0.0
735 + */
736 + protected function set_settings_schema() {
737 + $this->settings_schema[] = [
738 + 'key' => 'match_type',
739 + 'label' => __( 'Filter matching', 'elasticpress' ),
740 + 'options' => [
741 + [
742 + 'label' => __( 'Show results that match <strong>all</strong> selected filters', 'elasticpress' ),
743 + 'value' => 'all',
744 + ],
745 + [
746 + 'label' => __( 'Show results that match <strong>any</strong> selected filter', 'elasticpress' ),
747 + 'value' => 'any',
748 + ],
749 + ],
750 + 'type' => 'radio',
751 + ];
752 + }
753 +
754 + /**
563 755 * Figure out if Facet widget can display on page.
564 756 *
565 757 * @param WP_Query $query WP Query
566 758 * @since 4.2.1
@@ -567,6 +759,22 @@
567 759 * @return bool
568 760 */
569 761 protected function is_facetable_page( $query ) {
570 762 return $query->is_home() || $query->is_search() || $query->is_tax() || $query->is_tag() || $query->is_category() || $query->is_post_type_archive();
763 + }
764 +
765 + /**
766 + * Setup REST endpoints
767 + *
768 + * @since 5.0.0
769 + */
770 + public function setup_endpoints() {
771 + $meta_keys = new REST\MetaKeys();
772 + $meta_keys->register_routes();
773 +
774 + $meta_range = new REST\MetaRange();
775 + $meta_range->register_routes();
776 +
777 + $taxonomies = new REST\Taxonomies();
778 + $taxonomies->register_routes();
571 779 }
572 780 }