You are here

class TrackingHelper in Search API 8

Provides datasource-independent item change tracking functionality.

Hierarchy

Expanded class hierarchy of TrackingHelper

1 file declares its use of TrackingHelper
ReferencedEntitiesReindexingTest.php in tests/src/Kernel/Datasource/ReferencedEntitiesReindexingTest.php
1 string reference to 'TrackingHelper'
search_api.services.yml in ./search_api.services.yml
search_api.services.yml
1 service uses TrackingHelper
search_api.tracking_helper in ./search_api.services.yml
Drupal\search_api\Utility\TrackingHelper

File

src/Utility/TrackingHelper.php, line 28

Namespace

Drupal\search_api\Utility
View source
class TrackingHelper implements TrackingHelperInterface {

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

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

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

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

  /**
   * The cache backend.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * Constructs a new class instance.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager
   *   The language manager.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The event dispatcher.
   * @param \Drupal\search_api\Utility\FieldsHelperInterface $fieldsHelper
   *   The fields helper.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend.
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager, LanguageManagerInterface $languageManager, EventDispatcherInterface $eventDispatcher, FieldsHelperInterface $fieldsHelper, CacheBackendInterface $cache) {
    $this->languageManager = $languageManager;
    $this->entityTypeManager = $entityTypeManager;
    $this->eventDispatcher = $eventDispatcher;
    $this->fieldsHelper = $fieldsHelper;
    $this->cache = $cache;
  }

  /**
   * {@inheritdoc}
   */
  public function trackReferencedEntityUpdate(EntityInterface $entity, bool $deleted = FALSE) {

    /** @var \Drupal\search_api\IndexInterface[] $indexes */
    $indexes = [];
    try {
      $indexes = $this->entityTypeManager
        ->getStorage('search_api_index')
        ->loadMultiple();
    } catch (InvalidPluginDefinitionException $e) {

      // Can't really happen, but play it safe to appease static code analysis.
    } catch (PluginNotFoundException $e) {

      // Can't really happen, but play it safe to appease static code analysis.
    }

    // Original entity, if available.
    $original = $deleted ? NULL : $entity->original ?? NULL;
    foreach ($indexes as $index) {

      // Map of foreign entity relations. Will get lazily populated as soon as
      // we actually need it.
      $map = NULL;
      foreach ($index
        ->getDatasources() as $datasource_id => $datasource) {
        if (!$datasource
          ->canContainEntityReferences()) {
          continue;
        }
        if ($map === NULL) {
          $map = $this
            ->getForeignEntityRelationsMap($index);

          // If there are no foreign entities in the index, no need to continue.
          if (!$map) {
            break 1;
          }
        }
        $item_ids = $datasource
          ->getAffectedItemsForEntityChange($entity, $map, $original);
        if (!empty($item_ids)) {
          $index
            ->trackItemsUpdated($datasource_id, $item_ids);
        }
      }
    }
  }

