You are here

class ContentEntity in Search API 8

Represents a datasource which exposes the content entities.

Plugin annotation


@SearchApiDatasource(
  id = "entity",
  deriver = "Drupal\search_api\Plugin\search_api\datasource\ContentEntityDeriver"
)

Hierarchy

Expanded class hierarchy of ContentEntity

File

src/Plugin/search_api/datasource/ContentEntity.php, line 49

Namespace

Drupal\search_api\Plugin\search_api\datasource
View source
class ContentEntity extends DatasourcePluginBase implements PluginFormInterface {
  use LoggerTrait;
  use PluginFormTrait;

  /**
   * The key for accessing last tracked ID information in site state.
   *
   * @todo Make protected once we depend on PHP 7.1+.
   */
  const TRACKING_PAGE_STATE_KEY = 'search_api.datasource.entity.last_ids';

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * The entity memory cache.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $memoryCache;

  /**
   * The state service.
   *
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
   */
  protected $entityTypeManager;

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface|null
   */
  protected $entityFieldManager;

  /**
   * The entity display repository manager.
   *
   * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface|null
   */
  protected $entityDisplayRepository;

  /**
   * The entity type bundle info.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|null
   */
  protected $entityTypeBundleInfo;

  /**
   * The typed data manager.
   *
   * @var \Drupal\Core\TypedData\TypedDataManagerInterface|null
   */
  protected $typedDataManager;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface|null
   */
  protected $configFactory;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * The fields helper.
   *
   * @var \Drupal\search_api\Utility\FieldsHelperInterface|null
   */
  protected $fieldsHelper;

  /**
   * {@inheritdoc}
   */
  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
    if (($configuration['#index'] ?? NULL) instanceof IndexInterface) {
      $this
        ->setIndex($configuration['#index']);
      unset($configuration['#index']);
    }

    // Since defaultConfiguration() depends on the plugin definition, we need to
    // override the constructor and set the definition property before calling
    // that method.
    $this->pluginDefinition = $plugin_definition;
    $this->pluginId = $plugin_id;
    $this->configuration = $configuration + $this
      ->defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {

    /** @var static $datasource */
    $datasource = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $datasource
      ->setDatabaseConnection($container
      ->get('database'));
    $datasource
      ->setEntityTypeManager($container
      ->get('entity_type.manager'));
    $datasource
      ->setEntityFieldManager($container
      ->get('entity_field.manager'));
    $datasource
      ->setEntityDisplayRepository($container
      ->get('entity_display.repository'));
    $datasource
      ->setEntityTypeBundleInfo($container
      ->get('entity_type.bundle.info'));
    $datasource
      ->setTypedDataManager($container
      ->get('typed_data_manager'));
    $datasource
      ->setConfigFactory($container
      ->get('config.factory'));
    $datasource
      ->setLanguageManager($container
      ->get('language_manager'));
    $datasource
      ->setFieldsHelper($container
      ->get('search_api.fields_helper'));
    $datasource
      ->setState($container
      ->get('state'));
    $datasource
      ->setEntityMemoryCache($container
      ->get('entity.memory_cache'));
    $datasource
      ->setLogger($container
      ->get('logger.channel.search_api'));
    return $datasource;
  }

  /**
   * Retrieves the database connection.
   *
   * @return \Drupal\Core\Database\Connection
   *   The database connection.
   */
  public function getDatabaseConnection() : Connection {
    return $this->database ?: \Drupal::database();
  }

  /**
   * Sets the database connection.
   *
   * @param \Drupal\Core\Database\Connection $connection
   *   The new database connection.
   *
   * @return $this
   */
  public function setDatabaseConnection(Connection $connection) : self {
    $this->database = $connection;
    return $this;
  }

  /**
   * Retrieves the entity type manager.
   *
   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
   *   The entity type manager.
   */
  public function getEntityTypeManager() {
    return $this->entityTypeManager ?: \Drupal::entityTypeManager();
  }

  /**
   * Retrieves the entity storage.
   *
   * @return \Drupal\Core\Entity\EntityStorageInterface
   *   The entity storage.
   */
  protected function getEntityStorage() {
    return $this
      ->getEntityTypeManager()
      ->getStorage($this
      ->getEntityTypeId());
  }

  /**
   * Returns the definition of this datasource's entity type.
   *
   * @return \Drupal\Core\Entity\EntityTypeInterface
   *   The entity type definition.
   */
  protected function getEntityType() {
    return $this
      ->getEntityTypeManager()
      ->getDefinition($this
      ->getEntityTypeId());
  }

