class Importer

Same name and namespace in other branches
  1. 11.x core/lib/Drupal/Core/DefaultContent/Importer.php \Drupal\Core\DefaultContent\Importer

A service for handling import of content.

@internal This API is experimental.

Hierarchy

  • class \Drupal\Core\DefaultContent\Importer extends \Psr\Log\LoggerAwareInterface uses \Psr\Log\LoggerAwareTrait

Expanded class hierarchy of Importer

2 files declare their use of Importer
ContentImportTest.php in core/tests/Drupal/FunctionalTests/DefaultContent/ContentImportTest.php
RecipeRunner.php in core/lib/Drupal/Core/Recipe/RecipeRunner.php

File

core/lib/Drupal/Core/DefaultContent/Importer.php, line 28

Namespace

Drupal\Core\DefaultContent
View source
final class Importer implements LoggerAwareInterface {
  use LoggerAwareTrait;
  
  /**
   * The dependencies of the currently importing entity, if any.
   *
   * The keys are the UUIDs of the dependencies, and the values are arrays with
   * two members: the entity type ID of the dependency, and the UUID to load.
   *
   * @var array<string, string[]>|null
   */
  private ?array $dependencies = NULL;
  public function __construct(private readonly EntityTypeManagerInterface $entityTypeManager, private readonly AdminAccountSwitcher $accountSwitcher, private readonly FileSystemInterface $fileSystem, private readonly LanguageManagerInterface $languageManager, private readonly EntityRepositoryInterface $entityRepository) {
  }
  
  /**
   * Imports content entities from disk.
   *
   * @param \Drupal\Core\DefaultContent\Finder $content
   *   The content finder, which has information on the entities to create
   *   in the necessary dependency order.
   * @param \Drupal\Core\DefaultContent\Existing $existing
   *   (optional) What to do if one of the entities being imported already
   *   exists, by UUID:
   *   - \Drupal\Core\DefaultContent\Existing::Error: Throw an exception.
   *   - \Drupal\Core\DefaultContent\Existing::Skip: Leave the existing entity
   *     as-is.
   *
   * @throws \Drupal\Core\DefaultContent\ImportException
   *   - If any of the entities being imported are not content entities.
   *   - If any of the entities being imported already exists, by UUID, and
   *     $existing is \Drupal\Core\DefaultContent\Existing::Error.
   */
  public function importContent(Finder $content, Existing $existing = Existing::Error) : void {
    if (count($content->data) === 0) {
      return;
    }
    $account = $this->accountSwitcher
      ->switchToAdministrator();
    try {
      /** @var array{_meta: array<mixed>} $decoded */
      foreach ($content->data as $decoded) {
        [
          'uuid' => $uuid,
          'entity_type' => $entity_type_id,
          'path' => $path,
        ] = $decoded['_meta'];
        assert(is_string($uuid));
        assert(is_string($entity_type_id));
        assert(is_string($path));
        $entity_type = $this->entityTypeManager
          ->getDefinition($entity_type_id);
        /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
        if (!$entity_type->entityClassImplements(ContentEntityInterface::class)) {
          throw new ImportException("Content entity {$uuid} is a '{$entity_type_id}', which is not a content entity type.");
        }
        $entity = $this->entityRepository
          ->loadEntityByUuid($entity_type_id, $uuid);
        if ($entity) {
          if ($existing === Existing::Skip) {
            continue;
          }
          else {
            throw new ImportException("{$entity_type_id} {$uuid} already exists.");
          }
        }
        $entity = $this->toEntity($decoded)
          ->enforceIsNew();
        // Ensure that the entity is not owned by the anonymous user.
        if ($entity instanceof EntityOwnerInterface && empty($entity->getOwnerId())) {
          $entity->setOwnerId($account->id());
        }
        // If a file exists in the same folder, copy it to the designated
        // target URI.
        if ($entity instanceof FileInterface) {
          $this->copyFileAssociatedWithEntity(dirname($path), $entity);
        }
        $violations = $entity->validate();
        if (count($violations) > 0) {
          throw new InvalidEntityException($violations, $path);
        }
        $entity->save();
      }
    } finally {
      $this->accountSwitcher
        ->switchBack();
    }
  }
  
