You are here

search_api.views.inc in Search API 8

Views hook implementations for the Search API module.

File

search_api.views.inc
View source
<?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)) {
        list($type, $sub_type) = $type;
      }
      if (isset($mapping[$type])) {
        _search_api_views_handler_adjustments($type, $field, $mapping[$type], $sub_type);
        return $mapping[$type];
      }
    }
  } catch (SearchApiException $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);
  } catch (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(SearchApiEvents::MAPPING_VIEWS_HANDLERS, $event);
  }
  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';
  }
  $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(SearchApiEvents::MAPPING_VIEWS_FIELD_HANDLERS, $event);

    // 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;
}

Functions

Namesort descending Description
search_api_views_data Implements hook_views_data().
search_api_views_plugins_cache_alter Implements hook_views_plugins_cache_alter().
search_api_views_plugins_row_alter Implements hook_views_plugins_row_alter().
_search_api_views_add_handlers_for_properties Adds field and relationship handlers for the given properties.
_search_api_views_datasource_table Creates a Views table definition for one datasource of an index.
_search_api_views_data_special_fields Adds definitions for our special fields to a Views data table definition.
_search_api_views_entity_type_table Creates a Views table definition for an entity type.
_search_api_views_find_field_alias Finds an unused field alias for a field in a Views table definition.
_search_api_views_get_field_handler_for_property Computes a handler definition for the given property.
_search_api_views_get_field_handler_mapping Retrieves the field handler mapping used by the Search API Views integration.
_search_api_views_get_handlers Returns the Views handlers to use for a given field.
_search_api_views_handler_adjustments Makes necessary, field-specific adjustments to Views handler definitions.
_search_api_views_handler_mapping Determines the mapping of Search API data types to their Views handlers.