external_entities-8.x-2.x-dev/src/ExternalEntityStorage.php

src/ExternalEntityStorage.php
<?php

namespace Drupal\external_entities;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Component\Utility\DeprecationHelper;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Utility\Error;
use Drupal\external_entities\Entity\ConfigurableExternalEntityTypeInterface;
use Drupal\external_entities\Entity\ExternalEntityInterface;
use Drupal\external_entities\Event\ExternalEntitiesEvents;
use Drupal\external_entities\Event\ExternalEntityMapRawDataEvent;
use Drupal\external_entities\Event\ExternalEntityTransformRawDataEvent;
use Drupal\external_entities\FieldMapper\FieldMapperInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Defines the storage handler class for external entities.
 *
 * This extends the base storage class, adding required special handling for
 * e entities.
 */
class ExternalEntityStorage extends ContentEntityStorageBase implements ExternalEntityStorageInterface {

  /**
   * The external entity type this field mapper is configured for.
   *
   * @var \Drupal\external_entities\Entity\ConfigurableExternalEntityTypeInterface
   */
  protected $externalEntityType;

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

  /**
   * The field mapper manager.
   *
   * @var \Drupal\Component\Plugin\PluginManagerInterface
   */
  protected $fieldMapperManager;

  /**
   * An associative array of field mapper keyed by their field name.
   *
   * @var \Drupal\external_entities\FieldMapper\FieldMapperInterface[]
   */
  protected $fieldMappers;

  /**
   * The external storage client manager.
   *
   * @var \Drupal\Component\Plugin\PluginManagerInterface
   */
  protected $storageClientManager;

  /**
   * The logger channel factory.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerChannelFactory;

  /**
   * The field mapper plugin logger channel.
   *
   * @var \Drupal\Core\Logger\LoggerChannel
   */
  protected $logger;

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

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

  /**
   * The external entity identifier field name.
   *
   * @var string
   */
  protected $xnttIdFieldName;

  /**
   * The default language code for translatable external entities.
   *
   * @var string|null
   */
  protected $defaultLangcode;

  /**
   * Prefix to detect translation data in remote source raw data.
   *
   * If found the trailing langcode after the prefix is used to populate an
   * entity translation.
   *
   * @deprecated in external_entities:3.0.0-beta6 and is removed from
   * external_entities:3.0.0. Use external entity type language field mapper
   * overrides instead.
   *
   * @see https://www.drupal.org/project/external_entities/issues/3506455
   */
  public const EXTERNAL_ENTITY_TRANSLATION_SUB_FIELD_PREFIX = '__external_entity_translation__';

  /**
   * Constructs a new ExternalEntityStorage object.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $external_entity_type
   *   A configuration array containing information about the plugin instance.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend to be used.
   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
   *   The memory cache backend.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
   *   The entity type bundle info.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   * @param \Drupal\Component\Plugin\PluginManagerInterface $field_mapper_manager
   *   The field mapper manager.
   * @param \Drupal\Component\Plugin\PluginManagerInterface $storage_client_manager
   *   The storage client manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger channel factory.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   */
  public function __construct(
    EntityTypeInterface $external_entity_type,
    EntityFieldManagerInterface $entity_field_manager,
    CacheBackendInterface $cache,
    MemoryCacheInterface $memory_cache,
    EntityTypeBundleInfoInterface $entity_type_bundle_info,
    EntityTypeManagerInterface $entity_type_manager,
    PluginManagerInterface $field_mapper_manager,
    PluginManagerInterface $storage_client_manager,
    LoggerChannelFactoryInterface $logger_factory,
    EventDispatcherInterface $event_dispatcher,
    TimeInterface $time,
  ) {
    parent::__construct(
      $external_entity_type,
      $entity_field_manager,
      $cache,
      $memory_cache,
      $entity_type_bundle_info
    );
    $this->entityTypeManager = $entity_type_manager;
    $this->fieldMapperManager = $field_mapper_manager;
    $this->storageClientManager = $storage_client_manager;
    $this->loggerChannelFactory = $logger_factory;
    $this->logger = $this->loggerChannelFactory->get('external_entities');
    $this->eventDispatcher = $event_dispatcher;
    $this->time = $time;
    $this->defaultLangcode = $this->languageManager()->getDefaultLanguage()->getId();
  }

