I'm a bit late to the party, but want to leave a comment for future readers of this question:
Obviously, there's no plugin in core or migrate_plus that allows skipping a migration, if the destination of an update is not empty. So we created a process plugin, that does exactly that:
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipProcessException;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Skips migration updates, if the destination field is not empty.
*
* This process plugin is intended for content entity migrations. It prevents
* field values from being overwritten, if the migration performs an update
* (the destination entity exists) and the target field is NOT empty.
*
* Available configuration keys:
* - method: (optional) What to do, if the destination field is not empty.
* Possible values:
* - row: Skips the entire row. (Default behavior.)
* - process: Prevents further processing of the input property.
*
* Example usage:
*
* Skip updating the entire destination entity, if its
* 'field_ignore_if_not_empty' field is not empty; otherwise use the source
* value from 'field_in_source'.
*
* @code
* process:
* field_ignore_if_not_empty:
* plugin: skip_on_destination_not_empty
* source: field_in_source
* method: row
* @endcode
*
* @MigrateProcessPlugin(
* id = "skip_on_destination_not_empty"
* )
*/
class SkipOnDestinationNotEmpty extends ProcessPluginBase implements ContainerFactoryPluginInterface {
/**
* The destination entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $entityStorage;
/**
* The migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* {@inheritdoc}
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration.
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
* The destination entity storage.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $entity_storage) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityStorage = $entity_storage;
$this->migration = $migration;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
// Check, whether the migration is empty.
if (empty($migration)) {
throw new MigrateException('SkipOnDestinationNotEmpty plugin can only be used with a migration context.');
}
// We need to know the destination entity type, which we can get from the
// migration's destination plugin ID, if it's a content entity
// destination plugin ('entity:[entity_type_id]').
$destination_plugin = $migration->getDestinationPlugin();
$destination_plugin_id = $destination_plugin->getPluginId();
$entity_type_id = NULL;
if (strpos($destination_plugin_id, static::DERIVATIVE_SEPARATOR)) {
[, $entity_type_id] = explode(static::DERIVATIVE_SEPARATOR, $destination_plugin_id, 2);
}
// If we didn't find an entity type, we should stop here.
if (empty($entity_type_id)) {
throw new MigrateException('SkipOnDestinationNotEmpty plugin can only be used with content entity migrations.');
}
// If the entity storage doesn't exist, we stop here as well.
$storage = $container->get('entity_type.manager')->getStorage($entity_type_id);
if (empty($storage)) {
throw new MigrateException("SkipOnDestinationNotEmpty plugin can't initialize the entity storage for {$entity_type_id}.");
}
// Inject the entity storage into the constructor.
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
$storage
);
}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Determine the destination entity ID. We use the migration's ID map for
// update migrations, or an entity ID value provided in the migration
// process instructions.
$id_map = $this->migration->getIdMap();
$destination_ids = $id_map->lookupDestinationIds($row->getSourceIdValues());
$destination_id_values = $destination_ids ? reset($destination_ids) : [];
$entity_id = reset($destination_id_values) ?: $row->getDestinationProperty($this->entityStorage->getEntityType()->getKey('id'));
// Whether a target entity ID exists and the entity can be loaded.
if (!empty($entity_id) && ($entity = $this->entityStorage->load($entity_id))) {
// Whether the entity has the destination field and it's not empty.
if ($entity->hasField($destination_property) && !$entity->get($destination_property)->isEmpty()) {
// Whether process skipping has been configured.
if (!empty($this->configuration['mode']) && $this->configuration['mode'] == 'process') {
throw new MigrateSkipProcessException("Skipped processing $destination_property on {$this->entityStorage->getEntityType()->id()} {$entity->id()} due to destination field is not empty.");
}
// Skip the row.
else {
throw new MigrateSkipRowException("Skipped updating {$this->entityStorage->getEntityType()->id()} {$entity->id()} due to $destination_property field is not empty.");
}
}
}
// Return the not altered value.
return $value;
}
}
Now, this plugin skips, if the entire destination field is not empty, and it can be used on the first level of the field process only.
It would yield strange results, if used on a field property, as it uses the transform
process plugin method to determine the field name via the $destination_property
parameter, which reflects the field property name on the second level (e.g. target_id
, value
, summary
, ...).
BUT you can easily adapt it to do some different checks on the destination field. E.g., checking certain field properties. All you have to do is altering the line
if ($entity->hasField($destination_property) && !$entity->get($destination_property)->isEmpty()) {
to your needs.
Please also note, that this plugin does NOT support multilanguage entities and destination languages. That wasn't required for our task at hand, but you should be aware of it.
body/value
, but leave the existing summarybody/summary
as is. Theoverwrite_properties
option does not work here, as it works only for whole fields, not for sub values of a single field.