  /**
   * Copies a file from default content directory to the site's file system.
   *
   * @param string $path
   *   The path to the file to copy.
   * @param \Drupal\file\FileInterface $entity
   *   The file entity.
   */
  private function copyFileAssociatedWithEntity(string $path, FileInterface &$entity) : void {
    $destination = $entity->getFileUri();
    assert(is_string($destination));
    // If the source file doesn't exist, there's nothing we can do.
    $source = $path . '/' . basename($destination);
    if (!file_exists($source)) {
      $this->logger?->warning("File entity %name was imported, but the associated file (@path) was not found.", [
        '%name' => $entity->label(),
        '@path' => $source,
      ]);
      return;
    }
    $copy_file = TRUE;
    if (file_exists($destination)) {
      $source_hash = hash_file('sha256', $source);
      assert(is_string($source_hash));
      $destination_hash = hash_file('sha256', $destination);
      assert(is_string($destination_hash));
      if (hash_equals($source_hash, $destination_hash) && $this->entityTypeManager
        ->getStorage('file')
        ->loadByProperties([
        'uri' => $destination,
      ]) === []) {
        // If the file hashes match and the file is not already a managed file
        // then do not copy a new version to the file system. This prevents
        // re-installs during development from creating unnecessary duplicates.
        $copy_file = FALSE;
      }
    }
    $target_directory = dirname($destination);
    $this->fileSystem
      ->prepareDirectory($target_directory, FileSystemInterface::CREATE_DIRECTORY);
    if ($copy_file) {
      $uri = $this->fileSystem
        ->copy($source, $destination);
      $entity->setFileUri($uri);
    }
  }
  
  /**
   * Converts an array of content entity data to a content entity object.
   *
   * @param array<string, array<mixed>> $data
   *   The entity data.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface
   *   The unsaved entity.
   *
   * @throws \Drupal\Core\DefaultContent\ImportException
   *   If the `entity_type` or `uuid` meta keys are not set.
   */
  private function toEntity(array $data) : ContentEntityInterface {
    if (empty($data['_meta']['entity_type'])) {
      throw new ImportException('The entity type metadata must be specified.');
    }
    if (empty($data['_meta']['uuid'])) {
      throw new ImportException('The uuid metadata must be specified.');
    }
    $is_root = FALSE;
    // @see ::loadEntityDependency()
    if ($this->dependencies === NULL && !empty($data['_meta']['depends'])) {
      $is_root = TRUE;
      foreach ($data['_meta']['depends'] as $uuid => $entity_type) {
        assert(is_string($uuid));
        assert(is_string($entity_type));
        $this->dependencies[$uuid] = [
          $entity_type,
          $uuid,
        ];
      }
    }
    [
      'entity_type' => $entity_type,
    ] = $data['_meta'];
    assert(is_string($entity_type));
    /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
    $entity_type = $this->entityTypeManager
      ->getDefinition($entity_type);
    $values = [
      'uuid' => $data['_meta']['uuid'],
    ];
    if (!empty($data['_meta']['bundle'])) {
      $values[$entity_type->getKey('bundle')] = $data['_meta']['bundle'];
    }
    if (!empty($data['_meta']['default_langcode'])) {
      $data = $this->verifyNormalizedLanguage($data);
      $values[$entity_type->getKey('langcode')] = $data['_meta']['default_langcode'];
    }
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $this->entityTypeManager
      ->getStorage($entity_type->id())
      ->create($values);
    foreach ($data['default'] as $field_name => $values) {
      $this->setFieldValues($entity, $field_name, $values);
    }
    foreach ($data['translations'] ?? [] as $langcode => $translation_data) {
      if ($this->languageManager
        ->getLanguage($langcode)) {
        $translation = $entity->addTranslation($langcode, $entity->toArray());
        foreach ($translation_data as $field_name => $values) {
          $this->setFieldValues($translation, $field_name, $values);
        }
      }
    }
    if ($is_root) {
      $this->dependencies = NULL;
    }
    return $entity;
  }
  
  /**
   * Sets field values based on the normalized data.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The content entity.
   * @param string $field_name
   *   The name of the field.
   * @param array $values
   *   The normalized data for the field.
   */
  private function setFieldValues(ContentEntityInterface $entity, string $field_name, array $values) : void {
    foreach ($values as $delta => $item_value) {
      if (!$entity->get($field_name)
        ->get($delta)) {
        $entity->get($field_name)
          ->appendItem();
      }
      /** @var \Drupal\Core\Field\FieldItemInterface $item */
      $item = $entity->get($field_name)
        ->get($delta);
      // Update the URI based on the target UUID for link fields.
      if (isset($item_value['target_uuid']) && $item instanceof LinkItem) {
        $target_entity = $this->loadEntityDependency($item_value['target_uuid']);
        if ($target_entity) {
          $item_value['uri'] = 'entity:' . $target_entity->getEntityTypeId() . '/' . $target_entity->id();
        }
        unset($item_value['target_uuid']);
      }
      $serialized_property_names = $this->getCustomSerializedPropertyNames($item);
      foreach ($item_value as $property_name => $value) {
        if (\in_array($property_name, $serialized_property_names)) {
          if (\is_string($value)) {
            throw new ImportException("Received string for serialized property {$field_name}.{$delta}.{$property_name}");
          }
          $value = serialize($value);
        }
        $property = $item->get($property_name);
        if ($property instanceof EntityReference) {
          if (is_array($value)) {
            $value = $this->toEntity($value);
          }
          else {
            $value = $this->loadEntityDependency($value);
          }
        }
        $property->setValue($value);
      }
    }
  }
  
