diff --git a/src/Controller/MetadataProfileController.php b/src/Controller/MetadataProfileController.php index db0abb1..d436726 100644 --- a/src/Controller/MetadataProfileController.php +++ b/src/Controller/MetadataProfileController.php @@ -119,6 +119,20 @@ class MetadataProfileController extends ControllerBase { */ protected array $usageCounts = []; + /** + * Cached reverse map of field name → Search API indexed field entries. + * + * @var array>|null + */ + protected ?array $searchApiIndexMap = NULL; + + /** + * All loaded facet entities, keyed by facet machine name. + * + * @var array|null + */ + protected ?array $allFacets = NULL; + /** * Constructs a MetadataProfileController object. * @@ -180,6 +194,89 @@ class MetadataProfileController extends ControllerBase { $container->get('entity_type.manager'), ); } + /** + * Builds a reverse map of field name → indexed field entries across all + * Search API indexes. + * + * Map structure: + * @code + * [ + * 'field_abstract' => [ + * 'search_api.index.default.field_abstract' => [ + * 'search_api_field_name' => 'field_abstract', + * 'search_api_field_label' => 'Abstract', + * 'property_path' => 'field_abstract', + * 'type' => 'text', + * 'index_name' => 'Default', + * 'fields_included' => [], + * 'has_facet' => FALSE, + * 'facets' => [], + * ], + * ], + * ] + * @endcode + * + * @return array + * The reverse map, also stored in $this->searchApiIndexMap. + */ + protected function buildSearchApiIndexMap(): array { + if ($this->searchApiIndexMap !== NULL) { + return $this->searchApiIndexMap; + } + + $this->searchApiIndexMap = []; + + if (!$this->moduleHandler()->moduleExists('search_api')) { + return $this->searchApiIndexMap; + } + + // Load all facets once so getSearchApiField() can use them without + // hitting the DB on every call. + $this->allFacets = $this->moduleHandler()->moduleExists('facets') + ? $this->entityTypeManager()->getStorage('facets_facet')->loadMultiple() + : []; + + // Build a facet lookup keyed by field_identifier for O(1) access. + // $facetsByFieldId['field_abstract'] = [facet, facet, ...] + $facetsByFieldId = []; + foreach ($this->allFacets as $facet_id => $facet) { + $facetsByFieldId[$facet->get('field_identifier')][] = [ + 'field_identifier' => $facet->get('field_identifier'), + 'facet_name' => $facet->getName(), + 'facet_machine_name' => $facet_id, + 'facet_source' => $facet->getFacetSourceId(), + 'url_alias' => $facet->getUrlAlias(), + 'block_visible' => $this->getBlockVisible($facet), + ]; + } + + // Iterate every index config exactly once. + foreach ($this->configFactory->listAll('search_api.index') as $index_config_name) { + $index_config = $this->config($index_config_name); + + foreach ($index_config->get('field_settings') as $sa_field_name => $sa_field_setting) { + $indexed_field_key = $index_config->getName() . '.' . $sa_field_name; + + // Resolve which source field(s) this indexed field maps back to. + $source_field_names = $this->resolveSourceFieldNames( + $sa_field_setting, + $index_config + ); + + foreach ($source_field_names as $source_field_name) { + $this->searchApiIndexMap[$source_field_name][$indexed_field_key] = + $this->buildSearchApiFieldEntry( + $sa_field_name, + $sa_field_setting, + $index_config, + $facetsByFieldId + ); + } + } + } + + return $this->searchApiIndexMap; + } /** * Displays the metadata profile page for the current bundle. @@ -266,7 +363,7 @@ class MetadataProfileController extends ControllerBase { '#suffix' => ')', ], 'description' => [ - '#plain_text' => $description, + '#markup' => $description, '#prefix' => '

', '#suffix' => '

', ], @@ -369,91 +466,180 @@ class MetadataProfileController extends ControllerBase { } /** - * Builds Search API metadata for a field definition. + * Returns Search API metadata for a single field definition. * - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The field definition. + * This is now a pure lookup into the pre-built index map rather than a + * nested scan of every index and every field setting. * - * @return array|false - * An associative array containing: - * - in_search_api: bool - * - has_facet: bool - * - fields: array - * Or FALSE if Search API module is not enabled. + * {@inheritdoc} */ - private function getSearchApi($field_definition) { + private function getSearchApi(FieldDefinitionInterface $field_definition): array|false { if (!$this->moduleHandler()->moduleExists('search_api')) { return FALSE; } - $search_api = [ - 'in_search_api' => FALSE, - 'fields' => [], + + // Ensure the map has been built (idempotent). + $map = $this->buildSearchApiIndexMap(); + + $field_name = $field_definition->getName(); + $indexed_fields = $map[$field_name] ?? []; + + $has_facet = !empty(array_filter( + array_column($indexed_fields, 'has_facet') + )); + + return [ + 'in_search_api' => !empty($indexed_fields), + 'has_facet' => $has_facet, + 'fields' => $indexed_fields, ]; - $storage = $field_definition->getFieldStorageDefinition(); - if ($storage instanceof BaseFieldDefinition) { - $field_id = $this->entityTypeBundleOf . '.' . $storage->getName(); + } + + /** + * Resolves the source field machine names that a Search API field setting + * maps back to on the original entity. + * + * This consolidates the scattered if/elseif chain that previously lived + * inside getSearchApi() into a single, testable method. + * + * @param array $field_setting + * A single entry from $index_config->get('field_settings'). + * @param \Drupal\Core\Config\ImmutableConfig $index_config + * The parent index config object (needed for processor settings). + * + * @return string[] + * Zero or more source field machine names (e.g. ['field_abstract']). + */ + private function resolveSourceFieldNames( + array $field_setting, + \Drupal\Core\Config\ImmutableConfig $index_config + ): array { + $property_path = $field_setting['property_path'] ?? ''; + + // --- Aggregated fields -------------------------------------------------- + if ($property_path === 'aggregated_field') { + // Each entry looks like 'entity:node/field_abstract'. + $sources = []; + foreach ($field_setting['configuration']['fields'] ?? [] as $ref) { + // Strip the 'entity:node/' prefix to get the bare field name. + $parts = explode('/', $ref, 2); + if (isset($parts[1])) { + $sources[] = $parts[1]; + } + } + return $sources; } - else { - $field_id = 'field.storage.' . $storage->get('id'); + + // --- EDTF year ---------------------------------------------------------- + if ($property_path === 'edtf_year') { + $processor_fields = $index_config + ->get('processor_settings.edtf_year_only.fields') ?? []; + return $this->resolveEdtfSourceNames($processor_fields); + } + + // --- EDTF date ---------------------------------------------------------- + if ($property_path === 'edtf_dates') { + $processor_fields = $index_config + ->get('processor_settings.edtf_date_processor.fields') ?? []; + return $this->resolveEdtfSourceNames($processor_fields); } - $search_api_indexes = $this->configFactory->listAll('search_api.index'); - foreach ($search_api_indexes as $index) { - $index_config = $this->config($index); - - // Loop over fields. - foreach ($index_config->get('field_settings') as $search_api_field_name => $search_api_field_setting) { - $indexed_field_name = $index_config->getName() . '.' . $search_api_field_name; - // Get Aggregated Fields. - if ($search_api_field_setting['property_path'] == 'aggregated_field') { - if (in_array('entity:node/' . $field_definition->getName(), $search_api_field_setting['configuration']['fields'])) { - $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); - } - } - // Get EDTF fields. - else { - if ($field_definition->getType() == 'edtf') { - // Get EDTF year. - if ($search_api_field_setting['property_path'] == 'edtf_year') { - $edtf_year_fields = $index_config->get('processor_settings')['edtf_year_only']['fields'] ?: []; - if (in_array(str_replace('.', '|', $field_definition->id()), $edtf_year_fields)) { - $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); - } - } - // Get EDTF Date - else { - if ($search_api_field_setting['property_path'] == 'edtf_dates') { - $edtf_date_fields = $index_config->get('processor_settings')['edtf_date_processor']['fields'] ?: []; - if (in_array(str_replace('.', '|', $field_definition->id()), $edtf_date_fields)) { - $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); - } - } - } - } - } - // Check dependencies for a dependency on this field. - if (isset($search_api_field_setting['dependencies']) and isset($search_api_field_setting['dependencies']['config'])) { - $field_dependencies = $search_api_field_setting['dependencies']['config']; - if (in_array($field_id, $field_dependencies)) { - $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); - } - } - // Check if the property path equals this field. - if (isset($search_api_field_setting['datasource_id']) and $search_api_field_setting['property_path'] == $field_definition->getName()) { - $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); + // --- Dependency-declared fields ----------------------------------------- + $config_deps = $field_setting['dependencies']['config'] ?? []; + if (!empty($config_deps)) { + $sources = []; + foreach ($config_deps as $dep) { + // dep looks like 'field.storage.node.field_abstract' + $parts = explode('.', $dep); + $candidate = end($parts); + if (str_starts_with($candidate, 'field_')) { + $sources[] = $candidate; } } - if (count($search_api['fields']) > 0) { - $search_api['in_search_api'] = TRUE; - } - if (in_array(TRUE, array_column($search_api['fields'], 'has_facet'), TRUE)) { - $search_api['has_facet'] = TRUE; + if (!empty($sources)) { + return $sources; } - else { - $search_api['has_facet'] = FALSE; + } + + // --- Direct property_path match ----------------------------------------- + if (isset($field_setting['datasource_id']) && str_starts_with($property_path, 'field_')) { + return [$property_path]; + } + + return []; + } + + /** + * Converts EDTF processor field references to bare field machine names. + * + * EDTF processor fields are stored as 'node|field_abstract' (pipe-delimited). + * + * @param string[] $processor_fields + * Raw entries from an EDTF processor's 'fields' setting. + * + * @return string[] + * Bare field machine names. + */ + private function resolveEdtfSourceNames(array $processor_fields): array { + $names = []; + foreach ($processor_fields as $ref) { + // Format is 'entity_type|field_name' (pipe-separated after str_replace). + $parts = explode('|', $ref, 2); + if (isset($parts[1])) { + $names[] = $parts[1]; } } - return $search_api; + return $names; + } + + /** + * Builds one Search API field entry including resolved facet data. + * + * Replaces the original getSearchApiField() — same output shape, but + * receives pre-loaded facets instead of loading them itself. + * + * @param string $field_setting_name + * The Search API field machine name. + * @param array $field_setting + * The raw field setting array from index config. + * @param \Drupal\Core\Config\ImmutableConfig $index_config + * The parent index config. + * @param array $facets_by_field_id + * Pre-loaded facets keyed by field_identifier. + * + * @return array + * Structured Search API field entry. + */ + private function buildSearchApiFieldEntry( + string $field_setting_name, + array $field_setting, + \Drupal\Core\Config\ImmutableConfig $index_config, + array $facets_by_field_id + ): array { + $property_path = $field_setting['property_path'] ?? ''; + + // Resolve included fields for aggregate / EDTF paths. + $fields_included = match ($property_path) { + 'aggregated_field' => $field_setting['configuration']['fields'] ?? [], + 'edtf_year' => $index_config->get('processor_settings.edtf_year_only.fields') ?? [], + 'edtf_dates' => $index_config->get('processor_settings.edtf_date_processor.fields') ?? [], + default => [], + }; + + // O(1) facet lookup from the pre-built map. + $facets = $facets_by_field_id[$field_setting_name] ?? []; + $has_facet = !empty($facets); + + return [ + 'search_api_field_name' => $field_setting_name, + 'search_api_field_label' => $field_setting['label'] ?? '', + 'property_path' => $property_path, + 'type' => $field_setting['type'] ?? '', + 'index_name' => $index_config->get('name'), + 'fields_included' => $fields_included, + 'has_facet' => $has_facet, + 'facets' => $facets, + ]; } /**