  /**
   * {@inheritdoc}
   */
  public static function createInstance(
    ContainerInterface $container,
    EntityTypeInterface $entity_type,
  ) {
    return new static(
      $entity_type,
      $container->get('entity_field.manager'),
      $container->get('cache.entity'),
      $container->get('entity.memory_cache'),
      $container->get('entity_type.bundle.info'),
      $container->get('entity_type.manager'),
      $container->get('plugin.manager.external_entities.field_mapper'),
      $container->get('plugin.manager.external_entities.storage_client'),
      $container->get('logger.factory'),
      $container->get('event_dispatcher'),
      $container->get('datetime.time')
    );
  }

  /**
   * Gets the language manager.
   *
   * @return \Drupal\Core\Language\LanguageManagerInterface
   *   The language manager.
   */
  protected function languageManager() :LanguageManagerInterface {
    return \Drupal::languageManager();
  }

  /**
   * {@inheritdoc}
   */
  public function getExternalEntityType() :ConfigurableExternalEntityTypeInterface {
    if (empty($this->externalEntityType)) {
      $this->externalEntityType = $this->entityTypeManager
        ->getStorage('external_entity_type')
        ->load($this->getEntityTypeId());
    }
    return $this->externalEntityType;
  }

  /**
   * Returns the field mapper associated to the given field.
   *
   * @param string $field_name
   *   Field machine name.
   * @param string|null $langcode
   *   The language code. If null, use default.
   *
   * @return \Drupal\external_entities\FieldMapper\FieldMapperInterface|null
   *   The requested field mapper pluginor NULL if not available.
   */
  protected function getFieldMapper(
    string $field_name,
    ?string $langcode = NULL,
  ) :FieldMapperInterface|null {
    if (empty($langcode)) {
      $field_mapper_key = $field_name;
    }
    else {
      $field_mapper_key = $langcode . '-' . $field_name;
    }
    if (!array_key_exists($field_mapper_key, $this->fieldMappers ?? [])) {
      $this->fieldMappers[$field_mapper_key] =
        $this->getExternalEntityType()->getFieldMapper($field_name, $langcode);
    }
    return $this->fieldMappers[$field_mapper_key];
  }

  /**
   * Acts on entities before they are deleted and before hooks are invoked.
   *
   * Used before the entities are deleted and before invoking the delete hook.
   *
   * @param \Drupal\Core\Entity\EntityInterface[] $entities
   *   An array of entities.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function preDelete(array $entities) {
    if ($this->getExternalEntityType()->isReadOnly()) {
      throw new EntityStorageException(
        'Cannot delete read-only external entities.'
      );
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doDelete($entities) {
    // Do the actual delete.
    foreach ($entities as $entity) {
      $this
        ->getExternalEntityType()
        ->getDataAggregator($entity->language()->getId())
        ->delete($entity);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadMultiple(?array $ids = NULL) {
    // Attempt to load entities from the persistent cache. This will remove IDs
    // that were loaded from $ids.
    $entities_from_cache = $this->getFromPersistentCache($ids);

    // Load any remaining entities from the external storage.
    if ($entities_from_storage = $this->getFromExternalStorage($ids)) {
      $this->invokeStorageLoadHook($entities_from_storage);
      $this->setPersistentCache($entities_from_storage);
    }

    /** @var \Drupal\external_entities\Entity\ExternalEntityInterface $external_entity */
    $entities = $entities_from_cache + $entities_from_storage;

    // Map annotation fields to annotatable external entities.
    if ($this->getExternalEntityType()->isAnnotatable()) {
      $annotations = $this->getAnnotations(array_keys($entities));
      foreach ($entities as $external_entity) {
        if (!empty($annotations[$external_entity->id()])) {
          $external_entity->mapAnnotationFields($annotations[$external_entity->id()]);
        }
      }
    }

