diff --git a/includes/classes/Feature/Facets/Types/Meta/FacetType.php b/includes/classes/Feature/Facets/Types/Meta/FacetType.php index 0b8c690325..916bd2e7dd 100644 --- a/includes/classes/Feature/Facets/Types/Meta/FacetType.php +++ b/includes/classes/Feature/Facets/Types/Meta/FacetType.php @@ -9,6 +9,7 @@ namespace ElasticPress\Feature\Facets\Types\Meta; use ElasticPress\Features; +use WP_Query; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. @@ -333,6 +334,134 @@ public function get_facets_meta_fields() { return apply_filters( 'ep_facet_meta_fields', $facets_meta_fields ); } + /** + * Get aggregation data for a specific meta field. + * + * If the main query does not have aggregation data for the requested field, + * run a facetable fallback query that requests this field and merge the + * aggregation result back into the original query object. + * + * @since 5.3.3 + * @param WP_Query $query Query object. + * @param string $meta_field Meta field key. + * @return array|false + */ + public function get_facet_aggregation_for_field( WP_Query $query, string $meta_field ) { + $feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_name = $this->get_filter_name() . $meta_field; + + // If aggregation data is already on this query, reuse it and skip a fallback query. + $facet_aggregation = $feature->get_facet_aggregation( $query, $facet_name ); + + if ( false !== $facet_aggregation ) { + return $facet_aggregation; + } + + // Track attempted fields on this query so one miss does not trigger repeated fallback queries. + $attempted_fields = (array) $query->get( 'ep_meta_runtime_agg_fields_attempted', [] ); + + if ( in_array( $meta_field, $attempted_fields, true ) ) { + return false; + } + + $attempted_fields[] = $meta_field; + + $query->set( 'ep_meta_runtime_agg_fields_attempted', array_values( array_unique( $attempted_fields ) ) ); + + // Run a second query in the same context so late-rendered blocks can still get this field's counts. + $runtime_aggregations = $this->query_runtime_aggregations( $query, [ $meta_field ] ); + + if ( empty( $runtime_aggregations[ $facet_name ] ) || ! is_array( $runtime_aggregations[ $facet_name ] ) ) { + return false; + } + + // Save recovered aggregation data on the original query so renderers read one source of truth. + $current_aggregations = $feature->get_query_aggregations( $query ); + + if ( ! is_array( $current_aggregations ) ) { + $current_aggregations = []; + } + + $feature->set_query_aggregations( $query, array_merge( $current_aggregations, $runtime_aggregations ) ); + + return $runtime_aggregations[ $facet_name ]; + } + + /** + * Run a facetable query and return aggregations for selected meta fields. + * + * @since 5.3.3 + * @param WP_Query $query Query object. + * @param array $meta_fields Meta fields. + * @return array + */ + protected function query_runtime_aggregations( WP_Query $query, array $meta_fields ): array { + // Normalize input so facet keys are built from non-empty, unique field names. + $meta_fields = array_values( array_unique( array_filter( $meta_fields ) ) ); + + if ( empty( $meta_fields ) ) { + return []; + } + + $feature = Features::factory()->get_registered_feature( 'facets' ); + + // Include selected meta fields so fallback counts reflect currently selected facet filters. + $selected_meta_fields = []; + $selected_filters = (array) $feature->get_selected(); + + if ( ! empty( $selected_filters[ $this->get_filter_type() ] ) && is_array( $selected_filters[ $this->get_filter_type() ] ) ) { + $selected_meta_fields = array_keys( $selected_filters[ $this->get_filter_type() ] ); + } + + $runtime_fields = array_values( array_unique( array_merge( $meta_fields, $selected_meta_fields ) ) ); + + $add_meta_fields = function ( $fields ) use ( $runtime_fields ) { + return array_values( array_unique( array_merge( (array) $fields, $runtime_fields ) ) ); + }; + + // Inject runtime fields so normal facet aggregation building includes them. + add_filter( 'ep_facet_meta_fields', $add_meta_fields ); + + $query_args = $query->query_vars; + + // Remove internal guard state from the cloned query arguments. + unset( $query_args['ep_meta_runtime_agg_fields_attempted'] ); + + // Force facetable flags because Facets::is_facetable() decides whether aggregations are attached. + $query_args = wp_parse_args( + $query_args, + [ + 'ep_is_facetable' => true, + 'ep_integrate' => true, + // Keep query filters enabled; suppressing them can change which posts are included in facet counts. + 'suppress_filters' => false, + ] + ); + + try { + $runtime_query = new WP_Query( $query_args ); + } finally { + // Always remove temporary filters to restore global state. + remove_filter( 'ep_facet_meta_fields', $add_meta_fields ); + } + + // Return only requested fields. Any extra selected fields were included only for context. + $aggregations = $feature->get_query_aggregations( $runtime_query ); + + if ( ! is_array( $aggregations ) ) { + return []; + } + + $facet_names = array_map( + function ( $field ) { + return $this->get_filter_name() . $field; + }, + $meta_fields + ); + + return array_intersect_key( $aggregations, array_flip( $facet_names ) ); + } + /** * Get all values for the a given meta field. * diff --git a/includes/classes/Feature/Facets/Types/Meta/Renderer.php b/includes/classes/Feature/Facets/Types/Meta/Renderer.php index c6a9578a23..e6ebf6658b 100644 --- a/includes/classes/Feature/Facets/Types/Meta/Renderer.php +++ b/includes/classes/Feature/Facets/Types/Meta/Renderer.php @@ -77,8 +77,11 @@ public function render( $args, $instance ) { */ $raw_values = $facet_type->get_meta_values( $instance['facet'] ); - $facet_name = $facet_type->get_filter_name() . $this->meta_field; - $facet_aggregations = $feature->get_facet_aggregation( $wp_query, $facet_name ); + $facet_aggregations = $facet_type->get_facet_aggregation_for_field( $wp_query, $this->meta_field ); + + if ( ! is_array( $facet_aggregations ) ) { + $facet_aggregations = []; + } $values = []; diff --git a/tests/php/features/TestFacetTypeMeta.php b/tests/php/features/TestFacetTypeMeta.php index 9153c30bd1..1b6b17a932 100644 --- a/tests/php/features/TestFacetTypeMeta.php +++ b/tests/php/features/TestFacetTypeMeta.php @@ -317,6 +317,77 @@ public function testAddQueryFiltersWithNotAllowedParameters() { $this->assertSame( $expected, $new_filters ); } + /** + * Test that meta facet aggregations can be recovered at runtime when the + * main facetable query did not include that meta field aggregation. + * + * This verifies the fallback path used by the renderer to avoid empty + * (zero-count) facet items when aggregation data is missing. + * + * @since 5.3.1 + * @group facets + */ + public function testGetFacetAggregationForFieldRuntimeFallback() { + $meta_field = 'runtime_meta_' . wp_generate_password( 8, false, false ); + + $allow_meta_key = function ( $allowed_meta_keys ) use ( $meta_field ) { + return array_merge( $allowed_meta_keys, [ $meta_field ] ); + }; + add_filter( 'ep_prepare_meta_allowed_keys', $allow_meta_key ); + + try { + $this->ep_factory->post->create_many( + 2, + [ + 'meta_input' => [ + $meta_field => 'alpha', + ], + ] + ); + + $this->ep_factory->post->create( + [ + 'meta_input' => [ + $meta_field => 'beta', + ], + ] + ); + + \ElasticPress\Elasticsearch::factory()->refresh_indices(); + + $query = new \WP_Query( + [ + 'ep_is_facetable' => true, + 'post_type' => 'post', + ] + ); + + // Confirm this query was served by Elasticsearch and can hold facet aggs. + $this->assertTrue( $query->elasticsearch_success ); + + $facet_feature = Features::factory()->get_registered_feature( 'facets' ); + $facet_type = $facet_feature->types['meta']; + $facet_name = $facet_type->get_filter_name() . $meta_field; + + // The requested meta field is intentionally missing before fallback runs. + $this->assertFalse( $facet_feature->get_facet_aggregation( $query, $facet_name ) ); + + $aggregation = $facet_type->get_facet_aggregation_for_field( $query, $meta_field ); + + // Fallback returns real counts for the requested field. + $this->assertIsArray( $aggregation ); + $this->assertArrayHasKey( 'alpha', $aggregation ); + $this->assertArrayHasKey( 'beta', $aggregation ); + $this->assertSame( 2, $aggregation['alpha'] ); + $this->assertSame( 1, $aggregation['beta'] ); + + // Fallback result is merged back into the original query aggregation cache. + $this->assertSame( $aggregation, $facet_feature->get_facet_aggregation( $query, $facet_name ) ); + } finally { + remove_filter( 'ep_prepare_meta_allowed_keys', $allow_meta_key ); + } + } + /** * Test get_facets_meta_fields *