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 ) );
+ }
+}