Skip to content
Snippets Groups Projects

Allow reference fields that circularly referenced to be imported.

Files
12
+ 155
31
@@ -4,7 +4,10 @@ namespace Drupal\default_content;
use Drupal\Component\Graph\Graph;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Serialization\Yaml;
@@ -274,42 +277,63 @@ class Importer implements ImporterInterface {
// @todo what if no dependencies?
$sorted = $this->sortTree($this->graph);
foreach ($sorted as $link => $details) {
if (!empty($file_map[$link])) {
$file = $file_map[$link];
$entity_type_id = $file->entity_type_id;
$class = $this->entityTypeManager->getDefinition($entity_type_id)->getClass();
$contents = $this->parseFile($file);
$extension = pathinfo($file->uri, PATHINFO_EXTENSION);
if ($extension == 'json') {
$entity = $this->serializer->deserialize($contents, $class, 'hal_json', ['request_method' => 'POST']);
}
else {
$entity = $this->contentEntityNormalizer->denormalize(Yaml::decode($contents));
}
$entity->enforceIsNew(TRUE);
// Ensure that the entity is not owned by the anonymous user.
if ($entity instanceof EntityOwnerInterface && empty($entity->getOwnerId())) {
$entity->setOwner($root_user);
$this->markSortedItemsToResave($sorted);
foreach (['create', 'resolve_circular_dependencies'] as $stage) {
foreach ($sorted as $link => $details) {
// Bounce out early if no circular dependencies were detected.
if ($stage === 'resolve_circular_dependencies' && $details['needs_resave'] === FALSE) {
continue;
}
if (!empty($file_map[$link])) {
$file = $file_map[$link];
$entity_type_id = $file->entity_type_id;
$class = $this->entityTypeManager->getDefinition($entity_type_id)->getClass();
$contents = $this->parseFile($file);
$extension = pathinfo($file->uri, PATHINFO_EXTENSION);
if ($extension == 'json') {
$entity = $this->serializer->deserialize($contents, $class, 'hal_json', ['request_method' => 'POST']);
}
else {
$entity = $this->contentEntityNormalizer->denormalize(Yaml::decode($contents));
}
if ($stage === 'create') {
$entity->enforceIsNew(TRUE);
}
else {
// Replace an un-saved denormalized entity with an already saved
// version with the reference fields it was missing set.
$save_required = FALSE;
$entity = $this->setMissingReferences($entity, $created[$entity->uuid()], $save_required);
// Even though a circular reference was detected for this entity,
// that reference might be in an embedded entity, like a
// paragraph. If the paragraph has the reference field that needs
// to be updated, then only it needs to be re-saved.
if (FALSE === $save_required) {
continue;
}
}
// Ensure that the entity is not owned by the anonymous user.
if ($entity instanceof EntityOwnerInterface && empty($entity->getOwnerId())) {
$entity->setOwner($root_user);
}
// If a file exists in the same folder, copy it to the designed
// target URI.
if ($entity instanceof FileInterface) {
$file_source = \dirname($file->uri) . '/' . $entity->getFilename();
if (\file_exists($file_source)) {
$target_directory = dirname($entity->getFileUri());
$this->fileSystem->prepareDirectory($target_directory, FileSystemInterface::CREATE_DIRECTORY);
$new_uri = $this->fileSystem->copy($file_source, $entity->getFileUri());
$entity->setFileUri($new_uri);
// If a file exists in the same folder, copy it to the designed
// target URI.
if ($entity instanceof FileInterface) {
$file_source = \dirname($file->uri) . '/' . $entity->getFilename();
if (\file_exists($file_source)) {
$target_directory = dirname($entity->getFileUri());
$this->fileSystem->prepareDirectory($target_directory, FileSystemInterface::CREATE_DIRECTORY);
$new_uri = $this->fileSystem->copy($file_source, $entity->getFileUri());
$entity->setFileUri($new_uri);
}
}
}
$entity->setSyncing(TRUE);
$entity->save();
$entity->setSyncing(TRUE);
$entity->save();
$created[$entity->uuid()] = $entity;
$created[$entity->uuid()] = $entity;
}
}
}
$this->eventDispatcher->dispatch(new ImportEvent($created, $module), DefaultContentEvents::IMPORT);
@@ -416,4 +440,104 @@ class Importer implements ImporterInterface {
}
}
/**
* Add a flag to sorted items that need to be re-saved.
*
* This may be required when an entity has a circular dependency and it's
* dependency will not be available the first time around.
*
* @param array $sorted
* The results of sortTree().
*/
protected function markSortedItemsToResave(array &$sorted) {
// Create a copy of the sorted entities to be saved so that circular
// dependencies can be detected that require a re-save for entity
// references to be set properly.
$sorted_copy = $sorted;
while ($sorted_copy) {
$needs_resave = FALSE;
$current_uuid = key($sorted_copy);
$current = array_shift($sorted_copy);
// If the current entity has dependencies on entities that were saved
// after itself, then the dependencies would not have been available to
// be set as references. Mark the current as needing to be resaved
// during the 'resolve_circular_dependencies' stage.
if (!empty($current['edges']) && !empty(array_intersect_key($current['edges'], $sorted_copy))) {
$needs_resave = TRUE;
}
$sorted[$current_uuid]['needs_resave'] = $needs_resave;
}
}
/**
* Set any missing entity references on $entity.
*
* @param \Drupal\Core\Entity\FieldableEntityInterface $denormalized_entity
* An entity after de-normalizing and being ready to save.
* @param \Drupal\Core\Entity\FieldableEntityInterface $saved_entity
* The same entity after it's already been saved, but missing values.
* @param bool $save_required
* The returned entity only needs to be saved if this is true.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface
* The entity with the updated references.
*/
protected function setMissingReferences(FieldableEntityInterface $denormalized_entity, FieldableEntityInterface $saved_entity, bool &$save_required = FALSE) {
foreach ($saved_entity->getFields() as $field_name => $field_definition) {
$saved_entity_field = $saved_entity->get($field_name);
$field_type = $field_definition->getItemDefinition()->getFieldDefinition()->getType();
// If the field is an entity reference revisions field, then do NOT embed
// the referenced entity (like a paragraph) again. The paragraph already
// exists and the field will just be its entity ID this time. Instead,
// load the referenced entity and see if there are any entity reference
// fields recursively and re-save the entity reference revisions again.
// This is a must if you have paragraphs that have entity reference fields
// on them.
if ($field_type === 'entity_reference_revisions') {
$denormalized_referenced_entities = $denormalized_entity->get($field_name)->referencedEntities();
foreach ($saved_entity_field->referencedEntities() as $key => $referenced_entity) {
$inner_save_required = FALSE;
$referenced_entity = $this->setMissingReferences(
$denormalized_referenced_entities[$key],
$referenced_entity,
$inner_save_required
);
// Since this portion is going out and grabbing entities that were
// embedded, only re-save those that had reference fields AND are
// changing.
if ($inner_save_required) {
$referenced_entity->save();
}
}
continue;
}
$denormalized_value = $denormalized_entity->get($field_name)->getValue();
// Allow based on field type / data.
$allowed_field_type =
// This is a 'target_uuid' field, like a link that is not inherently
// a reference field but is treated like one because IDs change.
isset($denormalized_value[0]['uri']) && str_starts_with($denormalized_value[0]['uri'], 'entity:') ||
// Or if the field is an entity reference field.
$field_type === 'entity_reference';
if (!$allowed_field_type) {
continue;
}
// Ignore if the field value is not different from what is saved.
if ($denormalized_entity->get($field_name)->getString() === $saved_entity_field->getString()) {
continue;
}
// Copy the value from the default content configuration instead of from
// the already saved value.
$saved_entity->set($field_name, $denormalized_entity->get($field_name)->getValue());
$save_required = TRUE;
}
// No need to create a new revision as it should have been this value from
// the start.
if ($saved_entity instanceof EntityTypeInterface && $saved_entity->isRevisionable() && $saved_entity instanceof RevisionableInterface) {
$saved_entity->setNewRevision(FALSE);
}
return $saved_entity;
}
}
Loading