  /**
   * Sets the entity type manager.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The new entity type manager.
   *
   * @return $this
   */
  public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
    $this->entityTypeManager = $entity_type_manager;
    return $this;
  }

  /**
   * Retrieves the entity field manager.
   *
   * @return \Drupal\Core\Entity\EntityFieldManagerInterface
   *   The entity field manager.
   */
  public function getEntityFieldManager() {
    return $this->entityFieldManager ?: \Drupal::service('entity_field.manager');
  }

  /**
   * Sets the entity field manager.
   *
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The new entity field manager.
   *
   * @return $this
   */
  public function setEntityFieldManager(EntityFieldManagerInterface $entity_field_manager) {
    $this->entityFieldManager = $entity_field_manager;
    return $this;
  }

  /**
   * Retrieves the entity display repository.
   *
   * @return \Drupal\Core\Entity\EntityDisplayRepositoryInterface
   *   The entity entity display repository.
   */
  public function getEntityDisplayRepository() {
    return $this->entityDisplayRepository ?: \Drupal::service('entity_display.repository');
  }

  /**
   * Sets the entity display repository.
   *
   * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
   *   The new entity display repository.
   *
   * @return $this
   */
  public function setEntityDisplayRepository(EntityDisplayRepositoryInterface $entity_display_repository) {
    $this->entityDisplayRepository = $entity_display_repository;
    return $this;
  }

  /**
   * Retrieves the entity display repository.
   *
   * @return \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   *   The entity entity display repository.
   */
  public function getEntityTypeBundleInfo() {
    return $this->entityTypeBundleInfo ?: \Drupal::service('entity_type.bundle.info');
  }

  /**
   * Sets the entity type bundle info.
   *
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The new entity type bundle info.
   *
   * @return $this
   */
  public function setEntityTypeBundleInfo(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
    $this->entityTypeBundleInfo = $entity_type_bundle_info;
    return $this;
  }

  /**
   * Retrieves the typed data manager.
   *
   * @return \Drupal\Core\TypedData\TypedDataManagerInterface
   *   The typed data manager.
   */
  public function getTypedDataManager() {
    return $this->typedDataManager ?: \Drupal::typedDataManager();
  }

  /**
   * Sets the typed data manager.
   *
   * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
   *   The new typed data manager.
   *
   * @return $this
   */
  public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
    $this->typedDataManager = $typed_data_manager;
    return $this;
  }

  /**
   * Retrieves the config factory.
   *
   * @return \Drupal\Core\Config\ConfigFactoryInterface
   *   The config factory.
   */
  public function getConfigFactory() {
    return $this->configFactory ?: \Drupal::configFactory();
  }

  /**
   * Retrieves the config value for a certain key in the Search API settings.
   *
   * @param string $key
   *   The key whose value should be retrieved.
   *
   * @return mixed
   *   The config value for the given key.
   */
  protected function getConfigValue($key) {
    return $this
      ->getConfigFactory()
      ->get('search_api.settings')
      ->get($key);
  }

  /**
   * Sets the config factory.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The new config factory.
   *
   * @return $this
   */
  public function setConfigFactory(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
    return $this;
  }

  /**
   * Retrieves the language manager.
   *
   * @return \Drupal\Core\Language\LanguageManagerInterface
   *   The language manager.
   */
  public function getLanguageManager() {
    return $this->languageManager ?: \Drupal::languageManager();
  }

  /**
   * Sets the language manager.
   *
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The new language manager.
   */
  public function setLanguageManager(LanguageManagerInterface $language_manager) {
    $this->languageManager = $language_manager;
  }

  /**
   * Retrieves the fields helper.
   *
   * @return \Drupal\search_api\Utility\FieldsHelperInterface
   *   The fields helper.
   */
  public function getFieldsHelper() {
    return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
  }

  /**
   * Sets the fields helper.
   *
   * @param \Drupal\search_api\Utility\FieldsHelperInterface $fields_helper
   *   The new fields helper.
   *
   * @return $this
   */
  public function setFieldsHelper(FieldsHelperInterface $fields_helper) {
    $this->fieldsHelper = $fields_helper;
    return $this;
  }

  /**
   * Retrieves the state service.
   *
   * @return \Drupal\Core\State\StateInterface
   *   The entity type manager.
   */
  public function getState() {
    return $this->state ?: \Drupal::state();
  }

  /**
   * Sets the state service.
   *
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   *
   * @return $this
   */
  public function setState(StateInterface $state) {
    $this->state = $state;
    return $this;
  }

  /**
   * Retrieves the entity memory cache service.
   *
   * @return \Drupal\Core\Cache\CacheBackendInterface|null
   *   The memory cache, or NULL.
   */
  public function getEntityMemoryCache() {
    return $this->memoryCache;
  }

  /**
   * Sets the entity memory cache service.
   *
   * @param \Drupal\Core\Cache\CacheBackendInterface $memory_cache
   *   The memory cache.
   *
   * @return $this
   */
  public function setEntityMemoryCache(CacheBackendInterface $memory_cache) {
    $this->memoryCache = $memory_cache;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getPropertyDefinitions() {
    $type = $this
      ->getEntityTypeId();
    $properties = $this
      ->getEntityFieldManager()
      ->getBaseFieldDefinitions($type);
    if ($bundles = array_keys($this
      ->getBundles())) {
      foreach ($bundles as $bundle_id) {
        $properties += $this
          ->getEntityFieldManager()
          ->getFieldDefinitions($type, $bundle_id);
      }
    }

    // Exclude properties with custom storage, since we can't extract them
    // currently, due to a shortcoming of Core's Typed Data API. See #2695527.
    // Computed properties should mostly be OK, though, even though they still
    // count as having "custom storage". The "Path" field from the Core module
    // does not work, though, so we explicitly exclude it here to avoid
    // confusion.
    foreach ($properties as $key => $property) {
      if (!$property
        ->isComputed() || $key === 'path') {
        if ($property
          ->getFieldStorageDefinition()
          ->hasCustomStorage()) {
          unset($properties[$key]);
        }
      }
    }
    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public function loadMultiple(array $ids) {
    $allowed_languages = $this
      ->getLanguages();

    // Always allow items with undefined language. (Can be the case when
    // entities are created programmatically.)
    $allowed_languages[LanguageInterface::LANGCODE_NOT_SPECIFIED] = TRUE;
    $allowed_languages[LanguageInterface::LANGCODE_NOT_APPLICABLE] = TRUE;
    $entity_ids = [];
    foreach ($ids as $item_id) {
      $pos = strrpos($item_id, ':');

      // This can only happen if someone passes an invalid ID, since we always
      // include a language code. Still, no harm in guarding against bad input.
      if ($pos === FALSE) {
        continue;
      }
      $entity_id = substr($item_id, 0, $pos);
      $langcode = substr($item_id, $pos + 1);
      if (isset($allowed_languages[$langcode])) {
        $entity_ids[$entity_id][$item_id] = $langcode;
      }
    }

    /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
    $entities = $this
      ->getEntityStorage()
      ->loadMultiple(array_keys($entity_ids));
    $items = [];
    $allowed_bundles = $this
      ->getBundles();
    foreach ($entity_ids as $entity_id => $langcodes) {
      if (empty($entities[$entity_id]) || !isset($allowed_bundles[$entities[$entity_id]
        ->bundle()])) {
        continue;
      }
      foreach ($langcodes as $item_id => $langcode) {
        if ($entities[$entity_id]
          ->hasTranslation($langcode)) {
          $items[$item_id] = $entities[$entity_id]
            ->getTranslation($langcode)
            ->getTypedData();
        }
      }
    }
    return $items;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    $default_configuration = [];
    if ($this
      ->hasBundles()) {
      $default_configuration['bundles'] = [
        'default' => TRUE,
        'selected' => [],
      ];
    }
    if ($this
      ->isTranslatable()) {
      $default_configuration['languages'] = [
        'default' => TRUE,
        'selected' => [],
      ];
    }
    return $default_configuration;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    if ($this
      ->hasBundles() && ($bundles = $this
      ->getEntityBundleOptions())) {
      $form['bundles'] = [
        '#type' => 'details',
        '#title' => $this
          ->t('Bundles'),
        '#open' => TRUE,
      ];
      $form['bundles']['default'] = [
        '#type' => 'radios',
        '#title' => $this
          ->t('Which bundles should be indexed?'),
        '#options' => [
          0 => $this
            ->t('Only those selected'),
          1 => $this
            ->t('All except those selected'),
        ],
        '#default_value' => (int) $this->configuration['bundles']['default'],
      ];
      $form['bundles']['selected'] = [
        '#type' => 'checkboxes',
        '#title' => $this
          ->t('Bundles'),
        '#options' => $bundles,
        '#default_value' => $this->configuration['bundles']['selected'],
        '#size' => min(4, count($bundles)),
        '#multiple' => TRUE,
      ];
    }
    if ($this
      ->isTranslatable()) {
      $form['languages'] = [
        '#type' => 'details',
        '#title' => $this
          ->t('Languages'),
        '#open' => TRUE,
      ];
      $form['languages']['default'] = [
        '#type' => 'radios',
        '#title' => $this
          ->t('Which languages should be indexed?'),
        '#options' => [
          0 => $this
            ->t('Only those selected'),
          1 => $this
            ->t('All except those selected'),
        ],
        '#default_value' => (int) $this->configuration['languages']['default'],
      ];
      $form['languages']['selected'] = [
        '#type' => 'checkboxes',
        '#title' => $this
          ->t('Languages'),
        '#options' => $this
          ->getTranslationOptions(),
        '#default_value' => $this->configuration['languages']['selected'],
        '#multiple' => TRUE,
      ];
    }
    return $form;
  }

  /**
   * Retrieves the available bundles of this entity type as an options list.
   *
   * @return array
   *   An associative array of bundle labels, keyed by the bundle name.
   */
  protected function getEntityBundleOptions() {
    $options = [];
    if ($bundles = $this
      ->getEntityBundles()) {
      foreach ($bundles as $bundle => $bundle_info) {
        $options[$bundle] = Utility::escapeHtml($bundle_info['label']);
      }
    }
    return $options;
  }

  /**
   * Retrieves the available languages of this entity type as an options list.
   *
   * @return array
   *   An associative array of language labels, keyed by the language name.
   */
  protected function getTranslationOptions() {
    $options = [];
    foreach ($this
      ->getLanguageManager()
      ->getLanguages() as $language) {
      $options[$language
        ->getId()] = $language
        ->getName();
    }
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {

    // Filter out empty checkboxes.
    foreach ([
      'bundles',
      'languages',
    ] as $key) {
      if ($form_state
        ->hasValue($key)) {
        $parents = [
          $key,
          'selected',
        ];
        $value = $form_state
          ->getValue($parents, []);
        $value = array_keys(array_filter($value));
        $form_state
          ->setValue($parents, $value);
      }
    }
    $this
      ->setConfiguration($form_state
      ->getValues());
  }

  /**
   * Retrieves the entity from a search item.
   *
   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
   *   An item of this datasource's type.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   The entity object represented by that item, or NULL if none could be
   *   found.
   */
  protected function getEntity(ComplexDataInterface $item) {
    $value = $item
      ->getValue();
    return $value instanceof EntityInterface ? $value : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemId(ComplexDataInterface $item) {
    if ($entity = $this
      ->getEntity($item)) {
      $enabled_bundles = $this
        ->getBundles();
      if (isset($enabled_bundles[$entity
        ->bundle()])) {
        return $entity
          ->id() . ':' . $entity
          ->language()
          ->getId();
      }
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemLabel(ComplexDataInterface $item) {
    if ($entity = $this
      ->getEntity($item)) {
      return $entity
        ->label();
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemBundle(ComplexDataInterface $item) {
    if ($entity = $this
      ->getEntity($item)) {
      return $entity
        ->bundle();
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemUrl(ComplexDataInterface $item) {
    if ($entity = $this
      ->getEntity($item)) {
      if ($entity
        ->hasLinkTemplate('canonical')) {
        return $entity
          ->toUrl('canonical');
      }
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getItemAccessResult(ComplexDataInterface $item, AccountInterface $account = NULL) {
    $entity = $this
      ->getEntity($item);
    if ($entity) {
      return $this
        ->getEntityTypeManager()
        ->getAccessControlHandler($this
        ->getEntityTypeId())
        ->access($entity, 'view', $account, TRUE);
    }
    return AccessResult::neutral('Item is not an entity, so cannot check access');
  }

  /**
   * {@inheritdoc}
   */
  public function getItemIds($page = NULL) {
    return $this
      ->getPartialItemIds($page);
  }

  /**
   * {@inheritdoc}
   */
  public function getEntityTypeId() {
    $plugin_definition = $this
      ->getPluginDefinition();
    return $plugin_definition['entity_type'];
  }

  /**
   * Determines whether the entity type supports bundles.
   *
   * @return bool
   *   TRUE if the entity type supports bundles, FALSE otherwise.
   */
  protected function hasBundles() {
    return $this
      ->getEntityType()
      ->hasKey('bundle');
  }

  /**
   * Determines whether the entity type supports translations.
   *
   * @return bool
   *   TRUE if the entity is translatable, FALSE otherwise.
   */
  protected function isTranslatable() {
    return $this
      ->getEntityType()
      ->isTranslatable();
  }

  /**
   * Retrieves all bundles of this datasource's entity type.
   *
   * @return array
   *   An associative array of bundle infos, keyed by the bundle names.
   */
  protected function getEntityBundles() {
    return $this
      ->hasBundles() ? $this
      ->getEntityTypeBundleInfo()
      ->getBundleInfo($this
      ->getEntityTypeId()) : [];
  }

  /**
   * {@inheritdoc}
   */
  public function getPartialItemIds($page = NULL, array $bundles = NULL, array $languages = NULL) {

    // These would be pretty pointless calls, but for the sake of completeness
    // we should check for them and return early. (Otherwise makes the rest of
    // the code more complicated.)
    if ($bundles === [] && !$languages || $languages === [] && !$bundles) {
      return NULL;
    }
    $entity_type = $this
      ->getEntityType();
    $entity_id = $entity_type
      ->getKey('id');

    // Use a direct database query when an entity has a defined base table. This
    // should prevent performance issues associated with the use of entity query
    // on large data sets. This allows for better control over what tables are
    // included in the query.
    // If no base table is present, then perform an entity query instead.
    if ($entity_type
      ->getBaseTable()) {
      $select = $this
        ->getDatabaseConnection()
        ->select($entity_type
        ->getBaseTable(), 'base_table')
        ->fields('base_table', [
        $entity_id,
      ]);
    }
    else {
      $select = $this
        ->getEntityTypeManager()
        ->getStorage($this
        ->getEntityTypeId())
        ->getQuery();

      // When tracking items, we never want access checks.
      $select
        ->accessCheck(FALSE);
    }

    // Build up the context for tracking the last ID for this batch page.
    $batch_page_context = [
      'index_id' => $this
        ->getIndex()
        ->id(),
      // The derivative plugin ID includes the entity type ID.
      'datasource_id' => $this
        ->getPluginId(),
      'bundles' => $bundles,
      'languages' => $languages,
    ];
    $context_key = Crypt::hashBase64(serialize($batch_page_context));
    $last_ids = $this
      ->getState()
      ->get(self::TRACKING_PAGE_STATE_KEY, []);

    // We want to determine all entities of either one of the given bundles OR
    // one of the given languages. That means we can't just filter for $bundles
    // if $languages is given. Instead, we have to filter for all bundles we
    // might want to include and later sort out those for which we want only the
    // translations in $languages and those (matching $bundles) where we want
    // all (enabled) translations.
    if ($this
      ->hasBundles()) {
      $bundle_property = $entity_type
        ->getKey('bundle');
      if ($bundles && !$languages) {
        $select
          ->condition($bundle_property, $bundles, 'IN');
      }
      else {
        $enabled_bundles = array_keys($this
          ->getBundles());

        // Since this is also called for removed bundles/languages,
        // $enabled_bundles might not include $bundles.
        if ($bundles) {
          $enabled_bundles = array_unique(array_merge($bundles, $enabled_bundles));
        }
        if (count($enabled_bundles) < count($this
          ->getEntityBundles())) {
          $select
            ->condition($bundle_property, $enabled_bundles, 'IN');
        }
      }
    }
    if (isset($page)) {
      $page_size = $this
        ->getConfigValue('tracking_page_size');
      assert($page_size, 'Tracking page size is not set.');

      // If known, use a condition on the last tracked ID for paging instead of
      // the offset, for performance reasons on large sites.
      $offset = $page * $page_size;
      if ($page > 0) {

        // We only handle the case of picking up from where the last page left
        // off. (This will cause an infinite loop if anyone ever wants to index
        // Search API tasks in an index, so check for that to be on the safe
        // side.)
        if (isset($last_ids[$context_key]) && $last_ids[$context_key]['page'] == $page - 1 && $this
          ->getEntityTypeId() !== 'search_api_task') {
          $select
            ->condition($entity_id, $last_ids[$context_key]['last_id'], '>');
          $offset = 0;
        }
      }
      $select
        ->range($offset, $page_size);

      // For paging to reliably work, a sort should be present.
      if ($select instanceof SelectInterface) {
        $select
          ->orderBy($entity_id);
      }
      else {
        $select
          ->sort($entity_id);
      }
    }
    if ($select instanceof SelectInterface) {
      $entity_ids = $select
        ->execute()
        ->fetchCol();
    }
    else {
      $entity_ids = $select
        ->execute();
    }
    if (!$entity_ids) {
      if (isset($page)) {

        // Clean up state tracking of last ID.
        unset($last_ids[$context_key]);
        $this
          ->getState()
          ->set(self::TRACKING_PAGE_STATE_KEY, $last_ids);
      }
      return NULL;
    }

    // Remember the last tracked ID for the next call.
    if (isset($page)) {
      $last_ids[$context_key] = [
        'page' => (int) $page,
        'last_id' => end($entity_ids),
      ];
      $this
        ->getState()
        ->set(self::TRACKING_PAGE_STATE_KEY, $last_ids);
    }

    // For all loaded entities, compute all their item IDs (one for each
    // translation we want to include). For those matching the given bundles (if
    // any), we want to include translations for all enabled languages. For all
    // other entities, we just want to include the translations for the
    // languages passed to the method (if any).
    $item_ids = [];
    $enabled_languages = array_keys($this
      ->getLanguages());

    // As above for bundles, $enabled_languages might not include $languages.
    if ($languages) {
      $enabled_languages = array_unique(array_merge($languages, $enabled_languages));
    }

    // Also, we want to always include entities with unknown language.
    $enabled_languages[] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
    $enabled_languages[] = LanguageInterface::LANGCODE_NOT_APPLICABLE;

    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    foreach ($this
      ->getEntityStorage()
      ->loadMultiple($entity_ids) as $entity_id => $entity) {
      $translations = array_keys($entity
        ->getTranslationLanguages());
      $translations = array_intersect($translations, $enabled_languages);

      // If only languages were specified, keep only those translations matching
      // them. If bundles were also specified, keep all (enabled) translations
      // for those entities that match those bundles.
      if ($languages !== NULL && (!$bundles || !in_array($entity
        ->bundle(), $bundles))) {
        $translations = array_intersect($translations, $languages);
      }
      foreach ($translations as $langcode) {
        $item_ids[] = "{$entity_id}:{$langcode}";
      }
    }
    if (Utility::isRunningInCli()) {

      // When running in the CLI, this might be executed for all entities from
      // within a single process. To avoid running out of memory, reset the
      // static cache after each batch.
      $this
        ->getEntityMemoryCache()
        ->deleteAll();
    }
    return $item_ids;
  }

  /**
   * {@inheritdoc}
   */
  public function getBundles() {
    if (!$this
      ->hasBundles()) {

      // For entity types that have no bundle, return a default pseudo-bundle.
      return [
        $this
          ->getEntityTypeId() => $this
          ->label(),
      ];
    }
    $configuration = $this
      ->getConfiguration();

    // If "default" is TRUE (that is, "All except those selected"),remove all
    // the selected bundles from the available ones to compute the indexed
    // bundles. Otherwise, return all the selected bundles.
    $bundles = [];
    $entity_bundles = $this
      ->getEntityBundles();
    $selected_bundles = array_flip($configuration['bundles']['selected']);
    $function = $configuration['bundles']['default'] ? 'array_diff_key' : 'array_intersect_key';
    $entity_bundles = $function($entity_bundles, $selected_bundles);
    foreach ($entity_bundles as $bundle_id => $bundle_info) {
      $bundles[$bundle_id] = $bundle_info['label'] ?? $bundle_id;
    }
    return $bundles ?: [
      $this
        ->getEntityTypeId() => $this
        ->label(),
    ];
  }

  /**
   * Retrieves the enabled languages.
   *
   * @return \Drupal\Core\Language\LanguageInterface[]
   *   All languages that are enabled for this datasource, keyed by language
   *   code.
   */
  protected function getLanguages() {
    $all_languages = $this
      ->getLanguageManager()
      ->getLanguages();
    if ($this
      ->isTranslatable()) {
      $selected_languages = array_flip($this->configuration['languages']['selected']);
      if ($this->configuration['languages']['default']) {
        return array_diff_key($all_languages, $selected_languages);
      }
      else {
        return array_intersect_key($all_languages, $selected_languages);
      }
    }
    return $all_languages;
  }

  /**
   * {@inheritdoc}
   */
  public function getViewModes($bundle = NULL) {
    return $this
      ->getEntityDisplayRepository()
      ->getViewModeOptions($this
      ->getEntityTypeId());
  }

  /**
   * {@inheritdoc}
   */
  public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL) {
    try {
      if ($entity = $this
        ->getEntity($item)) {
        $langcode = $langcode ?: $entity
          ->language()
          ->getId();
        return $this
          ->getEntityTypeManager()
          ->getViewBuilder($this
          ->getEntityTypeId())
          ->view($entity, $view_mode, $langcode);
      }
    } catch (\Exception $e) {

      // The most common reason for this would be a
      // \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException in
      // getViewBuilder(), because the entity type definition doesn't specify a
      // view_builder class.
    }
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function viewMultipleItems(array $items, $view_mode, $langcode = NULL) {
    try {
      $view_builder = $this
        ->getEntityTypeManager()
        ->getViewBuilder($this
        ->getEntityTypeId());

      // Langcode passed, use that for viewing.
      if (isset($langcode)) {
        $entities = [];
        foreach ($items as $i => $item) {
          if ($entity = $this
            ->getEntity($item)) {
            $entities[$i] = $entity;
          }
        }
        if ($entities) {
          return $view_builder
            ->viewMultiple($entities, $view_mode, $langcode);
        }
        return [];
      }

      // Otherwise, separate the items by language, keeping the keys.
      $items_by_language = [];
      foreach ($items as $i => $item) {
        if ($entity = $this
          ->getEntity($item)) {
          $items_by_language[$entity
            ->language()
            ->getId()][$i] = $entity;
        }
      }

      // Then build the items for each language. We initialize $build beforehand
      // and use array_replace() to add to it so the order stays the same.
      $build = array_fill_keys(array_keys($items), []);
      foreach ($items_by_language as $langcode => $language_items) {
        $build = array_replace($build, $view_builder
          ->viewMultiple($language_items, $view_mode, $langcode));
      }
      return $build;
    } catch (\Exception $e) {

      // The most common reason for this would be a
      // \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException in
      // getViewBuilder(), because the entity type definition doesn't specify a
      // view_builder class.
      return [];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    $this->dependencies = parent::calculateDependencies();
    $this
      ->addDependency('module', $this
      ->getEntityType()
      ->getProvider());
    return $this->dependencies;
  }

  /**
   * {@inheritdoc}
   */
  public function getFieldDependencies(array $fields) {
    $dependencies = [];
    $properties = $this
      ->getPropertyDefinitions();
    foreach ($fields as $field_id => $property_path) {
      $dependencies[$field_id] = $this
        ->getPropertyPathDependencies($property_path, $properties);
    }
    return $dependencies;
  }

  /**
   * {@inheritdoc}
   */
  public function canContainEntityReferences() : bool {
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function getAffectedItemsForEntityChange(EntityInterface $entity, array $foreign_entity_relationship_map, EntityInterface $original_entity = NULL) : array {
    if (!$entity instanceof ContentEntityInterface) {
      return [];
    }
    $ids_to_reindex = [];
    $path_separator = IndexInterface::PROPERTY_PATH_SEPARATOR;
    foreach ($foreign_entity_relationship_map as $relation_info) {

      // Ignore relationships belonging to other datasources.
      if (!empty($relation_info['datasource']) && $relation_info['datasource'] !== $this
        ->getPluginId()) {
        continue;
      }

      // Check whether entity type and (if specified) bundles match the entity.
      if ($relation_info['entity_type'] !== $entity
        ->getEntityTypeId()) {
        continue;
      }
      if (!empty($relation_info['bundles']) && !in_array($entity
        ->bundle(), $relation_info['bundles'])) {
        continue;
      }

      // Maybe this entity belongs to a bundle that does not have this field
      // attached. Hence we have this check to ensure the field is present on
      // this particular entity.
      if (!$entity
        ->hasField($relation_info['field_name'])) {
        continue;
      }
      $items = $entity
        ->get($relation_info['field_name']);

      // We trigger re-indexing if either it is a removed entity or the
      // entity has changed its field value (in case it's an update).
      if (!$original_entity || !$items
        ->equals($original_entity
        ->get($relation_info['field_name']))) {
        $query = $this->entityTypeManager
          ->getStorage($this
          ->getEntityTypeId())
          ->getQuery();
        $query
          ->accessCheck(FALSE);

        // Luckily, to translate from property path to the entity query
        // condition syntax, all we have to do is replace the property path
        // separator with the entity query path separator (a dot) and that's it.
        $property_path = $relation_info['property_path_to_foreign_entity'];
        $property_path = str_replace($path_separator, '.', $property_path);
        $query
          ->condition($property_path, $entity
          ->id());
        try {
          $entity_ids = array_values($query
            ->execute());
        } catch (\Throwable $e) {

          // We don't want to catch all PHP \Error objects thrown, but just the
          // ones caused by #2893747.
          if (!$e instanceof \Exception && (get_class($e) !== \Error::class || $e
            ->getMessage() !== 'Call to a member function getColumns() on bool')) {
            throw $e;
          }
          $vars = [
            '%index' => $this->index
              ->label(),
            '%entity_type' => $entity
              ->getEntityType()
              ->getLabel(),
            '@entity_id' => $entity
              ->id(),
          ];
          try {
            $link = $entity
              ->toLink($this
              ->t('Go to changed %entity_type with ID "@entity_id"', $vars))
              ->toString()
              ->getGeneratedLink();
          } catch (\Throwable $e) {

            // Ignore any errors here, it's not that important that the log
            // message contains a link.
            $link = NULL;
          }
          $this
            ->logException($e, '%type while attempting to find indexed entities referencing changed %entity_type with ID "@entity_id" for index %index: @message in %function (line %line of %file).', $vars, RfcLogLevel::ERROR, $link);
          continue;
        }
        foreach ($entity_ids as $entity_id) {
          foreach ($this
            ->getLanguages() as $language) {
            $ids_to_reindex["{$entity_id}:{$language->getId()}"] = 1;
          }
        }
      }
    }
    return array_keys($ids_to_reindex);
  }

  /**
   * Computes all dependencies of the given property path.
   *
   * @param string $property_path
   *   The property path of the property.
   * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
   *   The properties which form the basis for the property path.
   *
   * @return string[][]
   *   An associative array with the dependencies for the given property path,
   *   mapping dependency types to arrays of dependency names.
   */
  protected function getPropertyPathDependencies($property_path, array $properties) {
    list($key, $nested_path) = Utility::splitPropertyPath($property_path, FALSE);
    if (!isset($properties[$key])) {
      return [];
    }
    $dependencies = new Dependencies();
    $property = $properties[$key];
    if ($property instanceof FieldConfigInterface) {
      $storage = $property
        ->getFieldStorageDefinition();
      if ($storage instanceof FieldStorageConfigInterface) {
        $name = $storage
          ->getConfigDependencyName();
        $dependencies
          ->addDependency($storage
          ->getConfigDependencyKey(), $name);
      }
    }

    // The field might be provided by a module which is not the provider of the
    // entity type, therefore we need to add a dependency on that module.
    if ($property instanceof FieldStorageDefinitionInterface) {
      $dependencies
        ->addDependency('module', $property
        ->getProvider());
    }
    $property = $this
      ->getFieldsHelper()
      ->getInnerProperty($property);
    if ($property instanceof EntityDataDefinitionInterface) {
      $entity_type_definition = $this
        ->getEntityTypeManager()
        ->getDefinition($property
        ->getEntityTypeId());
      if ($entity_type_definition) {
        $module = $entity_type_definition
          ->getProvider();
        $dependencies
          ->addDependency('module', $module);
      }
    }
    if ($nested_path !== NULL && $property instanceof ComplexDataDefinitionInterface) {
      $nested = $this
        ->getFieldsHelper()
        ->getNestedProperties($property);
      $nested_dependencies = $this
        ->getPropertyPathDependencies($nested_path, $nested);
      $dependencies
        ->addDependencies($nested_dependencies);
    }
    return $dependencies
      ->toArray();
  }

  /**
   * {@inheritdoc}
   */
  public static function getIndexesForEntity(ContentEntityInterface $entity) {
    return \Drupal::getContainer()
      ->get('search_api.entity_datasource.tracking_manager')
      ->getIndexesForEntity($entity);
  }

  /**
   * {@inheritdoc}
   */
  public function getListCacheContexts() {
    $contexts = parent::getListCacheContexts();
    $entity_list_contexts = $this
      ->getEntityType()
      ->getListCacheContexts();
    return Cache::mergeContexts($entity_list_contexts, $contexts);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ConfigurablePluginBase::calculatePluginDependencies Deprecated protected function Calculates and adds dependencies of a specific plugin instance.
ConfigurablePluginBase::getConfiguration public function Gets this plugin's configuration. Overrides ConfigurableInterface::getConfiguration
ConfigurablePluginBase::getDescription public function Returns the plugin's description. Overrides ConfigurablePluginInterface::getDescription
ConfigurablePluginBase::getPluginDependencies Deprecated protected function Calculates and returns dependencies of a specific plugin instance.
ConfigurablePluginBase::label public function Returns the label for use on the administration pages. Overrides ConfigurablePluginInterface::label
ConfigurablePluginBase::moduleHandler Deprecated protected function Wraps the module handler.
ConfigurablePluginBase::onDependencyRemoval public function Informs the plugin that some of its dependencies are being removed. Overrides ConfigurablePluginInterface::onDependencyRemoval 5
ConfigurablePluginBase::setConfiguration public function Sets the configuration for this plugin instance. Overrides ConfigurableInterface::setConfiguration 3
ConfigurablePluginBase::themeHandler Deprecated protected function Wraps the theme handler.
ContentEntity::$configFactory protected property The config factory.
ContentEntity::$database protected property The database connection.
ContentEntity::$entityDisplayRepository protected property The entity display repository manager.
ContentEntity::$entityFieldManager protected property The entity field manager.
ContentEntity::$entityTypeBundleInfo protected property The entity type bundle info.
ContentEntity::$entityTypeManager protected property The entity type manager.
ContentEntity::$fieldsHelper protected property The fields helper.
ContentEntity::$languageManager protected property The language manager.
ContentEntity::$memoryCache protected property The entity memory cache.
ContentEntity::$state protected property The state service.
ContentEntity::$typedDataManager protected property The typed data manager.
ContentEntity::buildConfigurationForm public function Form constructor. Overrides PluginFormInterface::buildConfigurationForm
ContentEntity::calculateDependencies public function Calculates dependencies for the configured plugin. Overrides ConfigurablePluginBase::calculateDependencies
ContentEntity::canContainEntityReferences public function Determines whether this datasource can contain entity references. Overrides DatasourcePluginBase::canContainEntityReferences
ContentEntity::create public static function Creates an instance of the plugin. Overrides ConfigurablePluginBase::create
ContentEntity::defaultConfiguration public function Gets default configuration for this plugin. Overrides ConfigurablePluginBase::defaultConfiguration
ContentEntity::getAffectedItemsForEntityChange public function Identifies items affected by a change to a referenced entity. Overrides DatasourcePluginBase::getAffectedItemsForEntityChange
ContentEntity::getBundles public function Retrieves the bundles associated to this datasource. Overrides DatasourcePluginBase::getBundles
ContentEntity::getConfigFactory public function Retrieves the config factory.
ContentEntity::getConfigValue protected function Retrieves the config value for a certain key in the Search API settings.
ContentEntity::getDatabaseConnection public function Retrieves the database connection.
ContentEntity::getEntity protected function Retrieves the entity from a search item.
ContentEntity::getEntityBundleOptions protected function Retrieves the available bundles of this entity type as an options list.
ContentEntity::getEntityBundles protected function Retrieves all bundles of this datasource's entity type.
ContentEntity::getEntityDisplayRepository public function Retrieves the entity display repository.
ContentEntity::getEntityFieldManager public function Retrieves the entity field manager.
ContentEntity::getEntityMemoryCache public function Retrieves the entity memory cache service.
ContentEntity::getEntityStorage protected function Retrieves the entity storage.
ContentEntity::getEntityType protected function Returns the definition of this datasource's entity type.
ContentEntity::getEntityTypeBundleInfo public function Retrieves the entity display repository.
ContentEntity::getEntityTypeId public function Retrieves the entity type ID of items from this datasource, if any. Overrides DatasourcePluginBase::getEntityTypeId
ContentEntity::getEntityTypeManager public function Retrieves the entity type manager.
ContentEntity::getFieldDependencies public function Retrieves any dependencies of the given fields. Overrides DatasourcePluginBase::getFieldDependencies
ContentEntity::getFieldsHelper public function Retrieves the fields helper.
ContentEntity::getIndexesForEntity public static function
ContentEntity::getItemAccessResult public function Checks whether a user has permission to view the given item. Overrides DatasourcePluginBase::getItemAccessResult
ContentEntity::getItemBundle public function Retrieves the item's bundle. Overrides DatasourcePluginBase::getItemBundle
ContentEntity::getItemId public function Retrieves the unique ID of an object from this datasource. Overrides DatasourceInterface::getItemId
ContentEntity::getItemIds public function Returns a list of IDs of items from this datasource. Overrides DatasourcePluginBase::getItemIds
ContentEntity::getItemLabel public function Retrieves a human-readable label for an item. Overrides DatasourcePluginBase::getItemLabel
ContentEntity::getItemUrl public function Retrieves a URL at which the item can be viewed on the web. Overrides DatasourcePluginBase::getItemUrl
ContentEntity::getLanguageManager public function Retrieves the language manager.
ContentEntity::getLanguages protected function Retrieves the enabled languages.
ContentEntity::getListCacheContexts public function Returns the list cache contexts associated with this datasource. Overrides DatasourcePluginBase::getListCacheContexts
ContentEntity::getPartialItemIds public function
ContentEntity::getPropertyDefinitions public function Retrieves the properties exposed by the underlying complex data type. Overrides DatasourcePluginBase::getPropertyDefinitions
ContentEntity::getPropertyPathDependencies protected function Computes all dependencies of the given property path.
ContentEntity::getState public function Retrieves the state service.
ContentEntity::getTranslationOptions protected function Retrieves the available languages of this entity type as an options list.
ContentEntity::getTypedDataManager public function Retrieves the typed data manager.
ContentEntity::getViewModes public function Returns the available view modes for this datasource. Overrides DatasourcePluginBase::getViewModes
ContentEntity::hasBundles protected function Determines whether the entity type supports bundles.
ContentEntity::isTranslatable protected function Determines whether the entity type supports translations.
ContentEntity::loadMultiple public function Loads multiple items. Overrides DatasourcePluginBase::loadMultiple
ContentEntity::setConfigFactory public function Sets the config factory.
ContentEntity::setDatabaseConnection public function Sets the database connection.
ContentEntity::setEntityDisplayRepository public function Sets the entity display repository.
ContentEntity::setEntityFieldManager public function Sets the entity field manager.
ContentEntity::setEntityMemoryCache public function Sets the entity memory cache service.
ContentEntity::setEntityTypeBundleInfo public function Sets the entity type bundle info.
ContentEntity::setEntityTypeManager public function Sets the entity type manager.
ContentEntity::setFieldsHelper public function Sets the fields helper.
ContentEntity::setLanguageManager public function Sets the language manager.
ContentEntity::setState public function Sets the state service.
ContentEntity::setTypedDataManager public function Sets the typed data manager.
ContentEntity::submitConfigurationForm public function Form submission handler. Overrides PluginFormTrait::submitConfigurationForm
ContentEntity::TRACKING_PAGE_STATE_KEY constant The key for accessing last tracked ID information in site state.
ContentEntity::viewItem public function Returns the render array for the provided item and view mode. Overrides DatasourcePluginBase::viewItem
ContentEntity::viewMultipleItems public function Returns the render array for the provided items and view mode. Overrides DatasourcePluginBase::viewMultipleItems
ContentEntity::__construct public function Constructs a \Drupal\Component\Plugin\PluginBase object. Overrides IndexPluginBase::__construct
DatasourcePluginBase::checkItemAccess public function Checks whether a user has permission to view the given item. Overrides DatasourceInterface::checkItemAccess
DatasourcePluginBase::getItemLanguage public function Retrieves the item's language. Overrides DatasourceInterface::getItemLanguage 1
DatasourcePluginBase::load public function Loads an item. Overrides DatasourceInterface::load
DependencySerializationTrait::$_entityStorages protected property An array of entity type IDs keyed by the property name of their storages.
DependencySerializationTrait::$_serviceIds protected property An array of service IDs keyed by property name used for serialization.
DependencySerializationTrait::__sleep public function 1
DependencySerializationTrait::__wakeup public function 2
DependencyTrait::$dependencies protected property The object's dependencies.
DependencyTrait::addDependencies protected function Adds multiple dependencies.
DependencyTrait::addDependency protected function Adds a dependency.
HideablePluginBase::isHidden public function Determines whether this plugin should be hidden in the UI. Overrides HideablePluginInterface::isHidden 1
IndexPluginBase::$index protected property The index this processor is configured for.
IndexPluginBase::getIndex public function Retrieves the index this plugin is configured for. Overrides IndexPluginInterface::getIndex
IndexPluginBase::setIndex public function Sets the index this plugin is configured for. Overrides IndexPluginInterface::setIndex
LoggerTrait::$logger protected property The logging channel to use.
LoggerTrait::getLogger public function Retrieves the logger.
LoggerTrait::logException protected function Logs an exception.
LoggerTrait::setLogger public function Sets the logger.
MessengerTrait::$messenger protected property The messenger. 29
MessengerTrait::messenger public function Gets the messenger. 29
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 3
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginDependencyTrait::calculatePluginDependencies protected function Calculates and adds dependencies of a specific plugin instance. Aliased as: traitCalculatePluginDependencies 1
PluginDependencyTrait::getPluginDependencies protected function Calculates and returns dependencies of a specific plugin instance. Aliased as: traitGetPluginDependencies
PluginDependencyTrait::moduleHandler protected function Wraps the module handler. Aliased as: traitModuleHandler 1
PluginDependencyTrait::themeHandler protected function Wraps the theme handler. Aliased as: traitThemeHandler 1
PluginFormTrait::validateConfigurationForm public function Form validation handler. 2
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.