  /**
   * Analyzes the index fields and constructs a map of entity references.
   *
   * This map tries to record all ways that entities' values are indirectly
   * indexed by the given index. (That is, what items' indexed contents might be
   * affected by a given entity being updated or deleted.)
   *
   * @param \Drupal\search_api\IndexInterface $index
   *   The index for which to create the map.
   *
   * @return array[]
   *   A (numerically keyed) array of foreign relationship mappings. Each
   *   sub-array represents a single known relationship. Such sub-arrays will
   *   have the following structure:
   *   - datasource: (string) The ID of the datasource which contains this
   *     relationship.
   *   - entity_type: (string) The entity type that is referenced from the
   *     index.
   *   - bundles: (string[]) An optional array of particular entity bundles that
   *     are referred to from the index. An empty array here means that the
   *     index refers to all the bundles.
   *   - property_path_to_foreign_entity: (string) Property path where the index
   *     refers to this entity.
   *   - field_name: (string) Name of the field on the referenced entity that is
   *     indexed in the search index.
   */
  protected function getForeignEntityRelationsMap(IndexInterface $index) : array {
    $cid = "search_api:{$index->id()}:foreign_entities_relations_map";
    $cache = $this->cache
      ->get($cid);
    if ($cache) {
      return $cache->data;
    }
    $cacheability = new CacheableMetadata();
    $cacheability
      ->addCacheableDependency($index);
    $data = [];
    foreach ($index
      ->getFields() as $field) {
      try {
        $datasource = $field
          ->getDatasource();
      } catch (SearchApiException $e) {
        continue;
      }
      if (!$datasource) {
        continue;
      }
      $relation_info = [
        'datasource' => $datasource
          ->getPluginId(),
        'entity_type' => NULL,
        'bundles' => NULL,
        'property_path_to_foreign_entity' => NULL,
      ];
      $seen_path_chunks = [];
      $property_definitions = $datasource
        ->getPropertyDefinitions();
      $field_property = Utility::splitPropertyPath($field
        ->getPropertyPath(), FALSE);
      for (; $field_property[0]; $field_property = Utility::splitPropertyPath($field_property[1], FALSE)) {
        $property_definition = $this->fieldsHelper
          ->retrieveNestedProperty($property_definitions, $field_property[0]);
        if (!$property_definition) {

          // Seems like we could not map it from the property path to some Typed
          // Data definition. In the absence of a better alternative, let's
          // simply disregard this field.
          break;
        }
        $seen_path_chunks[] = $field_property[0];
        if ($property_definition instanceof FieldItemDataDefinitionInterface && $property_definition
          ->getFieldDefinition()
          ->isComputed()) {

          // We cannot really deal with computed fields since we have no
          // knowledge about their internal logic. Thus we cannot process
          // this field any further.
          break;
        }
        if ($relation_info['entity_type'] && $property_definition instanceof FieldItemDataDefinitionInterface) {

          // Parent is an entity. Hence this level is fields of the entity.
          $cacheability
            ->addCacheableDependency($property_definition
            ->getFieldDefinition());
          $data[] = $relation_info + [
            'field_name' => $property_definition
              ->getFieldDefinition()
              ->getName(),
          ];
        }
        $entity_reference = $this
          ->isEntityReferenceDataDefinition($property_definition, $cacheability);
        if ($entity_reference) {

          // Unfortunately, the nested "entity" property for entity reference
          // fields comes without a bundles restriction, so we need to copy the
          // bundles information from the level above (on the field itself), if
          // any.
          if ($relation_info['entity_type'] === $entity_reference['entity_type'] && empty($entity_reference['bundles']) && !empty($relation_info['bundles']) && $field_property[0] === 'entity') {
            $entity_reference['bundles'] = $relation_info['bundles'];
          }
          $relation_info = $entity_reference;
          $relation_info['property_path_to_foreign_entity'] = implode(IndexInterface::PROPERTY_PATH_SEPARATOR, $seen_path_chunks);
          $relation_info['datasource'] = $datasource
            ->getPluginId();
        }
        if ($property_definition instanceof ComplexDataDefinitionInterface) {
          $property_definitions = $this->fieldsHelper
            ->getNestedProperties($property_definition);
        }
        else {

          // This item no longer has "nested" properties in its Typed Data
          // definition. Thus we cannot examine it any further than the current
          // point.
          break;
        }
      }
    }

    // Let other modules alter this information, potentially adding more
    // relationships.
    $event = new MappingForeignRelationshipsEvent($index, $data, $cacheability);
    $this->eventDispatcher
      ->dispatch(SearchApiEvents::MAPPING_FOREIGN_RELATIONSHIPS, $event);
    $this->cache
      ->set($cid, $data, $cacheability
      ->getCacheMaxAge(), $cacheability
      ->getCacheTags());
    return $data;
  }

  /**
   * Determines whether the given property is a reference to an entity.
   *
   * @param \Drupal\Core\TypedData\DataDefinitionInterface $property_definition
   *   The property to test.
   * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability
   *   A cache metadata object to track any caching information necessary in
   *   this method call.
   *
   * @return array
   *   This method will return an empty array if $property is not an entity
   *   reference. Otherwise it will return an associative array with the
   *   following structure:
   *   - entity_type: (string) The entity type to which $property refers.
   *   - bundles: (array) A list of bundles to which $property refers. In case
   *     specific bundles cannot be determined or the $property points to all
   *     the bundles, this key will contain an empty array.
   */
  protected function isEntityReferenceDataDefinition(DataDefinitionInterface $property_definition, RefinableCacheableDependencyInterface $cacheability) : array {
    $return = [];
    if ($property_definition instanceof FieldItemDataDefinitionInterface && $property_definition
      ->getFieldDefinition()
      ->getType() === 'entity_reference') {
      $field = $property_definition
        ->getFieldDefinition();
      $cacheability
        ->addCacheableDependency($field);
      $return['entity_type'] = $field
        ->getSetting('target_type');
      $field_settings = $field
        ->getSetting('handler_settings');
      $return['bundles'] = $field_settings['target_bundles'] ?? [];
    }
    elseif ($property_definition instanceof EntityDataDefinitionInterface) {
      $return['entity_type'] = $property_definition
        ->getEntityTypeId();
      $return['bundles'] = $property_definition
        ->getBundles() ?: [];
    }
    return $return;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
TrackingHelper::$cache protected property The cache backend.
TrackingHelper::$entityTypeManager protected property The entity type manager.
TrackingHelper::$eventDispatcher protected property The event dispatcher.
TrackingHelper::$fieldsHelper protected property The fields helper.
TrackingHelper::$languageManager protected property The language manager.
TrackingHelper::getForeignEntityRelationsMap protected function Analyzes the index fields and constructs a map of entity references.
TrackingHelper::isEntityReferenceDataDefinition protected function Determines whether the given property is a reference to an entity.
TrackingHelper::trackReferencedEntityUpdate public function Reacts to an entity being updated or deleted. Overrides TrackingHelperInterface::trackReferencedEntityUpdate
TrackingHelper::__construct public function Constructs a new class instance.