    return $entities;
  }

  /**
   * {@inheritdoc}
   */
  protected function setPersistentCache($entities) {
    if (!$this->entityType->isPersistentlyCacheable()) {
      return;
    }

    $cache_tags = $this->getExternalEntityType()->getCacheTags();

    foreach ($entities as $id => $entity) {
      $max_age = $this->getExternalEntityType()->getPersistentCacheMaxAge();
      $entity_cache_tags = Cache::mergeTags($cache_tags, $entity->getCacheTags());
      $expire = $max_age === Cache::PERMANENT
        ? Cache::PERMANENT
        : $this->time->getRequestTime() + $max_age;
      $this->cacheBackend->set($this->buildCacheId($id), $entity, $expire, $entity_cache_tags);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getAnnotations(array $ids): array {
    $external_entity_type = $this->getExternalEntityType();
    if (empty($ids) || !$external_entity_type->isAnnotatable()) {
      return [];
    }

    $properties = [
      $external_entity_type->getAnnotationFieldName() => $ids,
    ];

    $bundle_key = $this
      ->entityTypeManager
      ->getDefinition($external_entity_type->getAnnotationEntityTypeId())
      ->getKey('bundle');
    if ($bundle_key) {
      $properties[$bundle_key] = $external_entity_type->getAnnotationBundleId();
    }

    $annotations = $this
      ->entityTypeManager
      ->getStorage($external_entity_type->getAnnotationEntityTypeId())
      ->loadByProperties($properties);
    $annotations_per_external_entity = [];
    foreach ($annotations as $annotation) {
      $annotations_per_external_entity[$annotation->get($external_entity_type->getAnnotationFieldName())->target_id] = $annotation;
    }

    return $annotations_per_external_entity;
  }

  /**
   * Acts on an entity before the presave hook is invoked.
   *
   * Used before the entity is saved and before invoking the presave hook.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity object.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function preSave(EntityInterface $entity) {
    $external_entity_type = $this->getExternalEntityType();
    if ($external_entity_type->isReadOnly() && !$external_entity_type->isAnnotatable()) {
      throw new EntityStorageException('Cannot save read-only external entities.');
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function doSave($id, EntityInterface $entity) {
    /** @var \Drupal\external_entities\Entity\ExternalEntityInterface $entity */
    $result = FALSE;

    $external_entity_type = $this->getExternalEntityType();

    if (8 <= $external_entity_type->getDebugLevel()) {
      // No save in debug levels 8 and above.
      $this->logger->debug(
        'External entity to be saved:'
        . print_r($entity->toRawData(), TRUE)
      );
    }
    elseif (!$external_entity_type->isReadOnly()) {
      // Only save if not read-only.
      $result = parent::doSave($id, $entity);
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  protected function getQueryServiceName() {
    return 'entity.query.external';
  }

  /**
   * {@inheritdoc}
   */
  protected function has($id, EntityInterface $entity) {
    return !$entity->isNew();
  }

  /**
   * {@inheritdoc}
   */
  protected function doDeleteFieldItems($entities) {
  }

  /**
   * {@inheritdoc}
   */
  protected function doDeleteRevisionFieldItems(
    ContentEntityInterface $revision,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public function loadMultipleRevisions(array $revision_ids) {
    return $this->doLoadMultiple($revision_ids);
  }

  /**
   * {@inheritdoc}
   */
  public function loadUnchanged($id) {
    /** @var \Drupal\external_entities\Entity\ExternalEntityInterface $entity */
    $entity = parent::loadUnchanged($id);

    if ($entity) {
      // Load the unchanged annotation as well.
      $annotation = $entity->getAnnotation();
      if ($annotation) {
        $unchanged_annotation = $this
          ->entityTypeManager
          ->getStorage($annotation->getEntityTypeId())
          ->loadUnchanged($annotation->id());
        $entity->mapAnnotationFields($unchanged_annotation);
      }
    }

    return $entity;
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadRevisionFieldItems($revision_id) {
  }

  /**
   * {@inheritdoc}
   */
  protected function doLoadMultipleRevisionsFieldItems($revision_ids) {
    // Not managed.
    return [];
  }

  /**
   * {@inheritdoc}
   */
  protected function doSaveFieldItems(
    ContentEntityInterface $entity,
    array $names = [],
  ) {
    // If this external entity was automatically saved because of a change in
    // the annotation, there's no need to update the external storage.
    if (!empty($entity->{ExternalEntityInterface::EXTERNAL_ENTITY_AUTO_SAVE_INDUCED_BY_ANNOTATION_CHANGE_PROPERTY})) {
      return;
    }

    // Get language.
    $langcode = $entity->language()->getId();
    // Handle multi-storages.
    $status = $this
      ->getExternalEntityType()
      ->getDataAggregator($langcode)
      ->save($entity);

    // For newly created external entities, get new id.
    if ((SAVED_NEW == $status) && $entity->isNew()) {
      $id_field = $this->idKey;
      // Try to directly get its corresponding source raw data field.
      $id_field_mapper = $this->getFieldMapper($id_field, $langcode);
      // Note: if we don't have a field mapper for the id field, there is a
      // problem!
      if (empty($id_field_mapper)) {
        throw new EntityStorageException(
          'Cannot process External Entity "'
          . $this->getExternalEntityType()->getDerivedEntityTypeId()
          . '": no field mapper for identifier field.'
        );
      }
      $raw_data = $entity->toRawData();
      $source_id_field = $id_field_mapper->getMappedSourceFieldName();
      if (empty($source_id_field)) {
        // Not available, need to map.
        $id = $id_field_mapper
          ->extractFieldValuesFromRawData($raw_data)[$id_field][0]['value'];
      }
      else {
        $id = $raw_data[$source_id_field];
      }
      // Allow 0 as a valid id for remote data.
      if (!empty($id) || ('0' === $id) || (0 === $id) || (0. === $id)) {
        $entity->{$this->idKey} = $id;
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function readFieldItemsToPurge(
    FieldDefinitionInterface $field_definition,
    $batch_size,
  ) {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  protected function purgeFieldItems(
    ContentEntityInterface $entity,
    FieldDefinitionInterface $field_definition,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public function countFieldData($storage_definition, $as_bool = FALSE) {
    return $as_bool ? 0 : FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function hasData() {
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  protected function cleanIds(array $ids, $entity_key = 'id') {
    // getFieldStorageDefinitions() is used instead of
    // getActiveFieldStorageDefinitions() because the latter fails to return
    // all definitions in the event an external entity is not cached locally.
    $definitions = $this->entityFieldManager->getFieldStorageDefinitions(
      $this->entityTypeId
    );
    $field_name = $this->entityType->getKey($entity_key);
    if ($field_name && $definitions[$field_name]->getType() == 'integer') {
      $ids = array_filter($ids, function ($id) {
        return is_numeric($id) && $id == (int) $id;
      });
      $ids = array_map('intval', $ids);
    }
    return $ids;
  }

  /**
   * {@inheritdoc}
   */
  public function countExternalEntities(array $parameters) :int {
    // Count aggregated data from external sources.
    $count = $this
      ->getExternalEntityType()
      ->getDataAggregator()
      ->countQuery($parameters);
    return $count;
  }

  /**
   * {@inheritdoc}
   */
  public function getRawDataFromExternalStorage(
    ?array $ids = NULL,
    ?string $langcode = NULL,
  ) :array {
    $all_raw_data = [];
    if (!empty($ids)) {
      // Sanitize IDs. Before feeding ID array into buildQuery, check whether
      // it is empty as this would load all entities.
      $ids = $this->cleanIds($ids);
    }

    if ($ids === NULL || $ids) {
      // Load aggregated data from external sources.
      $loaded_raw_data = $this
        ->getExternalEntityType()
        ->getDataAggregator($langcode)
        ->loadMultiple($ids);

      foreach ($loaded_raw_data as $id => $raw_data) {
        // Ensure $raw_data is an array.
        if (!is_array($raw_data)) {
          $raw_data = [$raw_data];
        }
        // Allow other modules to perform custom mapping logic.
        $event = new ExternalEntityTransformRawDataEvent($this, $raw_data);
        $this->eventDispatcher->dispatch($event, ExternalEntitiesEvents::TRANSFORM_RAW_DATA);
        $raw_data = $event->getRawData();
        if (empty($raw_data)) {
          continue;
        }
        $all_raw_data[$id] = $raw_data;
      }
    }

    return $all_raw_data;
  }

  /**
   * {@inheritdoc}
   */
  public function queryRawDataFromExternalStorage(
    array $parameters = [],
    array $sorts = [],
    ?int $start = NULL,
    ?int $length = NULL,
    bool $id_only = FALSE,
  ) :array {
    // Fetch raw data from external sources.
    // Query aggregated data from external sources.
    $matching_raw_data = $this
      ->getExternalEntityType()
      ->getDataAggregator()
      ->query(
        $parameters,
        $sorts,
        $start,
        $length
      );

    // Allow other modules to perform custom mapping logic.
    $all_raw_data = [];
    foreach ($matching_raw_data as $raw_data) {
      $event = new ExternalEntityTransformRawDataEvent($this, $raw_data);
      $this->eventDispatcher->dispatch($event, ExternalEntitiesEvents::TRANSFORM_RAW_DATA);
      $raw_data = $event->getRawData();
      if (empty($raw_data)) {
        continue;
      }
      $all_raw_data[] = $raw_data;
    }

    // Remap by id.
    $all_raw_data_by_id = [];
    // Get ID field.
    $id_field = $this->idKey;
    // Get id field mapper.
    $id_field_mapper = $this->getFieldMapper($id_field);
    // Note: if we don't have a field mapper for the id field, there is a
    // problem!
    if (empty($id_field_mapper)) {
      throw new EntityStorageException(
        'Cannot process External Entity "'
        . $this->getExternalEntityType()->getDerivedEntityTypeId()
        . '": no field mapper for identifier field.'
      );
    }
    // Try to directly get its corresponding source raw data field.
    // @todo Sub-fields could be mapped directly using NestedArray lib.
    $source_id_field = $id_field_mapper->getMappedSourceFieldName();
    if (empty($source_id_field)
        || ((FALSE !== strpos($source_id_field, '.'))
          && ('field' != $id_field_mapper->getPropertyMapper('value')->getPluginId()))
    ) {
      // Not available, need to map.
      foreach ($all_raw_data as $raw_data) {
        $mapped_values = $id_field_mapper->extractFieldValuesFromRawData($raw_data);
        $id = $mapped_values[0]['value'] ?? NULL;
        if ((is_string($id) && ($id != '')) || (is_int($id))) {
          if ($id_only) {
            $all_raw_data_by_id[$id] = $id;
          }
          else {
            $all_raw_data_by_id[$id] = $raw_data;
          }
        }
        else {
          $this->logger->warning(
            "Unable to map external entity identifier (@id_field). Mapped id is not valid:\n@id",
            [
              '@id_field' => $id_field,
              '@id' => print_r($id, TRUE),
            ]
          );
        }
      }
    }
    else {
      // Directly get id field value.
      foreach ($all_raw_data as $raw_data) {
        $id = $raw_data[$source_id_field];
        if ((is_string($id) && ($id != '')) || (is_int($id))) {
          if ($id_only) {
            $all_raw_data_by_id[$id] = $id;
          }
          else {
            $all_raw_data_by_id[$id] = $raw_data;
          }
        }
        else {
          $this->logger->warning(
            "Unable to map raw identifier (@source_id_field). Returned id is not valid:\n@id",
            [
              '@source_id_field' => $source_id_field,
              '@id' => print_r($id, TRUE),
            ]
          );
        }
      }
    }

    return $all_raw_data_by_id;
  }

  /**
   * {@inheritdoc}
   */
  public function extractEntityValuesFromRawData(
    array $raw_data,
    array $field_names = [],
    ?string $langcode = NULL,
  ) :array {
    // Only keep $langcode value if not default.
    if (!empty($langcode)
      && (($langcode == LanguageInterface::LANGCODE_DEFAULT)
        || ($langcode == $this->defaultLangcode))
    ) {
      $langcode = NULL;
    }

    // Prepare a context.
    $context = [];
    $entity_array = [];
    if (empty($field_names)) {
      // Get all fields.
      $field_names = array_keys($this->getExternalEntityType()->getMappableFields());
    }

    // Map current external entity raw data to an entity array.
    foreach ($field_names as $field_name) {
      // For each mapped field, get the field mapper to use.
      $field_mapper = $this->getFieldMapper($field_name, $langcode);
      if (!empty($field_mapper)) {
        // Merge values to the global entity array.
        $entity_array = NestedArray::mergeDeep(
          $entity_array,
          [
            $field_name => $field_mapper->extractFieldValuesFromRawData(
              (array) $raw_data,
              $context
            ),
          ]
        );
      }
    }

    // Allow other modules to perform custom mapping logic.
    // @todo Add $context to event?
    $event = new ExternalEntityMapRawDataEvent($raw_data, $entity_array, $this);
    $this->eventDispatcher->dispatch($event, ExternalEntitiesEvents::MAP_RAW_DATA);
    $entity_array = $event->getEntityValues();

    /*
    This code was used to try to provide a language-aware entity array.
    However, it did not solve some issues and brought some more. It may need
    more investigations.
    Note: uncommenting the following code implies to also update
    ExternalEntity::getExternalTranslation() to remove the
    "Remap fields and values according to langcode." part and use
    $this->values = NestedArray::mergeDeep($translation->values, $this->values)
    $this->fields = NestedArray::mergeDeep($translation->fields, $this->fields)
    and
    $this->translatableEntityKeys = NestedArray::mergeDeep(
    $translation->translatableEntityKeys, $this->translatableEntityKeys).

    // // Adapt structure for translations if needed.
    // $language_settings = $this->getExternalEntityType()
    //   ->getLanguageSettings();
    /// if (!empty($language_settings['overrides'])
    //   && $this->languageManager()->isMultilingual()
    // ) {
    //   $entity_type_id = $this->getEntityTypeId();
    //   $field_definitions = $this->entityFieldManager->getFieldDefinitions(
    //     $entity_type_id,
    //     $entity_type_id
    //   );
    //   if (empty($langcode) || ($this->defaultLangcode == $langcode)) {
    //     $langcode = LanguageInterface::LANGCODE_DEFAULT;
    //   }
    //   $translated_array = [];
    //   foreach ($entity_array as $field_name => $value) {
    //     $field_storage_definition = $field_definitions[$field_name]
    //       ->getFieldStorageDefinition();
    //     $property_definitions = $field_storage_definition
    //       ->getPropertyDefinitions();
    //     // Check if the field has a complex structure.
    //     if ((1 < count($property_definitions))
    //       || ('value' != key($property_definitions))
    //     ) {
    //       // Handle complex structures.
    //       $new_value = $value;
    //     }
    //     else {
    //       // Handle simple structures.
    //       if (is_array($value)) {
    //         if (isset($value['value'])) {
    //           $new_value = $value['value'];
    //         }
    //         else {
    //           if ($field_storage_definition->isMultiple()) {
    //             $new_value = array_map(
    //               function ($subvalue) {
    //                 return $subvalue['value'];
    //               },
    //               $value
    //             );
    //           }
    //           else {
    //             $new_value = $value[0]['value'];
    //           }
    //         }
    //       }
    //       else {
    //         $new_value = $value;
    //       }
    //     }
    //
    //     // Check if the field is defined and translatable.
    //     if (isset($field_definitions[$field_name])
    //       && $field_definitions[$field_name]->isTranslatable()
    //     ) {
    //         $translated_array[$field_name] = [$langcode => $new_value];
    //     }
    //     else {
    //       // Untranslated.
    //       $translated_array[$field_name] = [
    //         LanguageInterface::LANGCODE_DEFAULT => $new_value
    //       ];
    //     }
    //   }
    //   if (LanguageInterface::LANGCODE_DEFAULT != $langcode) {
    //     $entity_array['langcode'] = [
    //       LanguageInterface::LANGCODE_DEFAULT => $this->defaultLangcode,
    //       $langcode => $langcode,
    //     ];
    //   }
    //   $entity_array = $translated_array;
    // }
     */
    // Remove the following line if commented code above is uncommented.
    if ($this->languageManager()->isMultilingual()) {
      $entity_array['langcode'] = [
        LanguageInterface::LANGCODE_DEFAULT =>
        $langcode ?? $this->defaultLangcode,
      ];
    }

    return $entity_array;
  }

  /**
   * {@inheritdoc}
   */
  public function mapRawDataToExternalEntities(
    array $all_raw_data,
    ?string $langcode = NULL,
  ) :array {
    if (!$all_raw_data) {
      return [];
    }

    $entities = [];
    foreach ($all_raw_data as $id => $raw_data) {
      $entity_array = $this->extractEntityValuesFromRawData($raw_data, [], $langcode);

      if (empty($entity_array) || (empty($entity_array['id']))) {
        continue;
      }
      try {
        // We do not call doCreate() because we want to provide an ID so the
        // entity is not detected as new.
        $bundle = $this->getBundleFromValues($entity_array);
        if ($this->bundleKey && !$bundle) {
          throw new EntityStorageException('Missing bundle for entity type ' . $this->entityTypeId);
        }
        $entity_class = $this->getEntityClass($bundle);
        if (empty($langcode) || (LanguageInterface::LANGCODE_DEFAULT == $langcode)) {
          $langcode = $this->defaultLangcode;
        }
        $entities[$id] = new $entity_class($entity_array, $this->entityTypeId, $bundle);
        $this->initFieldValues($entities[$id], $entity_array);
        $entities[$id]->setOriginalRawData($all_raw_data[$id]);
      }
      catch (EntityStorageException $exception) {
        DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '10.1.0', fn() => Error::logException(\Drupal::logger('external_entities'), $exception), fn() => watchdog_exception('external_entities', $exception));
      }

      // @todo The "foreach" loop below is deprecated as we now handle
      // translations using field mapping override. This code will be removed in
      // v3.0.0 release.
      // Check if there are translation data available.
      foreach ($all_raw_data[$id] as $key => $raw_translation_data) {
        if (isset($raw_translation_data[$id]) && str_starts_with($key, self::EXTERNAL_ENTITY_TRANSLATION_SUB_FIELD_PREFIX)) {
          @trigger_error(
            'The merge method "As a translation" is deprecated in external_entities:3.0.0-beta6 and it will be removed in external_entities:3.0.0. Use external entity translation field mapping overrides instead. See https://www.drupal.org/project/external_entities/issues/3506455',
            E_USER_DEPRECATED
          );
          try {
            $langcode = str_replace(self::EXTERNAL_ENTITY_TRANSLATION_SUB_FIELD_PREFIX, '', $key);
            $entity_translation_data = $this->extractEntityValuesFromRawData($raw_translation_data[$id]);
            $entities[$id]->addTranslation($langcode, $entity_translation_data);
          }
          catch (\Throwable $exception) {
            // Don't completely fail if a translation can't be handled.
            $this->logger->error(
              'Error while mapping translation data for external entity @entity_id: @message',
              ['@entity_id' => $id, '@message' => $exception->getMessage()]
            );
          }
        }
      }

      $entities[$id]->enforceIsNew(FALSE);
    }

    return $entities;
  }

  /**
   * Gets entities from the external storage.
   *
   * @param array|null $ids
   *   If not empty, return entities that match these IDs. Return no entities
   *   when NULL.
   * @param string|null $langcode
   *   The language code. If null, set default.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface[]
   *   Array of entities from the storage.
   */
  protected function getFromExternalStorage(
    ?array $ids = NULL,
    ?string $langcode = NULL,
  ) :array {
    $entities = [];
    $all_raw_data = $this->getRawDataFromExternalStorage($ids, $langcode);
    if ($all_raw_data) {
      // Map the data into entity objects and according fields.
      $entities = $this->mapRawDataToExternalEntities($all_raw_data, $langcode);
    }
    return $entities;
  }

  /**
   * {@inheritdoc}
   */
  public function queryExternalStorage(
    array $parameters = [],
    array $sorts = [],
    ?int $start = NULL,
    ?int $length = NULL,
  ) :array {
    $all_raw_data = $this->queryRawDataFromExternalStorage(
      $parameters,
      $sorts,
      $start,
      $length,
      FALSE
    );
    return $this->mapRawDataToExternalEntities($all_raw_data);
  }

  /**
   * {@inheritdoc}
   */
  public function createRawDataFromEntityValues(
    array $entity_values,
    array $original_values,
    ?string $langcode = NULL,
  ) :array {
    // Prepare a context.
    $context = [
      FieldMapperInterface::CONTEXT_SOURCE_KEY => $original_values,
    ];
    // Start from original data that will be altered.
    $raw_data = $original_values;
    if (empty($field_names)) {
      // Get all fields.
      $field_names = $this->getExternalEntityType()->getMappableFields();
    }
    // Map current external entity data array to source raw data.
    foreach ($field_names as $field_name => $field_def) {
      // For each mapped field, get the field mapper to use.
      $field_mapper = $this->getFieldMapper($field_name, $langcode);
      if (!empty($field_mapper)) {
        $field_mapper->addFieldValuesToRawData(
          $entity_values[$field_name],
          $raw_data,
          $context
        );
      }
    }
    return $raw_data;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc