diff --git a/assets/js/blocks/facets/block.json b/assets/js/blocks/facets/taxonomy/block.json similarity index 72% rename from assets/js/blocks/facets/block.json rename to assets/js/blocks/facets/taxonomy/block.json index 251d8d78ed..0f0d895c73 100644 --- a/assets/js/blocks/facets/block.json +++ b/assets/js/blocks/facets/taxonomy/block.json @@ -1,7 +1,7 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, - "title": "Facet (ElasticPress)", + "title": "Facet by Taxonomy (ElasticPress)", "textdomain": "elasticpress", "name": "elasticpress/facet", "icon": "feedback", @@ -25,6 +25,6 @@ "supports": { "html": false }, - "editorScript": "file:/../../../../dist/js/facets-block-script.min.js", - "style": "file:/../../../../dist/css/facets-styles.min.css" + "editorScript": "file:/../../../../../dist/js/facets-block-script.min.js", + "style": "file:/../../../../../dist/css/facets-styles.min.css" } \ No newline at end of file diff --git a/assets/js/blocks/facets/edit.js b/assets/js/blocks/facets/taxonomy/edit.js similarity index 100% rename from assets/js/blocks/facets/edit.js rename to assets/js/blocks/facets/taxonomy/edit.js diff --git a/assets/js/blocks/facets/index.js b/assets/js/blocks/facets/taxonomy/index.js similarity index 100% rename from assets/js/blocks/facets/index.js rename to assets/js/blocks/facets/taxonomy/index.js diff --git a/assets/js/blocks/facets/transforms.js b/assets/js/blocks/facets/taxonomy/transforms.js similarity index 100% rename from assets/js/blocks/facets/transforms.js rename to assets/js/blocks/facets/taxonomy/transforms.js diff --git a/includes/classes/Feature/Facets/FacetType.php b/includes/classes/Feature/Facets/FacetType.php new file mode 100644 index 0000000000..fa83f35ee2 --- /dev/null +++ b/includes/classes/Feature/Facets/FacetType.php @@ -0,0 +1,37 @@ + 'all', ]; + $types = [ + 'taxonomy' => __NAMESPACE__ . '\Types\Taxonomy\FacetType', + ]; + + /** + * Filter the Facet types available. + * + * ``` + * add_filter( + * 'ep_facet_types', + * function ( $types ) { + * $types['post_type'] = '\MyPlugin\PostType'; + * return $types; + * } + * ); + * ``` + * + * @since 4.3.0 + * @hook ep_facet_types + * @param {array} $types Array of types available. Keys are slugs, values are class names. + * @return {array} New array of types available + */ + $types = apply_filters( 'ep_facet_types', $types ); + + foreach ( $types as $type => $class ) { + if ( is_a( $class, __NAMESPACE__ . '\FacetType', true ) ) { + $this->types[ $type ] = new $class(); + } + } + parent::__construct(); } @@ -67,17 +95,17 @@ public function setup() { return; } - add_action( 'widgets_init', [ $this, 'register_widgets' ] ); + foreach ( $this->types as $type => $class ) { + $this->types[ $type ]->setup(); + } + add_filter( 'widget_types_to_hide_from_legacy_widget_block', [ $this, 'hide_legacy_widget' ] ); add_action( 'ep_valid_response', [ $this, 'get_aggs' ], 10, 4 ); - add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); - add_action( 'pre_get_posts', [ $this, 'facet_query' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'admin_scripts' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'front_scripts' ] ); add_action( 'ep_feature_box_settings_facets', [ $this, 'settings' ], 10, 1 ); - - $this->block = new Block(); - $this->block->setup(); + add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); + add_action( 'pre_get_posts', [ $this, 'facet_query' ] ); } /** @@ -106,7 +134,10 @@ public function output_feature_box_settings() { } /** - * If we are doing or matches, we need to remove filters from aggs + * If we are doing `or` matches, we need to remove filters from aggs. + * + * By default, the same filters applied to the main query are applied to aggregations. + * If doing `or` matches, those should be removed so we get a broader set of results. * * @param array $args ES arguments * @param array $query_args Query arguments @@ -115,46 +146,31 @@ public function output_feature_box_settings() { * @return array */ public function set_agg_filters( $args, $query_args, $query ) { + // Not a facetable query if ( empty( $query_args['ep_facet'] ) ) { return $args; } - // @todo For some reason these are appearing in the query args, need to investigate - unset( $query_args['category_name'] ); - unset( $query_args['cat'] ); - unset( $query_args['tag'] ); - unset( $query_args['tag_id'] ); - unset( $query_args['taxonomy'] ); - unset( $query_args['term'] ); - - $facet_query_args = $query_args; - - $settings = $this->get_settings(); - - $settings = wp_parse_args( - $settings, - array( - 'match_type' => 'all', - ) - ); - - if ( ! empty( $facet_query_args['tax_query'] ) ) { - remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); - - foreach ( $facet_query_args['tax_query'] as $key => $taxonomy ) { - if ( is_array( $taxonomy ) ) { - if ( 'any' === $settings['match_type'] ) { - unset( $facet_query_args['tax_query'][ $key ] ); - } - } - } - - $facet_formatted_args = Indexables::factory()->get( 'post' )->format_args( $facet_query_args, $query ); + /** + * Filter WP query arguments that will be used to build the aggregations filter. + * + * The returned `$query_args` will be used to build the aggregations filter passing + * it through `Indexable\Post\Post::format_args()`. + * + * @hook ep_facet_agg_filters + * @since 4.3.0 + * @param {array} $query_args Query arguments + * @param {array} $args ES arguments + * @param {array} $query WP Query instance + * @return {array} New facets aggregations + */ + $query_args = apply_filters( 'ep_facet_agg_filters', $query_args, $args, $query ); - $args['aggs']['terms']['filter'] = $facet_formatted_args['post_filter']; + remove_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); + $facet_formatted_args = Indexables::factory()->get( 'post' )->format_args( $query_args, $query ); + add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); - add_filter( 'ep_post_formatted_args', [ $this, 'set_agg_filters' ], 10, 3 ); - } + $args['aggs']['terms']['filter'] = $facet_formatted_args['post_filter']; return $args; } @@ -261,57 +277,32 @@ public function is_facetable( $query ) { * @since 2.5 */ public function facet_query( $query ) { - if ( ! $this->is_facetable( $query ) ) { + $feature = Features::factory()->get_registered_feature( 'facets' ); + + if ( ! $feature->is_facetable( $query ) ) { return; } - $taxonomies = get_taxonomies( array( 'public' => true ), 'object' ); - /** - * Filter taxonomies made available for faceting + * Filter facet aggregations. + * + * This is used by facet types to add their own aggregations to the + * general facet. * - * @hook ep_facet_include_taxonomies - * @param {array} $taxonomies Taxonomies - * @return {array} New taxonomies + * @hook ep_facet_wp_query_aggs_facet + * @since 4.3.0 + * @param {array} $facets Facets aggregations + * @return {array} New facets aggregations */ - $taxonomies = apply_filters( 'ep_facet_include_taxonomies', $taxonomies ); + $facets = apply_filters( 'ep_facet_wp_query_aggs_facet', [] ); - if ( empty( $taxonomies ) ) { + if ( empty( $facets ) ) { return; } $query->set( 'ep_integrate', true ); $query->set( 'ep_facet', true ); - $facets = []; - - /** - * Retrieve aggregations based on a custom field. This field must exist on the mapping. - * Values available out-of-the-box are: - * - slug (default) - * - term_id - * - name - * - parent - * - term_taxonomy_id - * - term_order - * - facet (retrieves a JSON representation of the term object) - * - * @since 3.6.0 - * @hook ep_facet_use_field - * @param {string} $field The term field to use - * @return {string} The chosen term field - */ - $facet_field = apply_filters( 'ep_facet_use_field', 'slug' ); - - foreach ( $taxonomies as $slug => $taxonomy ) { - $facets[ $slug ] = array( - 'terms' => array( - 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ), - 'field' => 'terms.' . $slug . '.' . $facet_field, - ), - ); - } - $aggs = array( 'name' => 'terms', 'use-filter' => true, @@ -319,44 +310,6 @@ public function facet_query( $query ) { ); $query->set( 'aggs', $aggs ); - - $selected_filters = $this->get_selected(); - - $settings = $this->get_settings(); - - $settings = wp_parse_args( - $settings, - array( - 'match_type' => 'all', - ) - ); - - $tax_query = $query->get( 'tax_query', [] ); - - // 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 ) { - $tax_query[] = [ - 'taxonomy' => isset( $attribute_taxonomies[ $taxonomy ] ) ? $attribute_taxonomies[ $taxonomy ] : $taxonomy, - 'field' => 'slug', - 'terms' => array_keys( $filter['terms'] ), - 'operator' => ( 'any' === $settings['match_type'] ) ? 'or' : 'and', - ]; - } - - if ( ! empty( $selected_filters['taxonomies'] ) && 'any' === $settings['match_type'] ) { - $tax_query['relation'] = 'or'; - } - - $query->set( 'tax_query', $tax_query ); } /** @@ -405,20 +358,33 @@ public function get_aggs( $response, $query, $query_args, $query_object ) { * @return array */ public function get_selected() { - $filters = array( - 'taxonomies' => [], - ); - $allowed_args = $this->get_allowed_query_args(); - $filter_name = $this->get_filter_name(); + + $filters = []; + $filter_names = []; + foreach ( $this->types as $type_obj ) { + $filter_type = $type_obj->get_filter_type(); + + $filters[ $filter_type ] = []; + $filter_names[ $filter_type ] = $type_obj->get_filter_name(); + } foreach ( $_GET as $key => $value ) { // phpcs:ignore WordPress.Security.NonceVerification - if ( 0 === strpos( $key, $filter_name ) ) { - $taxonomy = str_replace( $filter_name, '', $key ); + $key = sanitize_key( $key ); + if ( is_array( $value ) ) { + $value = array_map( 'sanitize_text_field', $value ); + } else { + $value = sanitize_text_field( $value ); + } - $filters['taxonomies'][ $taxonomy ] = array( - 'terms' => array_fill_keys( array_map( 'trim', explode( ',', trim( $value, ',' ) ) ), true ), - ); + foreach ( $filter_names as $filter_type => $filter_name ) { + if ( 0 === strpos( $key, $filter_name ) ) { + $facet = str_replace( $filter_name, '', $key ); + + $filters[ $filter_type ][ $facet ] = array( + 'terms' => array_fill_keys( array_map( 'trim', explode( ',', trim( $value, ',' ) ) ), true ), + ); + } } if ( in_array( $key, $allowed_args, true ) ) { @@ -439,17 +405,22 @@ public function get_selected() { public function build_query_url( $filters ) { $query_param = array(); - if ( ! empty( $filters['taxonomies'] ) ) { - $tax_filters = $filters['taxonomies']; + foreach ( $this->types as $type_obj ) { + $filter_type = $type_obj->get_filter_type(); + + if ( ! empty( $filters[ $filter_type ] ) ) { + $type_filters = $filters[ $filter_type ]; - foreach ( $tax_filters as $taxonomy => $filter ) { - if ( ! empty( $filter['terms'] ) ) { - $query_param[ $this->get_filter_name() . $taxonomy ] = implode( ',', array_keys( $filter['terms'] ) ); + foreach ( $type_filters as $facet => $filter ) { + if ( ! empty( $filter['terms'] ) ) { + $query_param[ $type_obj->get_filter_name() . $facet ] = implode( ',', array_keys( $filter['terms'] ) ); + } } } } - $allowed_args = $this->get_allowed_query_args(); + $feature = Features::factory()->get_registered_feature( 'facets' ); + $allowed_args = $feature->get_allowed_query_args(); if ( ! empty( $filters ) ) { foreach ( $filters as $filter => $value ) { @@ -483,10 +454,10 @@ public function build_query_url( $filters ) { /** * Register facet widget(s) * - * @since 2.5 + * @since 2.5, deprecated in 4.3.0 */ public function register_widgets() { - register_widget( __NAMESPACE__ . '\Widget' ); + _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types[ \$type ]->register_widgets()" ); } /** @@ -547,33 +518,22 @@ public function get_allowed_query_args() { * @return string The filter name. */ protected function get_filter_name() { - /** - * Filter the facet filter name that's added to the URL - * - * @hook ep_facet_filter_name - * @since 4.0.0 - * @param {string} Facet filter name - * @return {string} New facet filter name - */ - return apply_filters( 'ep_facet_filter_name', 'ep_filter_' ); + _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_filter_name()" ); + + return $this->types['taxonomy']->get_filter_name(); } /** * Get all taxonomies that could be selected for a facet. * - * @since 4.2.0 + * @since 4.2.0, deprecated in 4.3.0 * @return array */ public function get_facetable_taxonomies() { - $taxonomies = get_taxonomies( array( 'public' => true ), 'object' ); - /** - * Filter taxonomies made available for faceting - * - * @hook ep_facet_include_taxonomies - * @param {array} $taxonomies Taxonomies - * @return {array} New taxonomies - */ - return apply_filters( 'ep_facet_include_taxonomies', $taxonomies ); + _deprecated_function( __METHOD__, '4.3.0', "\ElasticPress\Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_facetable_taxonomies()" ); + + return $this->types['taxonomy']->get_filter_name(); + } /** diff --git a/includes/classes/Feature/Facets/Block.php b/includes/classes/Feature/Facets/Types/Taxonomy/Block.php similarity index 79% rename from includes/classes/Feature/Facets/Block.php rename to includes/classes/Feature/Facets/Types/Taxonomy/Block.php index bd529f3d32..7186775202 100644 --- a/includes/classes/Feature/Facets/Block.php +++ b/includes/classes/Feature/Facets/Types/Taxonomy/Block.php @@ -6,7 +6,7 @@ * @package elasticpress */ -namespace ElasticPress\Feature\Facets; +namespace ElasticPress\Feature\Facets\Types\Taxonomy; use ElasticPress\Features; @@ -24,8 +24,6 @@ class Block { public function setup() { add_action( 'init', [ $this, 'register_block' ] ); add_action( 'rest_api_init', [ $this, 'setup_endpoints' ] ); - - $this->renderer = new Renderer(); } /** @@ -83,7 +81,7 @@ public function check_facets_taxonomies_rest_permission() { * @return array */ public function get_rest_facetable_taxonomies() { - $taxonomies_raw = Features::factory()->get_registered_feature( 'facets' )->get_facetable_taxonomies(); + $taxonomies_raw = Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_facetable_taxonomies(); $taxonomies = []; foreach ( $taxonomies_raw as $slug => $taxonomy ) { @@ -115,7 +113,7 @@ public function get_rest_facetable_taxonomies() { */ public function register_block() { register_block_type_from_metadata( - EP_PATH . 'assets/js/blocks/facets', + EP_PATH . 'assets/js/blocks/facets/taxonomy', [ 'render_callback' => [ $this, 'render_block' ], ] @@ -129,10 +127,25 @@ public function register_block() { */ public function render_block( $attributes ) { $attributes = $this->parse_attributes( $attributes ); + + /** + * Filter the class name to be used to render the Facet. + * + * @since 4.3.0 + * @hook ep_facet_renderer_class + * @param {string} $classname The name of the class to be instantiated and used as a renderer. + * @param {string} $facet_type The type of the facet. + * @param {string} $context Context where the renderer will be used: `block` or `widget`, for example. + * @param {string} $attributes Element attributes. + * @return {string} The name of the class + */ + $renderer_class = apply_filters( 'ep_facet_renderer_class', __NAMESPACE__ . '\Renderer', 'taxonomy', 'block', $attributes ); + $renderer = new $renderer_class(); + ob_start(); ?>
- renderer->render( [], $attributes ); ?> + render( [], $attributes ); ?>
renderer->render( [], $attributes ); + $renderer->render( [], $attributes ); $block_content = ob_get_clean(); if ( empty( $block_content ) ) { @@ -203,7 +220,7 @@ protected function parse_attributes( $attributes ) { ] ); if ( empty( $attributes['facet'] ) ) { - $taxonomies = Features::factory()->get_registered_feature( 'facets' )->get_facetable_taxonomies(); + $taxonomies = Features::factory()->get_registered_feature( 'facets' )->types['taxonomy']->get_facetable_taxonomies(); if ( ! empty( $taxonomies ) ) { $attributes['facet'] = key( $taxonomies ); } diff --git a/includes/classes/Feature/Facets/Types/Taxonomy/FacetType.php b/includes/classes/Feature/Facets/Types/Taxonomy/FacetType.php new file mode 100644 index 0000000000..2aec603149 --- /dev/null +++ b/includes/classes/Feature/Facets/Types/Taxonomy/FacetType.php @@ -0,0 +1,236 @@ +block = new Block(); + $this->block->setup(); + } + + /** + * If we are doing or matches, we need to remove filters from aggs + * + * @param array $query_args Query arguments + * @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', + ) + ); + + 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; + } + + /** + * Register facet widget(s) + */ + public function register_widgets() { + register_widget( __NAMESPACE__ . '\Widget' ); + } + + /** + * Get the facet filter name. + * + * @return string The filter name. + */ + public function get_filter_name() : string { + /** + * Filter the facet filter name that's added to the URL + * + * @hook ep_facet_filter_name + * @since 4.0.0 + * @param {string} Facet filter name + * @return {string} New facet filter name + */ + return apply_filters( 'ep_facet_filter_name', 'ep_filter_' ); + } + + /** + * Get the facet filter type. + * + * @return string The filter name. + */ + public function get_filter_type() : string { + /** + * Filter the facet filter type. Used by the Facet feature to organize filters. + * + * @hook ep_facet_filter_type + * @since 4.3.0 + * @param {string} Facet filter type + * @return {string} New facet filter type + */ + return apply_filters( 'ep_facet_filter_type', 'taxonomies' ); + } + + /** + * Get all taxonomies that could be selected for a facet. + * + * @return array + */ + public function get_facetable_taxonomies() { + $taxonomies = get_taxonomies( array( 'public' => true ), 'object' ); + /** + * Filter taxonomies made available for faceting + * + * @hook ep_facet_include_taxonomies + * @param {array} $taxonomies Taxonomies + * @return {array} New taxonomies + */ + return apply_filters( 'ep_facet_include_taxonomies', $taxonomies ); + } + + /** + * 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 ) { + $feature = Features::factory()->get_registered_feature( 'facets' ); + + if ( ! $feature->is_facetable( $query ) ) { + return; + } + + $taxonomies = $this->get_facetable_taxonomies(); + + if ( empty( $taxonomies ) ) { + return; + } + + $selected_filters = $feature->get_selected(); + + $settings = $feature->get_settings(); + + $settings = wp_parse_args( + $settings, + array( + 'match_type' => 'all', + ) + ); + + $tax_query = $query->get( 'tax_query', [] ); + + // 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 ) { + $tax_query[] = [ + 'taxonomy' => isset( $attribute_taxonomies[ $taxonomy ] ) ? $attribute_taxonomies[ $taxonomy ] : $taxonomy, + 'field' => 'slug', + 'terms' => array_keys( $filter['terms'] ), + 'operator' => ( 'any' === $settings['match_type'] ) ? 'or' : 'and', + ]; + } + + if ( ! empty( $selected_filters['taxonomies'] ) && 'any' === $settings['match_type'] ) { + $tax_query['relation'] = 'or'; + } + + $query->set( 'tax_query', $tax_query ); + } + + /** + * Add taxonomies to facets aggs + * + * @param array $facet_aggs Facet Aggs array. + * @since 4.3.0 + * @return array + */ + public function set_wp_query_aggs( $facet_aggs ) { + $taxonomies = $this->get_facetable_taxonomies(); + + if ( empty( $taxonomies ) ) { + return $facet_aggs; + } + + foreach ( $taxonomies as $slug => $taxonomy ) { + /** + * Retrieve aggregations based on a custom field. This field must exist on the mapping. + * Values available out-of-the-box are: + * - slug (default) + * - term_id + * - name + * - parent + * - term_taxonomy_id + * - term_order + * - facet (retrieves a JSON representation of the term object) + * + * @since 3.6.0, 4.3.0 added $taxonomy + * @hook ep_facet_use_field + * @param {string} $field The term field to use + * @param {WP_Taxonomy} $taxonomy The taxonomy + * @return {string} The chosen term field + */ + $facet_field = apply_filters( 'ep_facet_use_field', 'slug', $taxonomy ); + + $facet_aggs[ $slug ] = array( + 'terms' => array( + 'size' => apply_filters( 'ep_facet_taxonomies_size', 10000, $taxonomy ), + 'field' => 'terms.' . $slug . '.' . $facet_field, + ), + ); + } + + return $facet_aggs; + } +} diff --git a/includes/classes/Feature/Facets/Renderer.php b/includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php similarity index 98% rename from includes/classes/Feature/Facets/Renderer.php rename to includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php index 6032a82ad8..1bd852ee4f 100644 --- a/includes/classes/Feature/Facets/Renderer.php +++ b/includes/classes/Feature/Facets/Types/Taxonomy/Renderer.php @@ -6,7 +6,7 @@ * @package elasticpress */ -namespace ElasticPress\Feature\Facets; +namespace ElasticPress\Feature\Facets\Types\Taxonomy; use ElasticPress\Features as Features; use ElasticPress\Utils as Utils; @@ -77,8 +77,6 @@ public function render( $args, $instance ) { $selected_filters = $feature->get_selected(); - $match_type = ( ! empty( $instance['match_type'] ) ) ? $instance['match_type'] : 'all'; - /** * Get all the terms so we know if we should output the widget */ @@ -158,9 +156,10 @@ public function render( $args, $instance ) { * @hook ep_facet_search_threshold * @param {int} $search_threshold Search threshold * @param {string} $taxonomy Current taxonomy + * @param {string} $context Hint about where the value will be used * @return {int} New threshold */ - $search_threshold = apply_filters( 'ep_facet_search_threshold', 15, $taxonomy ); + $search_threshold = apply_filters( 'ep_facet_search_threshold', 15, $taxonomy, 'taxonomy' ); ?>
diff --git a/includes/classes/Feature/Facets/Widget.php b/includes/classes/Feature/Facets/Types/Taxonomy/Widget.php similarity index 88% rename from includes/classes/Feature/Facets/Widget.php rename to includes/classes/Feature/Facets/Types/Taxonomy/Widget.php index 4779c928a0..fd52a50df4 100644 --- a/includes/classes/Feature/Facets/Widget.php +++ b/includes/classes/Feature/Facets/Types/Taxonomy/Widget.php @@ -5,7 +5,7 @@ * @package elasticpress */ -namespace ElasticPress\Feature\Facets; +namespace ElasticPress\Feature\Facets\Types\Taxonomy; use \WP_Widget as WP_Widget; use ElasticPress\Features as Features; @@ -35,8 +35,6 @@ public function __construct() { ); parent::__construct( 'ep-facet', esc_html__( 'ElasticPress - Facet', 'elasticpress' ), $options ); - - $this->renderer = new Renderer(); } /** @@ -47,7 +45,11 @@ public function __construct() { * @since 2.5, 4.2.0 made a wrapper for the renderer call. */ public function widget( $args, $instance ) { - $this->renderer->render( $args, $instance ); + /** This filter is documented in includes/classes/Feature/Facets/Types/Taxonomy/Block.php */ + $renderer_class = apply_filters( 'ep_facet_renderer_class', __NAMESPACE__ . '\Renderer', 'taxonomy', 'block', $instance ); + $renderer = new $renderer_class(); + + $renderer->render( $args, $instance ); } /** @@ -62,7 +64,11 @@ public function widget( $args, $instance ) { protected function get_facet_term_html( $term, $url, $selected = false ) { _deprecated_function( __FUNCTION__, '4.2.0', '$this->renderer->get_facet_term_html()' ); - return $this->renderer->get_facet_term_html( $term, $url, $selected ); + /** This filter is documented in includes/classes/Feature/Facets/Types/Taxonomy/Block.php */ + $renderer_class = apply_filters( 'ep_facet_renderer_class', __NAMESPACE__ . '\Renderer', 'taxonomy', 'block', [] ); + $renderer = new $renderer_class(); + + return $renderer->get_facet_term_html( $term, $url, $selected ); } /** @@ -105,7 +111,7 @@ public function form( $instance ) { $orderby = ( ! empty( $instance['orderby'] ) ) ? $instance['orderby'] : ''; $order = ( ! empty( $instance['order'] ) ) ? $instance['order'] : ''; - $taxonomies = $feature->get_facetable_taxonomies(); + $taxonomies = $feature->types['taxonomy']->get_facetable_taxonomies(); $orderby_options = [ 'count' => __( 'Count', 'elasticpress' ), diff --git a/package.json b/package.json index 4c8e8f97ae..1d527a7a1f 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "instant-results-admin-script.min": "./assets/js/instant-results/admin/index.js", "notice-script.min": "./assets/js/notice.js", "ordering-script.min": "./assets/js/ordering/index.js", - "facets-block-script.min": "./assets/js/blocks/facets/index.js", + "facets-block-script.min": "./assets/js/blocks/facets/taxonomy/index.js", "related-posts-block-script.min": "./assets/js/blocks/related-posts/index.js", "settings-script.min": "./assets/js/settings.js", "sync-script.min": "./assets/js/sync/index.js", diff --git a/tests/cypress/integration/features/facets.spec.js b/tests/cypress/integration/features/facets.spec.js index 5c1ae96484..6f4e034c5a 100644 --- a/tests/cypress/integration/features/facets.spec.js +++ b/tests/cypress/integration/features/facets.spec.js @@ -27,15 +27,15 @@ describe('Facets Feature', () => { /** * Test that the Related Posts block is functional. */ - it('Can insert, configure, and use the Facet block', () => { + it('Can insert, configure, and use the Facet by Taxonomy block', () => { /** * Insert two Facets blocks. */ cy.openWidgetsPage(); cy.openBlockInserter(); - cy.getBlocksList().should('contain.text', 'Facet (ElasticPress)'); - cy.insertBlock('Facet (ElasticPress)'); - cy.insertBlock('Facet (ElasticPress)'); + cy.getBlocksList().should('contain.text', 'Facet by Taxonomy (ElasticPress)'); + cy.insertBlock('Facet by Taxonomy (ElasticPress)'); + cy.insertBlock('Facet by Taxonomy (ElasticPress)'); cy.get('.wp-block-elasticpress-facet').last().as('block'); /** diff --git a/tests/cypress/integration/features/search/search.spec.js b/tests/cypress/integration/features/search/search.spec.js index bc7beda505..104194509c 100644 --- a/tests/cypress/integration/features/search/search.spec.js +++ b/tests/cypress/integration/features/search/search.spec.js @@ -134,11 +134,13 @@ describe('Post Search Feature', () => { }, }, }).then(() => { - cy.wpCli('elasticpress index --setup --yes'); - cy.visit('/?s=awesome-aluminum-shoes-variation-sku'); - cy.contains('.site-content article:nth-of-type(1) h2', 'Awesome Aluminum Shoes').should( - 'exist', - ); + cy.wpCli('elasticpress index --setup --yes').then(() => { + cy.visit('/?s=awesome-aluminum-shoes-variation-sku'); + cy.contains( + '.site-content article:nth-of-type(1) h2', + 'Awesome Aluminum Shoes', + ).should('exist'); + }); }); }); }); diff --git a/tests/php/features/TestFacet.php b/tests/php/features/TestFacet.php index 93dba823f8..3f6d720e92 100644 --- a/tests/php/features/TestFacet.php +++ b/tests/php/features/TestFacet.php @@ -13,32 +13,41 @@ * Facet test class */ class TestFacets extends BaseTestCase { - /** - * Setup each test. + * Test facet type registration * - * @since 3.6.0 + * @since 4.3.0 + * @group facets */ - public function setUp() { - parent::setUp(); - } + public function testFacetTypeRegistration() { + $facet_type = $this->getMockForAbstractClass( '\ElasticPress\Feature\Facets\FacetType' ); + $facet_type->expects( $this->exactly( 1 ) )->method( 'setup' ); - /** - * Clean up after each test. - * - * @since 3.6.0 - */ - public function tearDown() { - parent::tearDown(); - } + $register_facet_type = function( $types ) use ( $facet_type ) { + $types['test_custom'] = get_class( $facet_type ); + return $types; + }; + + add_filter( 'ep_facet_types', $register_facet_type ); + + $facets = new \ElasticPress\Feature\Facets\Facets(); + + $this->assertArrayHasKey( 'test_custom', $facets->types ); + $this->assertInstanceOf( get_class( $facet_type ), $facets->types['test_custom'] ); + // Make sure it uses our instance + $facets->types['test_custom'] = $facet_type; + + $facets->setup(); + + remove_filter( 'ep_facet_types', $register_facet_type ); + } /** * Test the `get_selected` method * * @since 3.6.0 * @group facets - * */ public function testGetSelected() { $facet_feature = Features::factory()->get_registered_feature( 'facets' ); @@ -80,7 +89,6 @@ public function testGetSelected() { * * @since 3.6.0 * @group facets - * */ public function testBuildQueryUrl() { $facet_feature = Features::factory()->get_registered_feature( 'facets' ); @@ -118,6 +126,70 @@ public function testBuildQueryUrl() { $filters['s'] = 'dolor'; $this->assertEquals( 'test/?ep_filter_category=augue%2Cconsectetur&s=dolor', $facet_feature->build_query_url( $filters ) ); + + /** + * Test the `ep_facet_query_string` filter. + */ + $change_facet_query_string = function ( $query_string, $query_params ) { + $this->assertIsArray( $query_params ); + $query_string .= '&foobar'; + return $query_string; + }; + add_filter( 'ep_facet_query_string', $change_facet_query_string, 10, 2 ); + $this->assertStringEndsWith( '&foobar', $facet_feature->build_query_url( $filters ) ); + remove_filter( 'ep_facet_query_string', $change_facet_query_string, 10, 2 ); + + /** + * (Indirectly) test the `ep_facet_filter_name` filter + */ + $change_ep_facet_filter_name = function( $original_name ) { + $this->assertEquals( 'ep_filter_', $original_name ); + return 'ep_custom_filter_'; + }; + add_filter( 'ep_facet_filter_name', $change_ep_facet_filter_name ); + $this->assertEquals( 'test/?ep_custom_filter_category=augue%2Cconsectetur&s=dolor', $facet_feature->build_query_url( $filters ) ); + remove_filter( 'ep_facet_filter_name', $change_ep_facet_filter_name ); + } + + /** + * Test set_agg_filters + * + * @since 4.3.0 + * @group facets + */ + public function testSetAggFilter() { + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + + $args = [ + 'aggs' => [ + 'terms' => [] + ] + ]; + + $query_args = []; + + $query = new \WP_Query(); + + // No `ep_facet` in query_args will make it return the same array. + $this->assertSame( $args, $facet_feature->set_agg_filters( $args, $query_args, $query ) ); + + /** + * Without any function hooked to `ep_facet_agg_filters` we expect + * aggregation filters to matche exactly the filter applied to the main + * query. + */ + remove_all_filters( 'ep_facet_agg_filters' ); + $query_args = [ + 'ep_facet' => 1, + 'post_type' => 'post', + 'post_status' => 'publish', + ]; + // Get the ES query args. + $formatted_args = \ElasticPress\Indexables::factory()->get( 'post' )->format_args( $query_args, $query ); + // Get the ES query args after applying the changes to aggs filters. + $formatted_args_with_args = $facet_feature->set_agg_filters( $formatted_args, $query_args, $query ); + + $this->assertSame( $formatted_args['post_filter'], $formatted_args_with_args['aggs']['terms']['filter'] ); } /** diff --git a/tests/php/features/TestFacetTypeTaxonomy.php b/tests/php/features/TestFacetTypeTaxonomy.php new file mode 100644 index 0000000000..cd27bb7e86 --- /dev/null +++ b/tests/php/features/TestFacetTypeTaxonomy.php @@ -0,0 +1,234 @@ +get_registered_feature( 'facets' ); + $facet_type = $facet_feature->types['taxonomy']; + + /** + * Test default behavior + */ + $this->assertEquals( 'ep_filter_', $facet_type->get_filter_name() ); + + /** + * Test the `ep_facet_filter_name` filter + */ + $change_filter_name = function( $filter_name ) { + return $filter_name . '_'; + }; + add_filter( 'ep_facet_filter_name', $change_filter_name ); + $this->assertEquals( 'ep_filter__', $facet_type->get_filter_name() ); + remove_filter( 'ep_facet_filter_name', $change_filter_name ); + } + + /** + * Test get_filter_type + * + * @since 4.3.0 + * @group facets + */ + public function testGetFilterType() { + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $facet_feature->types['taxonomy']; + + /** + * Test default behavior + */ + $this->assertEquals( 'taxonomies', $facet_type->get_filter_type() ); + + /** + * Test the `ep_facet_filter_type` filter + */ + $change_filter_type = function( $filter_type ) { + return $filter_type . '_'; + }; + add_filter( 'ep_facet_filter_type', $change_filter_type ); + $this->assertEquals( 'taxonomies_', $facet_type->get_filter_type() ); + remove_filter( 'ep_facet_filter_type', $change_filter_type ); + } + + /** + * Test get_facetable_taxonomies + * + * @since 4.3.0 + * @group facets + */ + public function testGetFacetableTaxonomies() { + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $facet_feature->types['taxonomy']; + + $public_taxonomies = array_keys( get_taxonomies( array( 'public' => true ), 'names' ) ); + $facetable_taxonomies = array_keys( $facet_type->get_facetable_taxonomies() ); + + /** + * Test default behavior + */ + $this->assertEqualsCanonicalizing( $public_taxonomies, $facetable_taxonomies ); + $this->assertContains( 'category', $facetable_taxonomies ); + + /** + * Test the `ep_facet_include_taxonomies` filter + */ + $change_facetable_taxonomies = function( $taxonomies ) { + unset( $taxonomies['category'] ); + return $taxonomies; + }; + add_filter( 'ep_facet_include_taxonomies', $change_facetable_taxonomies ); + + $facetable_taxonomies = array_keys( $facet_type->get_facetable_taxonomies() ); + $this->assertNotContains( 'category', $facetable_taxonomies ); + + remove_filter( 'ep_facet_include_taxonomies', $change_facetable_taxonomies ); + } + + /** + * Test set_wp_query_aggs + * + * @since 4.3.0 + * @group facets + */ + public function testSetWpQueryAggs() { + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $facet_feature->types['taxonomy']; + + $with_aggs = $facet_type->set_wp_query_aggs( [] ); + + /** + * Test default behavior + */ + $default_cat_agg = [ + 'terms' => [ + 'size' => 10000, + 'field' => 'terms.category.slug', + ], + ]; + $this->assertSame( $with_aggs['category'], $default_cat_agg ); + + /** + * Test the `ep_facet_use_field` filter + */ + $change_cat_facet_field = function( $field, $taxonomy ) { + return ( 'category' === $taxonomy->name ) ? 'term_id' : $field; + }; + + add_filter( 'ep_facet_use_field', $change_cat_facet_field, 10, 2 ); + + $with_aggs = $facet_type->set_wp_query_aggs( [] ); + $this->assertSame( 'terms.category.term_id', $with_aggs['category']['terms']['field'] ); + $this->assertSame( 'terms.post_tag.slug', $with_aggs['post_tag']['terms']['field'] ); + + remove_filter( 'ep_facet_use_field', $change_cat_facet_field ); + + /** + * Test the `ep_facet_taxonomies_size` filter + */ + $change_tax_bucket_size = function( $size, $taxonomy ) { + return ( 'category' === $taxonomy->name ) ? 5 : $size; + }; + + add_filter( 'ep_facet_taxonomies_size', $change_tax_bucket_size, 10, 2 ); + + $with_aggs = $facet_type->set_wp_query_aggs( [] ); + $this->assertSame( 5, $with_aggs['category']['terms']['size'] ); + $this->assertSame( 10000, $with_aggs['post_tag']['terms']['size'] ); + + remove_filter( 'ep_facet_taxonomies_size', $change_tax_bucket_size ); + } + + /** + * Test agg_filters + * + * @since 4.3.0 + * @group facets + */ + public function testAggFilters() { + $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 ) ); + + $query_args = [ + 'tax_query' => [ + [ + 'taxonomy' => 'category', + 'terms' => [ 1, 2, 3 ], + ], + [ + 'taxonomy' => 'post_tag', + 'terms' => [ 4, 5, 6 ], + ], + ], + ]; + + /** + * 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 ) ); + } +}