Importer.php
Same filename in other branches
Namespace
Drupal\Core\DefaultContentFile
-
core/
lib/ Drupal/ Core/ DefaultContent/ Importer.php
View source
<?php
declare (strict_types=1);
namespace Drupal\Core\DefaultContent;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityReference;
use Drupal\Core\Field\FieldItemInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Installer\InstallerKernel;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\file\FileInterface;
use Drupal\link\Plugin\Field\FieldType\LinkItem;
use Drupal\user\EntityOwnerInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
/**
* A service for handling import of content.
*
* @internal
* This API is experimental.
*/
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(EntityTypeManagerInterface $entityTypeManager, AdminAccountSwitcher $accountSwitcher, FileSystemInterface $fileSystem, LanguageManagerInterface $languageManager, 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;
}
}
Classes
Title | Deprecated | Summary |
---|---|---|
Importer | A service for handling import of content. |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.