|
|
|
|
@ -119,6 +119,20 @@ class MetadataProfileController extends ControllerBase {
|
|
|
|
|
*/ |
|
|
|
|
protected array $usageCounts = []; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* Cached reverse map of field name → Search API indexed field entries. |
|
|
|
|
* |
|
|
|
|
* @var array<string, array<string, array>>|null |
|
|
|
|
*/ |
|
|
|
|
protected ?array $searchApiIndexMap = NULL; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* All loaded facet entities, keyed by facet machine name. |
|
|
|
|
* |
|
|
|
|
* @var array<string, \Drupal\facets\FacetInterface>|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' => '<p>', |
|
|
|
|
'#suffix' => '</p>', |
|
|
|
|
], |
|
|
|
|
@ -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(); |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
$field_id = 'field.storage.' . $storage->get('id'); |
|
|
|
|
} |
|
|
|
|
$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); |
|
|
|
|
/** |
|
|
|
|
* 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]; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// 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); |
|
|
|
|
return $sources; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// --- 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); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// --- 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; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
// 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); |
|
|
|
|
if (!empty($sources)) { |
|
|
|
|
return $sources; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 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); |
|
|
|
|
} |
|
|
|
|
// --- Direct property_path match ----------------------------------------- |
|
|
|
|
if (isset($field_setting['datasource_id']) && str_starts_with($property_path, 'field_')) { |
|
|
|
|
return [$property_path]; |
|
|
|
|
} |
|
|
|
|
if (count($search_api['fields']) > 0) { |
|
|
|
|
$search_api['in_search_api'] = TRUE; |
|
|
|
|
|
|
|
|
|
return []; |
|
|
|
|
} |
|
|
|
|
if (in_array(TRUE, array_column($search_api['fields'], 'has_facet'), TRUE)) { |
|
|
|
|
$search_api['has_facet'] = TRUE; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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]; |
|
|
|
|
} |
|
|
|
|
else { |
|
|
|
|
$search_api['has_facet'] = FALSE; |
|
|
|
|
} |
|
|
|
|
return $names; |
|
|
|
|
} |
|
|
|
|
return $search_api; |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
* 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, |
|
|
|
|
]; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|