1035 lines
39 KiB
PHP
1035 lines
39 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file
|
|
* Views hook implementations for the Search API module.
|
|
*/
|
|
|
|
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
|
|
use Drupal\Core\Entity\FieldableEntityInterface;
|
|
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
|
|
use Drupal\Core\TypedData\DataDefinitionInterface;
|
|
use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
|
|
use Drupal\search_api\Datasource\DatasourceInterface;
|
|
use Drupal\search_api\Entity\Index;
|
|
use Drupal\search_api\Event\MappingViewsFieldHandlersEvent;
|
|
use Drupal\search_api\Event\MappingViewsHandlersEvent;
|
|
use Drupal\search_api\Event\SearchApiEvents;
|
|
use Drupal\search_api\IndexInterface;
|
|
use Drupal\search_api\Item\FieldInterface;
|
|
use Drupal\search_api\SearchApiException;
|
|
use Drupal\search_api\Utility\Utility;
|
|
|
|
/**
|
|
* Implements hook_views_data().
|
|
*
|
|
* For each search index, we provide the following tables:
|
|
* - One base table, with key "search_api_index_INDEX", which contains field,
|
|
* filter, argument and sort handlers for all indexed fields. (Field handlers,
|
|
* too, to allow things like click-sorting.)
|
|
* - Tables for each datasource, by default with key
|
|
* "search_api_datasource_INDEX_DATASOURCE", with field and (where applicable)
|
|
* relationship handlers for each property of the datasource. Those will be
|
|
* joined to the index base table by default.
|
|
*
|
|
* Also, for each entity type encountered in any table, a table with
|
|
* field/relationship handlers for all of that entity type's properties is
|
|
* created. Those tables will use the key "search_api_entity_ENTITY".
|
|
*/
|
|
function search_api_views_data() {
|
|
$data = [];
|
|
|
|
/** @var \Drupal\search_api\IndexInterface $index */
|
|
foreach (Index::loadMultiple() as $index) {
|
|
try {
|
|
// Fill in base data.
|
|
$key = 'search_api_index_' . $index->id();
|
|
$table = &$data[$key];
|
|
$index_label = $index->label();
|
|
$table['table']['group'] = t('Index @name', ['@name' => $index_label]);
|
|
$table['table']['base'] = [
|
|
'field' => 'search_api_id',
|
|
'index' => $index->id(),
|
|
'title' => t('Index @name', ['@name' => $index_label]),
|
|
'help' => t('Use the @name search index for filtering and retrieving data.', ['@name' => $index_label]),
|
|
'query_id' => 'search_api_query',
|
|
];
|
|
|
|
// Add suitable handlers for all indexed fields.
|
|
foreach ($index->getFields(TRUE) as $field_id => $field) {
|
|
$field_alias = _search_api_views_find_field_alias($field_id, $table);
|
|
$field_definition = _search_api_views_get_handlers($field);
|
|
// The field handler has to be extra, since it is a) determined by the
|
|
// field's underlying property and b) needs a different "real field"
|
|
// set.
|
|
if ($field->getPropertyPath()) {
|
|
$field_handler = _search_api_views_get_field_handler_for_property($field->getDataDefinition(), $field->getPropertyPath());
|
|
if ($field_handler) {
|
|
$field_definition['field'] = $field_handler;
|
|
$field_definition['field']['real field'] = $field->getCombinedPropertyPath();
|
|
$field_definition['field']['search_api field'] = $field_id;
|
|
}
|
|
}
|
|
if ($field_definition) {
|
|
$field_label = $field->getLabel();
|
|
$field_definition += [
|
|
'title' => $field_label,
|
|
'help' => $field->getDescription() ?: t('(No description available)'),
|
|
];
|
|
if ($datasource = $field->getDatasource()) {
|
|
$field_definition['group'] = t('@datasource datasource', ['@datasource' => $datasource->label()]);
|
|
}
|
|
else {
|
|
// Backend defined fields that don't have a datasource should be
|
|
// treated like special fields.
|
|
$field_definition['group'] = t('Search');
|
|
}
|
|
if ($field_id != $field_alias) {
|
|
$field_definition['real field'] = $field_id;
|
|
}
|
|
if (isset($field_definition['field'])) {
|
|
$field_definition['field']['title'] = t('@field (indexed field)', ['@field' => $field_label]);
|
|
}
|
|
$table[$field_alias] = $field_definition;
|
|
}
|
|
}
|
|
|
|
// Add special fields.
|
|
_search_api_views_data_special_fields($table, $index);
|
|
|
|
// Add relationships for field data of all datasources.
|
|
$datasource_tables_prefix = 'search_api_datasource_' . $index->id() . '_';
|
|
foreach ($index->getDatasources() as $datasource_id => $datasource) {
|
|
$table_key = _search_api_views_find_field_alias($datasource_tables_prefix . $datasource_id, $data);
|
|
$data[$table_key] = _search_api_views_datasource_table($datasource, $data);
|
|
// Automatically join this table for views of this index.
|
|
$data[$table_key]['table']['join'][$key] = [
|
|
'join_id' => 'search_api',
|
|
];
|
|
}
|
|
}
|
|
catch (\Exception $e) {
|
|
$args = [
|
|
'%index' => $index->label(),
|
|
];
|
|
watchdog_exception('search_api', $e, '%type while computing Views data for index %index: @message in %function (line %line of %file).', $args);
|
|
}
|
|
}
|
|
|
|
return array_filter($data);
|
|
}
|
|
|
|
/**
|
|
* Implements hook_views_plugins_cache_alter().
|
|
*/
|
|
function search_api_views_plugins_cache_alter(array &$plugins) {
|
|
// Collect all base tables provided by this module.
|
|
$bases = [];
|
|
/** @var \Drupal\search_api\IndexInterface $index */
|
|
foreach (Index::loadMultiple() as $index) {
|
|
$bases[] = 'search_api_index_' . $index->id();
|
|
}
|
|
|
|
// If no search indexes are defined yet, declare a dummy index as the base
|
|
// table. This will make sure our plugins do not become available for Views
|
|
// that are not based on search indexes.
|
|
if (!$bases) {
|
|
$bases = ['search_api_index_dummy'];
|
|
}
|
|
|
|
$plugins['search_api_tag']['base'] = $bases;
|
|
$plugins['search_api_time']['base'] = $bases;
|
|
}
|
|
|
|
/**
|
|
* Implements hook_views_plugins_row_alter().
|
|
*/
|
|
function search_api_views_plugins_row_alter(array &$plugins) {
|
|
// Collect all base tables provided by this module.
|
|
$bases = [];
|
|
/** @var \Drupal\search_api\IndexInterface $index */
|
|
foreach (Index::loadMultiple() as $index) {
|
|
$bases[] = 'search_api_index_' . $index->id();
|
|
}
|
|
|
|
// If no search indexes are defined yet, declare a dummy index as the base
|
|
// table. This will make sure our plugins do not become available for Views
|
|
// that are not based on search indexes.
|
|
if (!$bases) {
|
|
$bases = ['search_api_index_dummy'];
|
|
}
|
|
|
|
$plugins['search_api']['base'] = $bases;
|
|
$plugins['search_api_data']['base'] = $bases;
|
|
}
|
|
|
|
/**
|
|
* Finds an unused field alias for a field in a Views table definition.
|
|
*
|
|
* @param string $field_id
|
|
* The original ID of the Search API field.
|
|
* @param array $table
|
|
* The Views table definition.
|
|
*
|
|
* @return string
|
|
* The field alias to use.
|
|
*/
|
|
function _search_api_views_find_field_alias($field_id, array &$table) {
|
|
$base = $field_alias = preg_replace('/[^a-zA-Z0-9]+/S', '_', $field_id);
|
|
$i = 0;
|
|
while (isset($table[$field_alias])) {
|
|
$field_alias = $base . '_' . ++$i;
|
|
}
|
|
return $field_alias;
|
|
}
|
|
|
|
/**
|
|
* Returns the Views handlers to use for a given field.
|
|
*
|
|
* @param \Drupal\search_api\Item\FieldInterface $field
|
|
* The field to add to the definition.
|
|
*
|
|
* @return array
|
|
* The Views definition to add for the given field.
|
|
*/
|
|
function _search_api_views_get_handlers(FieldInterface $field) {
|
|
$mapping = _search_api_views_handler_mapping();
|
|
|
|
try {
|
|
$types = [];
|
|
|
|
$definition = $field->getDataDefinition();
|
|
$entity_type_id = $definition->getSetting('target_type');
|
|
if ($entity_type_id) {
|
|
$entity_type = \Drupal::entityTypeManager()
|
|
->getDefinition($entity_type_id);
|
|
$bundle_of = $entity_type->getBundleOf();
|
|
if ($bundle_of) {
|
|
$types[] = "bundle_of:$bundle_of";
|
|
$types[] = ['bundle_of', $bundle_of];
|
|
}
|
|
|
|
$types[] = "entity:$entity_type_id";
|
|
$types[] = ['entity', $entity_type_id];
|
|
}
|
|
|
|
// Special treatment for languages (as we have no specific Search API data
|
|
// type for those).
|
|
if ($definition->getSetting('views_type') === 'language') {
|
|
$types[] = 'language';
|
|
}
|
|
|
|
if ($definition->getSetting('allowed_values')) {
|
|
$types[] = 'options';
|
|
}
|
|
|
|
$types[] = $field->getType();
|
|
/** @var \Drupal\search_api\DataType\DataTypeInterface $data_type */
|
|
$data_type = \Drupal::service('plugin.manager.search_api.data_type')->createInstance($field->getType());
|
|
if (!$data_type->isDefault()) {
|
|
$types[] = $data_type->getFallbackType();
|
|
}
|
|
|
|
foreach ($types as $type) {
|
|
$sub_type = NULL;
|
|
if (is_array($type)) {
|
|
[$type, $sub_type] = $type;
|
|
}
|
|
if (isset($mapping[$type])) {
|
|
_search_api_views_handler_adjustments($type, $field, $mapping[$type], $sub_type);
|
|
return $mapping[$type];
|
|
}
|
|
}
|
|
}
|
|
// @todo Replace with multi-catch once we depend on PHP 7.1+.
|
|
catch (SearchApiException | PluginNotFoundException $e) {
|
|
$vars['%index'] = $field->getIndex()->label();
|
|
$vars['%field'] = $field->getPrefixedLabel();
|
|
watchdog_exception('search_api', $e, '%type while adding Views handlers for field %field on index %index: @message in %function (line %line of %file).', $vars);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Determines the mapping of Search API data types to their Views handlers.
|
|
*
|
|
* @return array
|
|
* An associative array with data types as the keys and Views field data
|
|
* definitions as the values. In addition to all normally defined data types,
|
|
* keys can also be "options" for any field with an options list, "entity" for
|
|
* general entity-typed fields or "entity:ENTITY_TYPE" (with "ENTITY_TYPE"
|
|
* being the machine name of an entity type) for entities of that type.
|
|
*
|
|
* @see search_api_views_handler_mapping_alter()
|
|
*/
|
|
function _search_api_views_handler_mapping() {
|
|
$mapping = &drupal_static(__FUNCTION__);
|
|
|
|
if ($mapping === NULL) {
|
|
$mapping = [
|
|
'boolean' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_boolean',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'date' => [
|
|
'argument' => [
|
|
'id' => 'search_api_date',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_date',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'decimal' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
'filter' => 'floatval',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_numeric',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'integer' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
'filter' => 'intval',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_numeric',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'string' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_string',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'text' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_text',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'language' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_language',
|
|
'allow empty' => FALSE,
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'options' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_options',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'entity:taxonomy_term' => [
|
|
'argument' => [
|
|
'id' => 'search_api_term',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_term',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'entity:user' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
'filter' => 'intval',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_user',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
'bundle_of' => [
|
|
'argument' => [
|
|
'id' => 'search_api',
|
|
],
|
|
'filter' => [
|
|
'id' => 'search_api_options',
|
|
],
|
|
'sort' => [
|
|
'id' => 'search_api',
|
|
],
|
|
],
|
|
];
|
|
|
|
$alter_id = 'search_api_views_handler_mapping';
|
|
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.mapping_views_handlers" event instead. See https://www.drupal.org/node/3059866';
|
|
\Drupal::moduleHandler()
|
|
->alterDeprecated($description, $alter_id, $mapping);
|
|
|
|
$event = new MappingViewsHandlersEvent($mapping);
|
|
\Drupal::getContainer()->get('event_dispatcher')
|
|
->dispatch($event, SearchApiEvents::MAPPING_VIEWS_HANDLERS);
|
|
}
|
|
|
|
return $mapping;
|
|
}
|
|
|
|
/**
|
|
* Makes necessary, field-specific adjustments to Views handler definitions.
|
|
*
|
|
* @param string $type
|
|
* The type of field, as defined in _search_api_views_handler_mapping().
|
|
* @param \Drupal\search_api\Item\FieldInterface $field
|
|
* The field whose handler definitions are being created.
|
|
* @param array $definitions
|
|
* The handler definitions for the field, as a reference.
|
|
* @param string|null $sub_type
|
|
* (optional) If applicable, the field's sub-type.
|
|
*
|
|
* @throws \Drupal\search_api\SearchApiException
|
|
* Thrown if the field's data definition could not be retrieved.
|
|
*/
|
|
function _search_api_views_handler_adjustments($type, FieldInterface $field, array &$definitions, string $sub_type = NULL) {
|
|
// By default, all fields can be empty (or at least have to be treated that
|
|
// way by the Search API).
|
|
if (!isset($definitions['filter']['allow empty'])) {
|
|
$definitions['filter']['allow empty'] = TRUE;
|
|
}
|
|
|
|
// For taxonomy term references, set the referenced vocabulary.
|
|
$data_definition = $field->getDataDefinition();
|
|
if ($type == 'entity:taxonomy_term') {
|
|
if (isset($data_definition->getSettings()['handler_settings']['target_bundles'])) {
|
|
$target_bundles = $data_definition->getSettings()['handler_settings']['target_bundles'];
|
|
if (count($target_bundles) == 1) {
|
|
$definitions['filter']['vocabulary'] = reset($target_bundles);
|
|
}
|
|
}
|
|
}
|
|
elseif ($type == 'options') {
|
|
if ($data_definition instanceof FieldItemDataDefinition) {
|
|
// If this is a normal Field API field, dynamically retrieve the options
|
|
// list at query time.
|
|
$field_definition = $data_definition->getFieldDefinition();
|
|
$bundle = $field_definition->getTargetBundle();
|
|
$field_name = $field_definition->getName();
|
|
$entity_type = $field_definition->getTargetEntityTypeId();
|
|
$definitions['filter']['options callback'] = '_search_api_views_get_allowed_values';
|
|
$definitions['filter']['options arguments'] = [$entity_type, $bundle, $field_name];
|
|
}
|
|
else {
|
|
// Otherwise, include the options list verbatim in the Views data, unless
|
|
// it's too big (or doesn't look valid).
|
|
$options = $data_definition->getSetting('allowed_values');
|
|
if (is_array($options) && count($options) <= 50) {
|
|
// Since the Views InOperator filter plugin doesn't allow just including
|
|
// the options in the definition, we use this workaround.
|
|
$definitions['filter']['options callback'] = 'array_filter';
|
|
$definitions['filter']['options arguments'] = [$options];
|
|
}
|
|
}
|
|
}
|
|
elseif ($type === 'bundle_of' && $sub_type) {
|
|
$definitions['filter']['options callback'] = '_search_api_views_get_bundle_names';
|
|
$definitions['filter']['options arguments'] = [$sub_type];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds definitions for our special fields to a Views data table definition.
|
|
*
|
|
* @param array $table
|
|
* The existing Views data table definition.
|
|
* @param \Drupal\search_api\IndexInterface $index
|
|
* The index for which the Views data table is created.
|
|
*/
|
|
function _search_api_views_data_special_fields(array &$table, IndexInterface $index) {
|
|
$id_field = _search_api_views_find_field_alias('search_api_id', $table);
|
|
$table[$id_field]['title'] = t('Item ID');
|
|
$table[$id_field]['help'] = t("The item's internal (Search API-specific) ID");
|
|
$table[$id_field]['field']['id'] = 'standard';
|
|
$table[$id_field]['sort']['id'] = 'search_api';
|
|
if ($id_field != 'search_api_id') {
|
|
$table[$id_field]['real field'] = 'search_api_id';
|
|
}
|
|
|
|
$datasource_field = _search_api_views_find_field_alias('search_api_datasource', $table);
|
|
$table[$datasource_field]['title'] = t('Datasource');
|
|
$table[$datasource_field]['help'] = t('The datasource ID');
|
|
$table[$datasource_field]['argument']['id'] = 'search_api';
|
|
$table[$datasource_field]['argument']['disable_break_phrase'] = TRUE;
|
|
$table[$datasource_field]['field']['id'] = 'standard';
|
|
$table[$datasource_field]['filter']['id'] = 'search_api_datasource';
|
|
$table[$datasource_field]['sort']['id'] = 'search_api';
|
|
if ($datasource_field != 'search_api_datasource') {
|
|
$table[$datasource_field]['real field'] = 'search_api_datasource';
|
|
}
|
|
|
|
$language_field = _search_api_views_find_field_alias('search_api_language', $table);
|
|
$table[$language_field]['title'] = t('Item language');
|
|
$table[$language_field]['help'] = t("The item's language");
|
|
$table[$language_field]['field']['id'] = 'language';
|
|
$table[$language_field]['filter']['id'] = 'search_api_language';
|
|
$table[$language_field]['filter']['allow empty'] = FALSE;
|
|
$table[$language_field]['sort']['id'] = 'search_api';
|
|
if ($language_field != 'search_api_language') {
|
|
$table[$language_field]['real field'] = 'search_api_language';
|
|
}
|
|
|
|
$url_field = _search_api_views_find_field_alias('search_api_url', $table);
|
|
$table[$url_field]['title'] = t('Item URL');
|
|
$table[$url_field]['help'] = t("The item's URL");
|
|
$table[$url_field]['field']['id'] = 'search_api';
|
|
if ($url_field != 'search_api_url') {
|
|
$table[$url_field]['real field'] = 'search_api_url';
|
|
}
|
|
|
|
$relevance_field = _search_api_views_find_field_alias('search_api_relevance', $table);
|
|
$table[$relevance_field]['group'] = t('Search');
|
|
$table[$relevance_field]['title'] = t('Relevance');
|
|
$table[$relevance_field]['help'] = t('The relevance of this search result with respect to the query');
|
|
$table[$relevance_field]['field']['type'] = 'decimal';
|
|
$table[$relevance_field]['field']['id'] = 'numeric';
|
|
$table[$relevance_field]['field']['search_api field'] = 'search_api_relevance';
|
|
$table[$relevance_field]['sort']['id'] = 'search_api';
|
|
if ($relevance_field != 'search_api_relevance') {
|
|
$table[$relevance_field]['real field'] = 'search_api_relevance';
|
|
}
|
|
|
|
$excerpt_field = _search_api_views_find_field_alias('search_api_excerpt', $table);
|
|
$table[$excerpt_field]['group'] = t('Search');
|
|
$table[$excerpt_field]['title'] = t('Excerpt');
|
|
$table[$excerpt_field]['help'] = t('The search result excerpted to show found search terms');
|
|
$table[$excerpt_field]['field']['id'] = 'search_api';
|
|
$table[$excerpt_field]['field']['filter_type'] = 'xss';
|
|
if ($excerpt_field != 'search_api_excerpt') {
|
|
$table[$excerpt_field]['real field'] = 'search_api_excerpt';
|
|
}
|
|
|
|
$fulltext_field = _search_api_views_find_field_alias('search_api_fulltext', $table);
|
|
$table[$fulltext_field]['group'] = t('Search');
|
|
$table[$fulltext_field]['title'] = t('Fulltext search');
|
|
$table[$fulltext_field]['help'] = t('Search several or all fulltext fields at once.');
|
|
$table[$fulltext_field]['filter']['id'] = 'search_api_fulltext';
|
|
$table[$fulltext_field]['argument']['id'] = 'search_api_fulltext';
|
|
if ($fulltext_field != 'search_api_fulltext') {
|
|
$table[$fulltext_field]['real field'] = 'search_api_fulltext';
|
|
}
|
|
|
|
$mlt_field = _search_api_views_find_field_alias('search_api_more_like_this', $table);
|
|
$table[$mlt_field]['group'] = t('Search');
|
|
$table[$mlt_field]['title'] = t('More like this');
|
|
$table[$mlt_field]['help'] = t('Find similar content.');
|
|
$table[$mlt_field]['argument']['id'] = 'search_api_more_like_this';
|
|
if ($mlt_field != 'search_api_more_like_this') {
|
|
$table[$mlt_field]['real field'] = 'search_api_more_like_this';
|
|
}
|
|
|
|
$rendered_field = _search_api_views_find_field_alias('search_api_rendered_item', $table);
|
|
$table[$rendered_field]['group'] = t('Search');
|
|
$table[$rendered_field]['title'] = t('Rendered item');
|
|
$table[$rendered_field]['help'] = t('Renders item in a view mode.');
|
|
$table[$rendered_field]['field']['id'] = 'search_api_rendered_item';
|
|
if ($rendered_field != 'search_api_rendered_item') {
|
|
$table[$rendered_field]['real field'] = 'search_api_rendered_item';
|
|
}
|
|
|
|
// If at least one datasource is based on an entity type that offers
|
|
// operations, we provide them as a field.
|
|
foreach ($index->getDatasources() as $datasource) {
|
|
if ($entity_type_id = $datasource->getEntityTypeId()) {
|
|
$entity_type_id = \Drupal::entityTypeManager()
|
|
->getDefinition($entity_type_id);
|
|
if ($entity_type_id->hasListBuilderClass()) {
|
|
$operations_field = _search_api_views_find_field_alias('search_api_operations', $table);
|
|
$table[$operations_field]['title'] = t('Operations links');
|
|
$table[$operations_field]['help'] = t('Provides links to perform entity operations.');
|
|
$table[$operations_field]['field']['id'] = 'search_api_entity_operations';
|
|
if ($operations_field !== 'search_api_operations') {
|
|
$table[$operations_field]['real field'] = 'search_api_operations';
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If there are taxonomy term references indexed in the index, include the
|
|
// "All taxonomy term fields" contextual filter. We also save for all fields
|
|
// whether they contain only terms of a certain vocabulary, keying that
|
|
// information by vocabulary for later ease of use.
|
|
$vocabulary_fields = [];
|
|
/** @var \Drupal\search_api\Item\Field $field */
|
|
foreach ($index->getFields() as $field_id => $field) {
|
|
// Search for taxonomy term reference fields, and catch problems early on.
|
|
try {
|
|
$property = $field->getDataDefinition();
|
|
$datasource = $field->getDatasource();
|
|
}
|
|
catch (SearchApiException $e) {
|
|
// Will probably cause other problems elsewhere, but here we can just
|
|
// ignore it.
|
|
continue;
|
|
}
|
|
if (!$datasource) {
|
|
continue;
|
|
}
|
|
if ($property->getDataType() !== 'field_item:entity_reference') {
|
|
continue;
|
|
}
|
|
$settings = $property->getSettings();
|
|
if (($settings['target_type'] ?? '') !== 'taxonomy_term') {
|
|
continue;
|
|
}
|
|
$entity_type_id = $datasource->getEntityTypeId();
|
|
// Field instances per bundle can reference different vocabularies, make
|
|
// sure we add them all.
|
|
$bundles = \Drupal::getContainer()->get('entity_type.bundle.info')
|
|
->getBundleInfo($entity_type_id);
|
|
foreach ($bundles as $bundle_id => $bundle) {
|
|
$bundle_fields = \Drupal::getContainer()->get('entity_field.manager')
|
|
->getFieldDefinitions($entity_type_id, $bundle_id);
|
|
// Check if this bundle has the taxonomy entity reference field.
|
|
if (array_key_exists($field->getFieldIdentifier(), $bundle_fields)) {
|
|
/** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
|
|
$field_definition = $bundle_fields[$field->getFieldIdentifier()];
|
|
$bundle_settings = $field_definition->getSettings();
|
|
if (!empty($bundle_settings['handler_settings']['target_bundles'])) {
|
|
foreach ($bundle_settings['handler_settings']['target_bundles'] as $vocabulary_id) {
|
|
$vocabulary_fields[$vocabulary_id][] = $field_id;
|
|
}
|
|
}
|
|
else {
|
|
// If we can't determine the referenced vocabularies, we use the
|
|
// special "" key to mean "any vocabulary".
|
|
$vocabulary_fields[''][] = $field_id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($vocabulary_fields) {
|
|
// Make sure $vocabulary_fields doesn't contain duplicates for fields that
|
|
// are shared between bundles.
|
|
$vocabulary_fields = array_map('array_unique', $vocabulary_fields);
|
|
|
|
$all_terms_field = _search_api_views_find_field_alias('search_api_all_terms', $table);
|
|
$table[$all_terms_field]['group'] = t('Search');
|
|
$table[$all_terms_field]['title'] = t('All taxonomy term fields');
|
|
$table[$all_terms_field]['help'] = t('Search all indexed taxonomy term fields');
|
|
$table[$all_terms_field]['argument']['id'] = 'search_api_all_terms';
|
|
$table[$all_terms_field]['argument']['vocabulary_fields'] = $vocabulary_fields;
|
|
if ($all_terms_field != 'search_api_all_terms') {
|
|
$table[$all_terms_field]['real field'] = 'search_api_all_terms';
|
|
}
|
|
}
|
|
|
|
$bulk_form_field = _search_api_views_find_field_alias('search_api_bulk_form', $table);
|
|
$table[$bulk_form_field] = [
|
|
'title' => t('Bulk update'),
|
|
'help' => t('Allows users to apply an action to one or more items.'),
|
|
'field' => [
|
|
'id' => 'search_api_bulk_form',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Creates a Views table definition for one datasource of an index.
|
|
*
|
|
* @param \Drupal\search_api\Datasource\DatasourceInterface $datasource
|
|
* The datasource for which to create a table definition.
|
|
* @param array $data
|
|
* The existing Views data definitions. Passed by reference so additionally
|
|
* needed tables can be inserted.
|
|
*
|
|
* @return array
|
|
* A Views table definition for the given datasource.
|
|
*/
|
|
function _search_api_views_datasource_table(DatasourceInterface $datasource, array &$data) {
|
|
$datasource_id = $datasource->getPluginId();
|
|
$table = [
|
|
'table' => [
|
|
'group' => t('@datasource datasource', ['@datasource' => $datasource->label()]),
|
|
'index' => $datasource->getIndex()->id(),
|
|
'datasource' => $datasource_id,
|
|
],
|
|
];
|
|
$entity_type_id = $datasource->getEntityTypeId();
|
|
if ($entity_type_id) {
|
|
$table['table']['entity type'] = $entity_type_id;
|
|
$table['table']['entity revision'] = FALSE;
|
|
}
|
|
|
|
_search_api_views_add_handlers_for_properties($datasource->getPropertyDefinitions(), $table, $data);
|
|
|
|
// Prefix the "real field" of each entry with the datasource ID.
|
|
foreach ($table as $key => $definition) {
|
|
if ($key == 'table') {
|
|
continue;
|
|
}
|
|
|
|
$real_field = $definition['real field'] ?? $key;
|
|
$table[$key]['real field'] = Utility::createCombinedId($datasource_id, $real_field);
|
|
|
|
// Relationships sometimes have different real fields set, since they might
|
|
// also include the nested property that contains the actual reference. So,
|
|
// if a "real field" is set for that, we need to adapt it as well.
|
|
if (isset($definition['relationship']['real field'])) {
|
|
$real_field = $definition['relationship']['real field'];
|
|
$table[$key]['relationship']['real field'] = Utility::createCombinedId($datasource_id, $real_field);
|
|
}
|
|
}
|
|
|
|
return $table;
|
|
}
|
|
|
|
/**
|
|
* Creates a Views table definition for an entity type.
|
|
*
|
|
* @param string $entity_type_id
|
|
* The ID of the entity type.
|
|
* @param array $data
|
|
* The existing Views data definitions, passed by reference.
|
|
*
|
|
* @return array
|
|
* A Views table definition for the given entity type. Or an empty array if
|
|
* the entity type could not be found.
|
|
*/
|
|
function _search_api_views_entity_type_table($entity_type_id, array &$data) {
|
|
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
|
|
if (!$entity_type || !$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
|
|
return [];
|
|
}
|
|
|
|
$table = [
|
|
'table' => [
|
|
'group' => t('@entity_type relationship', ['@entity_type' => $entity_type->getLabel()]),
|
|
'entity type' => $entity_type_id,
|
|
'entity revision' => FALSE,
|
|
],
|
|
];
|
|
|
|
$entity_field_manager = \Drupal::getContainer()->get('entity_field.manager');
|
|
$bundle_info = \Drupal::getContainer()->get('entity_type.bundle.info');
|
|
$properties = $entity_field_manager->getBaseFieldDefinitions($entity_type_id);
|
|
foreach (array_keys($bundle_info->getBundleInfo($entity_type_id)) as $bundle_id) {
|
|
$additional = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle_id);
|
|
$properties += $additional;
|
|
}
|
|
_search_api_views_add_handlers_for_properties($properties, $table, $data);
|
|
|
|
return $table;
|
|
}
|
|
|
|
/**
|
|
* Adds field and relationship handlers for the given properties.
|
|
*
|
|
* @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
|
|
* The properties for which handlers should be added.
|
|
* @param array $table
|
|
* The existing Views data table definition, passed by reference.
|
|
* @param array $data
|
|
* The existing Views data definitions, passed by reference.
|
|
*/
|
|
function _search_api_views_add_handlers_for_properties(array $properties, array &$table, array &$data) {
|
|
$entity_reference_types = array_flip([
|
|
'field_item:entity_reference',
|
|
'field_item:image',
|
|
'field_item:file',
|
|
]);
|
|
|
|
foreach ($properties as $property_path => $property) {
|
|
$key = _search_api_views_find_field_alias($property_path, $table);
|
|
$original_property = $property;
|
|
$property = \Drupal::getContainer()
|
|
->get('search_api.fields_helper')
|
|
->getInnerProperty($property);
|
|
|
|
// Add a field handler, if applicable.
|
|
$definition = _search_api_views_get_field_handler_for_property($property, $property_path);
|
|
if ($definition) {
|
|
$table[$key]['field'] = $definition;
|
|
}
|
|
|
|
// For entity-typed properties, also add a relationship to the entity type
|
|
// table.
|
|
if ($property instanceof FieldItemDataDefinition && isset($entity_reference_types[$property->getDataType()])) {
|
|
$entity_type_id = $property->getSetting('target_type');
|
|
if ($entity_type_id) {
|
|
$entity_type_table_key = 'search_api_entity_' . $entity_type_id;
|
|
if (!isset($data[$entity_type_table_key])) {
|
|
// Initialize the table definition before calling
|
|
// _search_api_views_entity_type_table() to avoid an infinite
|
|
// recursion.
|
|
$data[$entity_type_table_key] = [];
|
|
$data[$entity_type_table_key] = _search_api_views_entity_type_table($entity_type_id, $data);
|
|
}
|
|
// Add the relationship only if we have a non-empty table definition.
|
|
if ($data[$entity_type_table_key]) {
|
|
// Get the entity type to determine the label for the relationship.
|
|
$entity_type = \Drupal::entityTypeManager()
|
|
->getDefinition($entity_type_id);
|
|
$entity_type_label = $entity_type ? $entity_type->getLabel() : $entity_type_id;
|
|
$args = [
|
|
'@label' => $entity_type_label,
|
|
'@field_name' => $original_property->getLabel(),
|
|
];
|
|
// Look through the child properties to find the data reference
|
|
// property that should be the "real field" for the relationship.
|
|
// (For Core entity references, this will usually be ":entity".)
|
|
$suffix = '';
|
|
foreach ($property->getPropertyDefinitions() as $name => $nested_property) {
|
|
if ($nested_property instanceof DataReferenceDefinitionInterface) {
|
|
$suffix = ":$name";
|
|
break;
|
|
}
|
|
}
|
|
$table[$key]['relationship'] = [
|
|
'title' => t('@label referenced from @field_name', $args),
|
|
'label' => t('@field_name: @label', $args),
|
|
'help' => $property->getDescription() ?: t('(No description available)'),
|
|
'id' => 'search_api',
|
|
'base' => $entity_type_table_key,
|
|
'entity type' => $entity_type_id,
|
|
'entity revision' => FALSE,
|
|
'real field' => $property_path . $suffix,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!empty($table[$key]) && empty($table[$key]['title'])) {
|
|
$table[$key]['title'] = $original_property->getLabel();
|
|
$table[$key]['help'] = $original_property->getDescription() ?: t('(No description available)');
|
|
if ($key != $property_path) {
|
|
$table[$key]['real field'] = $property_path;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Computes a handler definition for the given property.
|
|
*
|
|
* @param \Drupal\Core\TypedData\DataDefinitionInterface $property
|
|
* The property definition.
|
|
* @param string|null $property_path
|
|
* (optional) The property path of the property. If set, it will be used for
|
|
* Field API fields to set the "field_name" property of the definition.
|
|
*
|
|
* @return array|null
|
|
* Either a Views field handler definition for this property, or NULL if the
|
|
* property shouldn't have one.
|
|
*
|
|
* @see hook_search_api_views_field_handler_mapping_alter()
|
|
*/
|
|
function _search_api_views_get_field_handler_for_property(DataDefinitionInterface $property, $property_path = NULL) {
|
|
$mappings = _search_api_views_get_field_handler_mapping();
|
|
|
|
// First, look for an exact match.
|
|
$data_type = $property->getDataType();
|
|
if (array_key_exists($data_type, $mappings['simple'])) {
|
|
$definition = $mappings['simple'][$data_type];
|
|
}
|
|
else {
|
|
// Then check all the patterns defined by regular expressions, defaulting to
|
|
// the "default" definition.
|
|
$definition = $mappings['default'];
|
|
foreach ($mappings['regex'] as $regex => $mapping_definition) {
|
|
if (preg_match($regex, $data_type)) {
|
|
$definition = $mapping_definition;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Field items have a special handler, but need a fallback handler set to be
|
|
// able to optionally circumvent entity field rendering. That's why we just
|
|
// set the "field_item:…" types to their fallback handlers in
|
|
// _search_api_views_get_field_handler_mapping(), along with non-field item
|
|
// types, and here manually update entity field properties to have the correct
|
|
// definition, with "search_api_field" handler, correct fallback handler and
|
|
// "field_name" and "entity_type" correctly set.
|
|
// Since the Views EntityField handler class doesn't support computed fields,
|
|
// neither can we (easily), so keep the fallback handler as the only
|
|
// definition for those.
|
|
if (isset($definition)
|
|
&& $property instanceof FieldItemDataDefinition
|
|
&& !$property->isComputed()
|
|
&& !$property->getFieldDefinition()->isComputed()) {
|
|
list(, $field_name) = Utility::splitPropertyPath($property_path, TRUE);
|
|
if (!isset($definition['fallback_handler'])) {
|
|
$definition['fallback_handler'] = $definition['id'];
|
|
$definition['id'] = 'search_api_field';
|
|
}
|
|
$definition['field_name'] = $field_name;
|
|
$definition['entity_type'] = $property
|
|
->getFieldDefinition()
|
|
->getTargetEntityTypeId();
|
|
}
|
|
|
|
return $definition;
|
|
}
|
|
|
|
/**
|
|
* Retrieves the field handler mapping used by the Search API Views integration.
|
|
*
|
|
* @return array
|
|
* An associative array with three keys:
|
|
* - simple: An associative array mapping property data types to their field
|
|
* handler definitions.
|
|
* - regex: An array associative array mapping regular expressions for
|
|
* property data types to their field handler definitions, ordered by
|
|
* descending string length of the regular expression.
|
|
* - default: The default definition for data types that match no other field.
|
|
*/
|
|
function _search_api_views_get_field_handler_mapping() {
|
|
$mappings = &drupal_static(__FUNCTION__);
|
|
|
|
if ($mappings === NULL) {
|
|
// First create a plain mapping and pass it to the alter hook.
|
|
$plain_mapping = [];
|
|
|
|
$plain_mapping['*'] = [
|
|
'id' => 'search_api',
|
|
];
|
|
|
|
$text_mapping = [
|
|
'id' => 'search_api',
|
|
'filter_type' => 'xss',
|
|
];
|
|
$plain_mapping['field_item:text_long'] = $text_mapping;
|
|
$plain_mapping['field_item:text_with_summary'] = $text_mapping;
|
|
$plain_mapping['search_api_html'] = $text_mapping;
|
|
unset($text_mapping['filter_type']);
|
|
$plain_mapping['search_api_text'] = $text_mapping;
|
|
|
|
$numeric_mapping = [
|
|
'id' => 'search_api_numeric',
|
|
];
|
|
$plain_mapping['field_item:integer'] = $numeric_mapping;
|
|
$plain_mapping['field_item:list_integer'] = $numeric_mapping;
|
|
$plain_mapping['integer'] = $numeric_mapping;
|
|
$plain_mapping['timespan'] = $numeric_mapping;
|
|
|
|
$float_mapping = [
|
|
'id' => 'search_api_numeric',
|
|
'float' => TRUE,
|
|
];
|
|
$plain_mapping['field_item:decimal'] = $float_mapping;
|
|
$plain_mapping['field_item:float'] = $float_mapping;
|
|
$plain_mapping['field_item:list_float'] = $float_mapping;
|
|
$plain_mapping['decimal'] = $float_mapping;
|
|
$plain_mapping['float'] = $float_mapping;
|
|
|
|
$date_mapping = [
|
|
'id' => 'search_api_date',
|
|
];
|
|
$plain_mapping['field_item:created'] = $date_mapping;
|
|
$plain_mapping['field_item:changed'] = $date_mapping;
|
|
$plain_mapping['datetime_iso8601'] = $date_mapping;
|
|
$plain_mapping['timestamp'] = $date_mapping;
|
|
|
|
$bool_mapping = [
|
|
'id' => 'search_api_boolean',
|
|
];
|
|
$plain_mapping['boolean'] = $bool_mapping;
|
|
$plain_mapping['field_item:boolean'] = $bool_mapping;
|
|
|
|
$language_mapping = [
|
|
'id' => 'language',
|
|
];
|
|
$plain_mapping['language'] = $language_mapping;
|
|
|
|
$ref_mapping = [
|
|
'id' => 'search_api_entity',
|
|
];
|
|
$plain_mapping['field_item:entity_reference'] = $ref_mapping;
|
|
$plain_mapping['field_item:comment'] = $ref_mapping;
|
|
|
|
// Finally, set a default handler for unknown field items.
|
|
$plain_mapping['field_item:*'] = [
|
|
'id' => 'search_api',
|
|
];
|
|
|
|
// Let other modules change or expand this mapping.
|
|
$alter_id = 'search_api_views_field_handler_mapping';
|
|
$description = 'This hook is deprecated in search_api:8.x-1.14 and is removed from search_api:2.0.0. Please use the "search_api.mapping_views_field_handlers" event instead. See https://www.drupal.org/node/3059866';
|
|
\Drupal::moduleHandler()
|
|
->alterDeprecated($description, $alter_id, $plain_mapping);
|
|
|
|
$event = new MappingViewsFieldHandlersEvent($plain_mapping);
|
|
\Drupal::getContainer()->get('event_dispatcher')
|
|
->dispatch($event, SearchApiEvents::MAPPING_VIEWS_FIELD_HANDLERS);
|
|
|
|
// Then create a new, more practical structure, with the mappings grouped by
|
|
// mapping type.
|
|
$mappings = [
|
|
'simple' => [],
|
|
'regex' => [],
|
|
'default' => NULL,
|
|
];
|
|
foreach ($plain_mapping as $type => $definition) {
|
|
if ($type == '*') {
|
|
$mappings['default'] = $definition;
|
|
}
|
|
elseif (strpos($type, '*') === FALSE) {
|
|
$mappings['simple'][$type] = $definition;
|
|
}
|
|
else {
|
|
// Transform the type into a PCRE regular expression, taking care to
|
|
// quote everything except for the wildcards.
|
|
$parts = explode('*', $type);
|
|
// Passing the second parameter to preg_quote() is a bit tricky with
|
|
// array_map(), we need to construct an array of slashes.
|
|
$slashes = array_fill(0, count($parts), '/');
|
|
$parts = array_map('preg_quote', $parts, $slashes);
|
|
// Use the "S" modifier for closer analysis of the pattern, since it
|
|
// might be executed a lot.
|
|
$regex = '/^' . implode('.*', $parts) . '$/S';
|
|
$mappings['regex'][$regex] = $definition;
|
|
}
|
|
}
|
|
// Finally, order the regular expressions descending by their lengths.
|
|
$compare = function ($a, $b) {
|
|
return strlen($b) - strlen($a);
|
|
};
|
|
uksort($mappings['regex'], $compare);
|
|
}
|
|
|
|
return $mappings;
|
|
}
|