By default, the migrate module in Drupal 8 updates existing nodes by completely overwriting all the fields in the target node with the data from the migration. But what if your client has modified content on the target (D8) system, and still wants to update the content with newly-mapped fields? For instance, there is a “description” that has been updated after the first migration, but now the client wants “subtitle” field to be migrated (which was empty or not present during the first pass), without overwriting the description field changes.
The safest option here would be updating nodes not on a “node by node” basis, but, on a “field by field” one. It is easily done with a slightly modified destination plugin.
Firstly, in order to minimize the effort, we have to extend the base migration class
1 |
Drupal\migrate\Plugin\migrate\destination\EntityContentBase |
Secondly, we have to override the wrapper static class “create”, in order to make sure that the plugin is fully compatible with the default “entity:node” handler:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/** * Entity type. * * @var string $entityType */ public static $entityType = 'node'; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { return parent::create($container, $configuration, 'entity:' . static::$entityType, $plugin_definition, $migration); } |
The last bit is actually overriding the updateEntity method, that handles the merge logic. Most of is is copied from the base class, with a slight modification:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
/** * {@inheritdoc} */ protected function updateEntity(EntityInterface $entity, Row $row) { // By default, an update will be preserved. $rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE; // Make sure we have the right translation. if ($this->isTranslationDestination()) { $property = $this->storage->getEntityType()->getKey('langcode'); if ($row->hasDestinationProperty($property)) { $language = $row->getDestinationProperty($property); if (!$entity->hasTranslation($language)) { $entity->addTranslation($language); // We're adding a translation, so delete it on rollback. $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE; } $entity = $entity->getTranslation($language); } } // If the migration has specified a list of properties to be overwritten, // clone the row with an empty set of destination values, and re-add only // the specified properties. if (isset($this->configuration['overwrite_properties'])) { $clone = $row->cloneWithoutDestination(); foreach ($this->configuration['overwrite_properties'] as $property) { $clone->setDestinationProperty($property, $row->getDestinationProperty($property)); } $row = $clone; } foreach ($row->getDestination() as $field_name => $values) { $field = $entity->$field_name; if ($field instanceof TypedDataInterface) { // Update field ONLY if the destination entity has no value for it. if (empty($field->getValue())) { $field->setValue($values); } } } $this->setRollbackAction($row->getIdMap(), $rollback_action); // We might have a different (translated) entity, so return it. return $entity; } |
As a summary, here is the full code for the plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
<?php namespace Drupal\ju_migrations\Plugin\migrate\destination; use Drupal\Core\Entity\EntityInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\migrate\Plugin\migrate\destination\EntityContentBase; use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Row; use Drupal\migrate\Plugin\MigrateIdMapInterface; use Drupal\Core\TypedData\TypedDataInterface; /** * Provides node destination, updating only the non-empty fields. * * @MigrateDestination( * id = "node_merge", * ) */ class NodeMerge extends EntityContentBase { /** * Entity type. * * @var string $entityType */ public static $entityType = 'node'; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { return parent::create($container, $configuration, 'entity:' . static::$entityType, $plugin_definition, $migration); } /** * {@inheritdoc} */ protected function updateEntity(EntityInterface $entity, Row $row) { // By default, an update will be preserved. $rollback_action = MigrateIdMapInterface::ROLLBACK_PRESERVE; // Make sure we have the right translation. if ($this->isTranslationDestination()) { $property = $this->storage->getEntityType()->getKey('langcode'); if ($row->hasDestinationProperty($property)) { $language = $row->getDestinationProperty($property); if (!$entity->hasTranslation($language)) { $entity->addTranslation($language); // We're adding a translation, so delete it on rollback. $rollback_action = MigrateIdMapInterface::ROLLBACK_DELETE; } $entity = $entity->getTranslation($language); } } // If the migration has specified a list of properties to be overwritten, // clone the row with an empty set of destination values, and re-add only // the specified properties. if (isset($this->configuration['overwrite_properties'])) { $clone = $row->cloneWithoutDestination(); foreach ($this->configuration['overwrite_properties'] as $property) { $clone->setDestinationProperty($property, $row->getDestinationProperty($property)); } $row = $clone; } foreach ($row->getDestination() as $field_name => $values) { $field = $entity->$field_name; if ($field instanceof TypedDataInterface) { // Update field ONLY if the destination entity has no value for it. if (empty($field->getValue())) { $field->setValue($values); } } } $this->setRollbackAction($row->getIdMap(), $rollback_action); // We might have a different (translated) entity, so return it. return $entity; } } |