Source: includes/classes/Feature/Facets/Types/MetaRange/FacetType.php

<?php
/**
 * Meta range facet type
 *
 * @since 4.5.0
 * @package elasticpress
 */

namespace ElasticPress\Feature\Facets\Types\MetaRange;

use \ElasticPress\Features;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Meta facet type class
 */
class FacetType extends \ElasticPress\Feature\Facets\FacetType {

	/**
	 * Block instance.
	 *
	 * @var Block
	 */
	public $block;

	/**
	 * Setup hooks and filters for feature
	 */
	public function setup() {
		add_filter( 'ep_facet_query_filters', [ $this, 'add_query_filters' ], 10, 2 );
		add_filter( 'ep_facet_wp_query_aggs_facet', [ $this, 'set_wp_query_aggs' ] );

		$this->block = new Block();
		$this->block->setup();
	}

	/**
	 * 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_meta_range_filter_name
		 * @since 4.5.0
		 * @param   {string} Facet filter name
		 * @return  {string} New facet filter name
		 */
		return apply_filters( 'ep_facet_meta_range_filter_name', 'ep_meta_range_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_meta_range_filter_type
		 * @since 4.5.0
		 * @param   {string} Facet filter type
		 * @return  {string} New facet filter type
		 */
		return apply_filters( 'ep_facet_meta_range_filter_type', 'meta-range' );
	}

	/**
	 * Add selected filters to the Facet filter in the ES query
	 *
	 * @param array $filters    Current Facet filters
	 * @param array $query_args WP_Query arguments
	 * @return array
	 */
	public function add_query_filters( $filters, $query_args = [] ) {
		/**
		 * We do not want to apply the range filter to the aggregations because then
		 * the minimum and maximum values will be the filter already applied.
		 *
		 * @see https://github.com/10up/ElasticPress/issues/3341
		 */
		if ( ! empty( $query_args['ep_facet_adding_agg_filters'] ) ) {
			return $filters;
		}

		$feature = Features::factory()->get_registered_feature( 'facets' );

		$all_selected_filters = $feature->get_selected();
		if ( empty( $all_selected_filters ) || empty( $all_selected_filters[ $this->get_filter_type() ] ) ) {
			return $filters;
		}

		/**
		 * Filter if EP should only filter by fields selected in facets. Defaults to true.
		 *
		 * @since 4.5.1
		 * @hook ep_facet_should_check_if_allowed
		 * @param {bool} $should_check Whether it should or not check fields
		 * @return {string} New value
		 */
		$should_check_if_allowed = apply_filters( 'ep_facet_should_check_if_allowed', true );
		if ( $should_check_if_allowed ) {
			$allowed_meta_fields = $this->get_facets_meta_fields();

			$selected_range_filters = array_filter(
				$all_selected_filters[ $this->get_filter_type() ],
				function ( $meta_field ) use ( $allowed_meta_fields ) {
					return in_array( $meta_field, $allowed_meta_fields, true );
				},
				ARRAY_FILTER_USE_KEY
			);
		} else {
			$selected_range_filters = $all_selected_filters[ $this->get_filter_type() ];
		}

		$range_filters = [];
		foreach ( $selected_range_filters as $field_name => $values ) {
			foreach ( $values as $min_or_max => $value ) {
				$operator = '_min' === $min_or_max ? 'gte' : 'lte';

				$range_filters[ $field_name ][ $operator ] = floatval( $value );
			}
		}

		foreach ( $range_filters as $field_name => $range_filter ) {
			$filters[] = [
				'range' => [
					'meta.' . $field_name . '.double' => $range_filter,
				],
			];
		}

		return $filters;
	}

	/**
	 * Add meta fields to facets aggs
	 *
	 * @param array $facet_aggs Facet Aggs array.
	 * @return array
	 */
	public function set_wp_query_aggs( $facet_aggs ) {
		$facets_meta_fields = $this->get_facets_meta_fields();

		foreach ( $facets_meta_fields as $meta_field ) {
			/**
			 * Retrieve aggregations based on a custom field. This field must exist on the mapping and be numeric
			 * so ES can apply min and max to it.
			 *
			 * `meta.<field>.value` is *not* available, as that throws a `Fielddata is disabled on text fields by default` error.
			 *
			 * @since 4.5.0
			 * @hook ep_facet_meta_range_use_field
			 * @param {string} $es_field   The Elasticsearch field to use for this meta field
			 * @param {string} $meta_field The meta field key
			 * @return {string} The chosen ES field
			 */
			$facet_field = apply_filters( 'ep_facet_meta_range_use_field', 'double', $meta_field );

			$facet_aggs[ $this->get_filter_name() . $meta_field . '_min' ] = array(
				'min' => array(
					'field' => 'meta.' . $meta_field . '.' . $facet_field,
				),
			);

			$facet_aggs[ $this->get_filter_name() . $meta_field . '_max' ] = array(
				'max' => array(
					'field' => 'meta.' . $meta_field . '.' . $facet_field,
				),
			);
		}

		return $facet_aggs;
	}

	/**
	 * Format selected values.
	 *
	 * For meta range facets, we will have `[ 'facet_name' => [ '_min' => X, '_max' => Y ] ]`;
	 *
	 * @since 4.5.0
	 * @param string $facet   Facet name
	 * @param mixed  $value   Facet value
	 * @param array  $filters Selected filters
	 * @return array
	 */
	public function format_selected( string $facet, $value, array $filters ) {
		$min_or_max = substr( $facet, -4 );
		$field_name = substr( $facet, 0, -4 );

		$filters[ $this->get_filter_type() ][ $field_name ][ $min_or_max ] = $value;

		return $filters;
	}

	/**
	 * Add selected filters to the query string.
	 *
	 * @since 4.5.0
	 * @param array $query_params Existent query parameters
	 * @param array $filters      Selected filters
	 * @return array
	 */
	public function add_query_params( array $query_params, array $filters ) : array {
		$selected = $filters[ $this->get_filter_type() ];

		foreach ( $selected as $facet => $values ) {
			foreach ( $values as $min_or_max => $value ) {
				$query_params[ $this->get_filter_name() . $facet . $min_or_max ] = $value;
			}
		}

		return $query_params;
	}

	/**
	 * Get all fields selected in all Facet blocks
	 *
	 * @return array
	 */
	public function get_facets_meta_fields() {
		$facets_meta_fields = [];

		$widget_block_instances = ( new \WP_Widget_Block() )->get_settings();
		foreach ( $widget_block_instances as $instance ) {
			if ( ! isset( $instance['content'] ) ) {
				continue;
			}

			if ( false === strpos( $instance['content'], 'elasticpress/facet-meta-range' ) ) {
				continue;
			}

			if ( ! preg_match_all( '/"facet":"(.*?)"/', $instance['content'], $matches ) ) {
				continue;
			}

			$facets_meta_fields = array_merge( $facets_meta_fields, $matches[1] );
		}

		if ( current_theme_supports( 'block-templates' ) ) {
			$facets_meta_fields = array_merge(
				$facets_meta_fields,
				$this->block_template_meta_fields( 'elasticpress/facet-meta-range' )
			);
		}

		/**
		 * Filter meta fields to be used in aggregations related to meta range blocks.
		 *
		 * @since 4.5.0
		 * @hook ep_facet_meta_range_fields
		 * @param {string} $facets_meta_fields Array of meta field keys
		 * @return {string} The array of meta field keys
		 */
		return apply_filters( 'ep_facet_meta_range_fields', $facets_meta_fields );
	}
}