  /**
   * Gets the names of all properties the plugin treats as serialized data.
   *
   * This allows the field storage definition or entity type to provide a
   * setting for serialized properties. This can be used for fields that
   * handle serialized data themselves and do not rely on the serialized schema
   * flag.
   *
   * @param \Drupal\Core\Field\FieldItemInterface $field_item
   *   The field item.
   *
   * @return string[]
   *   The property names for serialized properties.
   *
   * @see \Drupal\serialization\Normalizer\SerializedColumnNormalizerTrait::getCustomSerializedPropertyNames
   */
  private function getCustomSerializedPropertyNames(FieldItemInterface $field_item) : array {
    if ($field_item instanceof PluginInspectionInterface) {
      $definition = $field_item->getPluginDefinition();
      $serialized_fields = $field_item->getEntity()
        ->getEntityType()
        ->get('serialized_field_property_names');
      $field_name = $field_item->getFieldDefinition()
        ->getName();
      if (is_array($serialized_fields) && isset($serialized_fields[$field_name]) && is_array($serialized_fields[$field_name])) {
        return $serialized_fields[$field_name];
      }
      if (isset($definition['serialized_property_names']) && is_array($definition['serialized_property_names'])) {
        return $definition['serialized_property_names'];
      }
    }
    return [];
  }
  
  /**
   * Loads the entity dependency by its UUID.
   *
   * @param string $target_uuid
   *   The entity UUID.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface|null
   *   The loaded entity.
   */
  private function loadEntityDependency(string $target_uuid) : ?ContentEntityInterface {
    if ($this->dependencies && array_key_exists($target_uuid, $this->dependencies)) {
      $entity = $this->entityRepository
        ->loadEntityByUuid(...$this->dependencies[$target_uuid]);
      assert($entity instanceof ContentEntityInterface || $entity === NULL);
      return $entity;
    }
    return NULL;
  }
  
  /**
   * Verifies that the site knows the default language of the normalized entity.
   *
   * Will attempt to switch to an alternative translation or just import it
   * with the site default language.
   *
   * @param array $data
   *   The normalized entity data.
   *
   * @return array
   *   The normalized entity data, possibly with altered default language
   *   and translations.
   */
  private function verifyNormalizedLanguage(array $data) : array {
    $default_langcode = $data['_meta']['default_langcode'];
    $default_language = $this->languageManager
      ->getDefaultLanguage();
    // Check the language. If the default language isn't known, import as one
    // of the available translations if one exists with those values. If none
    // exists, create the entity in the default language.
    // During the installer, when installing with an alternative language,
    // `en` is still the default when modules are installed so check the default language
    // instead.
    if (!$this->languageManager
      ->getLanguage($default_langcode) || InstallerKernel::installationAttempted() && $default_language->getId() !== $default_langcode) {
      $use_default = TRUE;
      foreach ($data['translations'] ?? [] as $langcode => $translation_data) {
        if ($this->languageManager
          ->getLanguage($langcode)) {
          $data['_meta']['default_langcode'] = $langcode;
          $data['default'] = \array_merge($data['default'], $translation_data);
          unset($data['translations'][$langcode]);
          $use_default = FALSE;
          break;

        }
      }
      if ($use_default) {
        $data['_meta']['default_langcode'] = $default_language->getId();
      }
    }
    return $data;
  }

}

Members

Title Sort descending Modifiers Object type Summary
Importer::$dependencies private property The dependencies of the currently importing entity, if any.
Importer::copyFileAssociatedWithEntity private function Copies a file from default content directory to the site&#039;s file system.
Importer::getCustomSerializedPropertyNames private function Gets the names of all properties the plugin treats as serialized data.
Importer::importContent public function Imports content entities from disk.
Importer::loadEntityDependency private function Loads the entity dependency by its UUID.
Importer::setFieldValues private function Sets field values based on the normalized data.
Importer::toEntity private function Converts an array of content entity data to a content entity object.
Importer::verifyNormalizedLanguage private function Verifies that the site knows the default language of the normalized entity.
Importer::__construct public function

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.