diff --git a/includes/classes/Feature/Facets/Facets.php b/includes/classes/Feature/Facets/Facets.php index 7fdde8b6b3..c6b78ead16 100644 --- a/includes/classes/Feature/Facets/Facets.php +++ b/includes/classes/Feature/Facets/Facets.php @@ -106,6 +106,7 @@ public function setup() { add_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ], 10, 1 ); add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); add_action( 'pre_get_posts', [ $this, 'facet_query' ] ); + add_filter( 'ep_post_filters', [ $this, 'apply_facets_filters' ], 10, 3 ); } /** @@ -151,6 +152,10 @@ public function set_agg_filters( $args, $query_args, $query ) { return $args; } + if ( 'any' === $this->get_match_type() ) { + add_filter( 'ep_post_filters', [ $this, 'remove_facets_filter' ], 11 ); + } + /** * Filter WP query arguments that will be used to build the aggregations filter. * @@ -170,6 +175,8 @@ public function set_agg_filters( $args, $query_args, $query ) { $facet_formatted_args = Indexables::factory()->get( 'post' )->format_args( $query_args, $query ); add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); + remove_filter( 'ep_post_filters', [ $this, 'remove_facets_filter' ], 11 ); + $args['aggs']['terms']['filter'] = $facet_formatted_args['post_filter']; return $args; @@ -277,12 +284,16 @@ public function is_facetable( $query ) { * @since 2.5 */ public function facet_query( $query ) { - $feature = Features::factory()->get_registered_feature( 'facets' ); - - if ( ! $feature->is_facetable( $query ) ) { + if ( ! $this->is_facetable( $query ) ) { return; } + // If any filter was selected, there is no reason to prepend the list with sticky posts. + $selected_filters = $this->get_selected(); + if ( ! empty( array_filter( $selected_filters ) ) ) { + $query->set( 'ignore_sticky_posts', true ); + } + /** * Filter facet aggregations. * @@ -536,6 +547,86 @@ public function get_facetable_taxonomies() { } + /** + * Add a new filter to the ES query with selected facets + * + * @since 4.4.0 + * @param array $filters Current filters + * @param array $args WP Query args + * @param WP_Query $query WP Query object + * @return array + */ + public function apply_facets_filters( $filters, $args, $query ) { + if ( ! $this->is_facetable( $query ) ) { + return $filters; + } + + /** + * Filter facet selection filters to be applied to the ES query + * + * @hook ep_facet_query_filters + * @since 4.4.0 + * @param {array} $filters Current filters + * @param {array} $args WP Query args + * @param {WP_Query} $query WP Query object + * @return {array} New filters + */ + $facets_filters = apply_filters( 'ep_facet_query_filters', [], $args, $query ); + + if ( empty( $facets_filters ) ) { + return $filters; + } + + $es_operator = ( 'any' === $this->get_match_type() ) ? 'should' : 'must'; + + $filters['facets'] = [ + 'bool' => [ + $es_operator => $facets_filters, + ], + ]; + + return $filters; + } + + /** + * Utilitary function to retrieve the match type selected by the user. + * + * @since 4.4.0 + * @return string + */ + public function get_match_type() { + $settings = wp_parse_args( + $this->get_settings(), + array( + 'match_type' => 'all', + ) + ); + + /** + * Filter the match type of all facets. Can be 'all' or 'any'. + * + * @hook ep_facet_match_type + * @since 4.4.0 + * @param {string} $match_type Current selection + * @return {string} New selection + */ + return apply_filters( 'ep_facet_match_type', $settings['match_type'] ); + } + + /** + * Given an array of filters, remove the facets filter. + * + * This is used when the user wants posts matching ANY criteria, so aggregations should not restrict their results. + * + * @since 4.4.0 + * @param array $filters Filters to be applied to the ES query + * @return array + */ + public function remove_facets_filter( $filters ) { + unset( $filters['facets'] ); + return $filters; + } + /** * Figure out if Facet widget can display on page. * diff --git a/includes/classes/Feature/Facets/Types/Meta/FacetType.php b/includes/classes/Feature/Facets/Types/Meta/FacetType.php index 037033d8de..578ca49637 100644 --- a/includes/classes/Feature/Facets/Types/Meta/FacetType.php +++ b/includes/classes/Feature/Facets/Types/Meta/FacetType.php @@ -32,8 +32,7 @@ class FacetType extends \ElasticPress\Feature\Facets\FacetType { * Setup hooks and filters for feature */ public function setup() { - add_filter( 'ep_facet_agg_filters', [ $this, 'agg_filters' ], 10, 3 ); - add_action( 'pre_get_posts', [ $this, 'facet_query' ] ); + add_filter( 'ep_facet_query_filters', [ $this, 'add_query_filters' ] ); add_filter( 'ep_facet_wp_query_aggs_facet', [ $this, 'set_wp_query_aggs' ] ); add_action( 'ep_delete_post', [ $this, 'invalidate_meta_values_cache' ] ); @@ -53,48 +52,11 @@ public function setup() { * @return array */ public function agg_filters( $query_args ) { - // Not a facetable query - if ( empty( $query_args['ep_facet'] ) ) { - return $query_args; - } - - if ( ! class_exists( '\WP_Widget_Block' ) ) { - return $query_args; - } - - // Without a meta_query, there is nothing to do here. - if ( empty( $query_args['meta_query'] ) || ! is_array( $query_args['meta_query'] ) ) { - return $query_args; - } - - /** - * If the aggregations need to match ALL the criteria applied to the main query, - * all the filters applied to the main query should be applied to aggregations as well. - */ - $feature = Features::factory()->get_registered_feature( 'facets' ); - $settings = wp_parse_args( - $feature->get_settings(), - array( - 'match_type' => 'all', - ) + _doing_it_wrong( + __METHOD__, + esc_html( 'Aggregation filters related to facet types are now managed by the main Facets class.' ), + 'ElasticPress 4.4.0' ); - if ( 'all' === $settings['match_type'] ) { - return $query_args; - } - - /** - * If we got to this point, let's remove from the aggregation filters all - * meta fields used in facets. - */ - $facets_meta_fields = $this->get_facets_meta_fields(); - - foreach ( $query_args['meta_query'] as $i => $meta_query_clause ) { - if ( is_array( $meta_query_clause ) - && ! empty( $meta_query_clause['key'] ) - && in_array( $meta_query_clause['key'], $facets_meta_fields, true ) ) { - unset( $query_args['meta_query'][ $i ] ); - } - } return $query_args; } @@ -186,11 +148,17 @@ public function set_wp_query_aggs( $facet_aggs ) { } /** - * Apply the facet selection to the main query. + * DEPRECATED. Apply the facet selection to the main query. * * @param WP_Query $query WP Query */ public function facet_query( $query ) { + _doing_it_wrong( + __METHOD__, + esc_html( 'Facet selections are now applied directly to the ES Query.' ), + 'ElasticPress 4.4.0' + ); + $feature = Features::factory()->get_registered_feature( 'facets' ); if ( ! $feature->is_facetable( $query ) ) { @@ -230,6 +198,33 @@ public function facet_query( $query ) { $query->set( 'ignore_sticky_posts', true ); } + /** + * Add selected filters to the Facet filter in the ES query + * + * @since 4.4.0 + * @param array $filters Current Facet filters + * @return array + */ + public function add_query_filters( $filters ) { + $feature = Features::factory()->get_registered_feature( 'facets' ); + + $selected_filters = $feature->get_selected(); + if ( empty( $selected_filters ) || empty( $selected_filters[ $this->get_filter_type() ] ) ) { + return $filters; + } + + $meta_fields = $selected_filters[ $this->get_filter_type() ]; + foreach ( $meta_fields as $meta_field => $values ) { + $filters[] = [ + 'terms' => [ + 'meta.' . $meta_field . '.raw' => array_keys( $values['terms'] ), + ], + ]; + } + + return $filters; + } + /** * Get all fields selected in all Facet blocks * diff --git a/includes/classes/Feature/Facets/Types/Taxonomy/FacetType.php b/includes/classes/Feature/Facets/Types/Taxonomy/FacetType.php index 882fa5e168..b4d1219b8e 100644 --- a/includes/classes/Feature/Facets/Types/Taxonomy/FacetType.php +++ b/includes/classes/Feature/Facets/Types/Taxonomy/FacetType.php @@ -27,8 +27,7 @@ class FacetType extends \ElasticPress\Feature\Facets\FacetType { */ public function setup() { add_action( 'widgets_init', [ $this, 'register_widgets' ] ); - add_filter( 'ep_facet_agg_filters', [ $this, 'agg_filters' ] ); - add_action( 'pre_get_posts', [ $this, 'facet_query' ] ); + add_filter( 'ep_facet_query_filters', [ $this, 'add_query_filters' ] ); add_filter( 'ep_facet_wp_query_aggs_facet', [ $this, 'set_wp_query_aggs' ] ); $this->block = new Block(); @@ -42,33 +41,12 @@ public function setup() { * @return array */ public function agg_filters( $query_args ) { - // Without taxonomies there is nothing to do here. - if ( empty( $query_args['tax_query'] ) ) { - return $query_args; - } - - $feature = Features::factory()->get_registered_feature( 'facets' ); - $settings = wp_parse_args( - $feature->get_settings(), - array( - 'match_type' => 'all', - ) + _doing_it_wrong( + __METHOD__, + esc_html( 'Aggregation filters related to facet types are now managed by the main Facets class.' ), + 'ElasticPress 4.4.0' ); - if ( 'any' === $settings['match_type'] ) { - foreach ( $query_args['tax_query'] as $key => $taxonomy ) { - if ( is_array( $taxonomy ) ) { - unset( $query_args['tax_query'][ $key ] ); - } - } - } - - // @todo For some reason these are appearing in the query args, need to investigate - $unwanted_args = [ 'category_name', 'cat', 'tag', 'tag_id', 'taxonomy', 'term' ]; - foreach ( $unwanted_args as $unwanted_arg ) { - unset( $query_args[ $unwanted_arg ] ); - } - return $query_args; } @@ -138,13 +116,19 @@ public function get_facetable_taxonomies() { } /** - * We enable ElasticPress facet on all archive/search queries as well as non-static home pages. There is no way to know + * DEPRECATED. We enable ElasticPress facet on all archive/search queries as well as non-static home pages. There is no way to know * when a facet widget is used before the main query is executed so we enable EP * everywhere where a facet widget could be used. * * @param WP_Query $query WP Query */ public function facet_query( $query ) { + _doing_it_wrong( + __METHOD__, + esc_html( 'Facet selections are now applied directly to the ES Query.' ), + 'ElasticPress 4.4.0' + ); + $feature = Features::factory()->get_registered_feature( 'facets' ); if ( ! $feature->is_facetable( $query ) ) { @@ -196,6 +180,49 @@ public function facet_query( $query ) { $query->set( 'tax_query', $tax_query ); } + /** + * Add selected filters to the Facet filter in the ES query + * + * @since 4.4.0 + * @param array $filters Current Facet filters + * @return array + */ + public function add_query_filters( $filters ) { + $feature = Features::factory()->get_registered_feature( 'facets' ); + + $taxonomies = $this->get_facetable_taxonomies(); + if ( empty( $taxonomies ) ) { + return; + } + + $selected_filters = $feature->get_selected(); + if ( empty( $selected_filters ) || empty( $selected_filters[ $this->get_filter_type() ] ) ) { + return; + } + + // Account for taxonomies that should be woocommerce attributes, if WC is enabled + $attribute_taxonomies = []; + if ( function_exists( 'wc_attribute_taxonomy_name' ) ) { + $all_attr_taxonomies = wc_get_attribute_taxonomies(); + + foreach ( $all_attr_taxonomies as $attr_taxonomy ) { + $attribute_taxonomies[ $attr_taxonomy->attribute_name ] = wc_attribute_taxonomy_name( $attr_taxonomy->attribute_name ); + } + } + + foreach ( $selected_filters['taxonomies'] as $taxonomy => $filter ) { + $taxonomy_slug = $attribute_taxonomies[ $taxonomy ] ?? $taxonomy; + + $filters[] = [ + 'terms' => [ + 'terms.' . $taxonomy_slug . '.slug' => array_keys( $filter['terms'] ), + ], + ]; + } + + return $filters; + } + /** * Add taxonomies to facets aggs * diff --git a/tests/cypress/integration/features/facets.spec.js b/tests/cypress/integration/features/facets.spec.js index 04abf65df0..a336fd1a9f 100644 --- a/tests/cypress/integration/features/facets.spec.js +++ b/tests/cypress/integration/features/facets.spec.js @@ -457,6 +457,24 @@ describe('Facets Feature', () => { cy.get('@secondBlock').contains('.term', 'Meta Value (2) - 20').click(); cy.url().should('not.include', 'ep_meta_filter_meta_field_2=Meta+Value+%282%29+-+20'); cy.url().should('include', 'ep_meta_filter_meta_field_1=Meta+Value+%281%29+-+20'); + cy.get('@secondBlock') + .contains('a[aria-disabled="true"]', 'Meta Value (2) - 19') + .should('exist'); + + /** + * When Match Type is "any", all options need to be clickable + */ + cy.visitAdminPage('admin.php?page=elasticpress'); + cy.get('.ep-feature-facets .settings-button').click(); + cy.get('input[name="settings[match_type]"][value="any"]').check(); + cy.get('.ep-feature-facets .button-primary').click(); + + cy.visit('/'); + cy.get('@secondBlock').contains('.term', 'Meta Value (2) - 20').click(); + cy.get('@secondBlock').contains('.term', 'Meta Value (2) - 1').click(); + cy.get('.wp-block-elasticpress-facet a[aria-disabled="true"]').should('not.exist'); + cy.contains('.site-content article h2', 'Facet By Meta Post 20').should('exist'); + cy.contains('.site-content article h2', 'Facet By Meta Post 1').should('exist'); }); }); }); diff --git a/tests/php/features/TestFacet.php b/tests/php/features/TestFacet.php index f7d89421c3..3cb182756e 100644 --- a/tests/php/features/TestFacet.php +++ b/tests/php/features/TestFacet.php @@ -196,6 +196,77 @@ public function testSetAggFilter() { $this->assertSame( $formatted_args['post_filter'], $formatted_args_with_args['aggs']['terms']['filter'] ); } + /** + * Test apply_facets_filters + * + * @since 4.4.0 + * @group facets + */ + public function testApplyFacetsFilters() { + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + + $new_filters = $facet_feature->apply_facets_filters( [], [], new \WP_Query( [] ) ); + $this->assertSame( [], $new_filters ); + + /** + * Test the `ep_facet_query_filters` filter + */ + $add_filter = function( $filters, $args, $query ) { + $filters[] = [ + 'terms' => [ + 'post_type' => [ 'post', 'page' ], + ], + ]; + + $this->assertSame( [], $args ); + $this->assertInstanceOf( '\WP_Query', $query ); + + return $filters; + }; + add_filter( 'ep_facet_query_filters', $add_filter, 10, 3 ); + add_filter( 'ep_is_facetable', '__return_true' ); + + $new_filters = $facet_feature->apply_facets_filters( [], [], new \WP_Query( [] ) ); + $expected_filter = [ + 'facets' => [ + 'bool' => [ + 'must' => [ + [ + 'terms' => [ + 'post_type' => [ 'post', 'page' ], + ], + ], + ], + ], + ], + ]; + $this->assertSame( $expected_filter, $new_filters ); + + /** + * Changing the match type should change from `must` to `should` + */ + $change_match_type = function () { + return 'any'; + }; + add_filter( 'ep_facet_match_type', $change_match_type ); + + $new_filters = $facet_feature->apply_facets_filters( [], [], new \WP_Query( [] ) ); + $expected_filter = [ + 'facets' => [ + 'bool' => [ + 'should' => [ + [ + 'terms' => [ + 'post_type' => [ 'post', 'page' ], + ], + ], + ], + ], + ], + ]; + $this->assertSame( $expected_filter, $new_filters ); + } + /** * Utilitary function for the testGetSelected test. * diff --git a/tests/php/features/TestFacetTypeMeta.php b/tests/php/features/TestFacetTypeMeta.php index 708e826f7f..abb3515f8d 100644 --- a/tests/php/features/TestFacetTypeMeta.php +++ b/tests/php/features/TestFacetTypeMeta.php @@ -211,13 +211,26 @@ public function testGetMetaValues() { } /** - * Test agg_filters + * Test add_query_filters * - * @since 4.3.0 + * @since 4.4.0 * @group facets */ - public function testAggFilters() { - $this->markTestIncomplete(); + public function testAddQueryFilters() { + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $facet_feature->types['meta']; + + parse_str( 'ep_meta_filter_my_custom_field=dolor', $_GET ); + + $new_filters = $facet_type->add_query_filters( [] ); + $expected = [ + [ + 'terms' => [ + 'meta.my_custom_field.raw' => [ 'dolor' ], + ], + ], + ]; + $this->assertSame( $expected, $new_filters ); } /** @@ -230,16 +243,6 @@ public function testGetFacetsMetaFields() { $this->markTestIncomplete(); } - /** - * Test facet_query - * - * @since 4.3.0 - * @group facets - */ - public function testFacetQuery() { - $this->markTestIncomplete(); - } - /** * Test invalidate_meta_values_cache * diff --git a/tests/php/features/TestFacetTypeTaxonomy.php b/tests/php/features/TestFacetTypeTaxonomy.php index 2f4be1075a..618e9d9e5c 100644 --- a/tests/php/features/TestFacetTypeTaxonomy.php +++ b/tests/php/features/TestFacetTypeTaxonomy.php @@ -154,81 +154,25 @@ public function testSetWpQueryAggs() { } /** - * Test agg_filters + * Test add_query_filters * - * @since 4.3.0 + * @since 4.4.0 * @group facets */ - public function testAggFilters() { + public function testAddQueryFilters() { $facet_feature = Features::factory()->get_registered_feature( 'facets' ); $facet_type = $facet_feature->types['taxonomy']; - $query_args = []; - $this->assertSame( $query_args, $facet_type->agg_filters( $query_args ) ); + parse_str( 'ep_filter_taxonomy=dolor', $_GET ); - $query_args = [ - 'tax_query' => [ - [ - 'taxonomy' => 'category', - 'terms' => [ 1, 2, 3 ], - ], - [ - 'taxonomy' => 'post_tag', - 'terms' => [ 4, 5, 6 ], + $new_filters = $facet_type->add_query_filters( [] ); + $expected = [ + [ + 'terms' => [ + 'terms.taxonomy.slug' => [ 'dolor' ], ], ], ]; - - /** - * Test when `match_type` is `all`. In this case, all the filters applied to the - * main query should be applied to aggregations as well. - */ - $set_facet_match_type_all = function() { - return [ - 'facets' => [ - 'match_type' => 'all', - ], - ]; - }; - add_filter( 'pre_site_option_ep_feature_settings', $set_facet_match_type_all ); - add_filter( 'pre_option_ep_feature_settings', $set_facet_match_type_all ); - - $this->assertSame( $query_args, $facet_type->agg_filters( $query_args ) ); - - remove_filter( 'pre_site_option_ep_feature_settings', $set_facet_match_type_all ); - remove_filter( 'pre_option_ep_feature_settings', $set_facet_match_type_all ); - - /** - * Test when `match_type` is `any`. In this case, the code should remove - * from the aggregations filter the taxonomy filters applied to the main query. - */ - $set_facet_match_type_any = function() { - return [ - 'facets' => [ - 'match_type' => 'any', - ], - ]; - }; - add_filter( 'pre_site_option_ep_feature_settings', $set_facet_match_type_any ); - add_filter( 'pre_option_ep_feature_settings', $set_facet_match_type_any ); - - $this->assertSame( [ 'tax_query' => [] ], $facet_type->agg_filters( $query_args ) ); - - remove_filter( 'pre_site_option_ep_feature_settings', $set_facet_match_type_any ); - remove_filter( 'pre_option_ep_feature_settings', $set_facet_match_type_any ); - - /** - * Test the removal of unwanted parameters. - */ - $query_args = [ - 'category_name' => 'lorem', - 'cat' => 'lorem', - 'tag' => 'lorem', - 'tag_id' => 'lorem', - 'taxonomy' => 'lorem', - 'term' => 'lorem', - 'tax_query' => [ [] ], - ]; - $this->assertSame( [ 'tax_query' => [ [] ] ], $facet_type->agg_filters( $query_args ) ); + $this->assertSame( $expected, $new_filters ); } }