Skip to content
Snippets Groups Projects

Allow reference fields that circularly referenced to be imported.

5 unresolved threads
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);
Please register or sign in to reply
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);
    • Adds a new method that uses the already existing 'graph' to determine which entities were saved with references to content that had not been created yet. That is the purpose of the loop, the first is to create all like normal, then go back over updating any that have references that could not be fulfilled.

Please register or sign in to reply
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;
Please register or sign in to reply
}
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') {
Please register or sign in to reply
$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();
Please register or sign in to reply
}
}
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