diff --git a/migrate_tools.services.yml b/migrate_tools.services.yml index 25393a9..50abdd2 100644 --- a/migrate_tools.services.yml +++ b/migrate_tools.services.yml @@ -10,5 +10,11 @@ services: migrate_tools.migration_drush_command_progress: class: Drupal\migrate_tools\EventSubscriber\MigrationDrushCommandProgress tags: - - { name: event_subscriber, priority: 0 } + - { name: event_subscriber } arguments: ['@logger.channel.migrate_tools'] + migrate_tools.migration_sync: + class: Drupal\migrate_tools\EventSubscriber\MigrationImportSync + tags: + - { name: event_subscriber } + arguments: + - '@event_dispatcher' diff --git a/src/Commands/MigrateToolsCommands.php b/src/Commands/MigrateToolsCommands.php index 3b54ec2..8846cb2 100644 --- a/src/Commands/MigrateToolsCommands.php +++ b/src/Commands/MigrateToolsCommands.php @@ -262,6 +262,8 @@ class MigrateToolsCommands extends DrushCommands { * remaining migrations. * @option execute-dependencies Execute all dependent migrations first. * @option skip-progress-bar Skip displaying a progress bar. + * @option sync Sync source and destination. Delete destination records that + * do not exist in the source. * * @default $options [] * @@ -302,6 +304,7 @@ class MigrateToolsCommands extends DrushCommands { 'continue-on-failure' => FALSE, 'execute-dependencies' => FALSE, 'skip-progress-bar' => FALSE, + 'sync' => FALSE, ]) { $group_names = $options['group']; $tag_names = $options['tag']; @@ -773,6 +776,9 @@ class MigrateToolsCommands extends DrushCommands { $executed_migrations += $required_migrations; } } + if ($options['sync']) { + $migration->set('syncSource', TRUE); + } if ($options['skip-progress-bar']) { $migration->set('skipProgressBar', TRUE); } diff --git a/src/EventSubscriber/MigrationImportSync.php b/src/EventSubscriber/MigrationImportSync.php new file mode 100644 index 0000000..0223b48 --- /dev/null +++ b/src/EventSubscriber/MigrationImportSync.php @@ -0,0 +1,96 @@ +dispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + $events = []; + $events[MigrateEvents::PRE_IMPORT][] = ['sync']; + return $events; + } + + /** + * Event callback to sync source and destination. + * + * @param \Drupal\migrate\Event\MigrateImportEvent $event + * The migration import event. + */ + public function sync(MigrateImportEvent $event) { + $migration = $event->getMigration(); + if (!empty($migration->syncSource)) { + $id_map = $migration->getIdMap(); + $id_map->prepareUpdate(); + $source = $migration->getSourcePlugin(); + $source->rewind(); + $source_id_values = []; + while ($source->valid()) { + $source_id_values[] = $source->current()->getSourceIdValues(); + $source->next(); + } + $id_map->rewind(); + $destination = $migration->getDestinationPlugin(); + while ($id_map->valid()) { + $map_source_id = $id_map->currentSource(); + if (!in_array($map_source_id, $source_id_values, TRUE)) { + $destination_ids = $id_map->currentDestination(); + $this->dispatchRowDeleteEvent(MigrateEvents::PRE_ROW_DELETE, $migration, $destination_ids); + $this->dispatchRowDeleteEvent(MigratePlusEvents::MISSING_SOURCE_ITEM, $migration, $destination_ids); + $destination->rollback($destination_ids); + $this->dispatchRowDeleteEvent(MigrateEvents::POST_ROW_DELETE, $migration, $destination_ids); + $id_map->delete($map_source_id); + } + $id_map->next(); + } + $this->dispatcher->dispatch(MigrateEvents::POST_ROLLBACK, new MigrateRollbackEvent($migration)); + } + } + + /** + * Dispatches MigrateRowDeleteEvent event. + * + * @param string $event_name + * The event name to dispatch. + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The active migration. + * @param array $destination_ids + * The destination identifier values of the record. + */ + protected function dispatchRowDeleteEvent($event_name, MigrationInterface $migration, array $destination_ids) { + // Symfony changing dispatcher so implementation could change. + $this->dispatcher->dispatch($event_name, new MigrateRowDeleteEvent($migration, $destination_ids)); + } + +} diff --git a/src/MigrateExecutable.php b/src/MigrateExecutable.php index 0988d48..18f9312 100644 --- a/src/MigrateExecutable.php +++ b/src/MigrateExecutable.php @@ -114,7 +114,7 @@ class MigrateExecutable extends MigrateExecutableBase { $this->listeners[MigrateEvents::POST_ROW_DELETE] = [$this, 'onPostRowDelete']; $this->listeners[MigratePlusEvents::PREPARE_ROW] = [$this, 'onPrepareRow']; foreach ($this->listeners as $event => $listener) { - \Drupal::service('event_dispatcher')->addListener($event, $listener); + $this->getEventDispatcher()->addListener($event, $listener); } } @@ -245,7 +245,7 @@ class MigrateExecutable extends MigrateExecutableBase { */ protected function removeListeners() { foreach ($this->listeners as $event => $listener) { - \Drupal::service('event_dispatcher')->removeListener($event, $listener); + $this->getEventDispatcher()->removeListener($event, $listener); } } @@ -290,7 +290,11 @@ class MigrateExecutable extends MigrateExecutableBase { $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); $migrate_last_imported_store->set($event->getMigration()->id(), FALSE); $this->rollbackMessage(); - $this->removeListeners(); + // If this is a sync import, then don't remove listeners or post import will + // not be executed. Leave it to post import to remove listeners. + if (empty($event->getMigration()->syncSource)) { + $this->removeListeners(); + } } /** diff --git a/tests/src/Functional/DrushCommandsTest.php b/tests/src/Functional/DrushCommandsTest.php index 04ffe37..01faf94 100644 --- a/tests/src/Functional/DrushCommandsTest.php +++ b/tests/src/Functional/DrushCommandsTest.php @@ -88,9 +88,9 @@ class DrushCommandsTest extends BrowserTestBase { public function testDrush() { $this->drush('ms', [], [], NULL, NULL, 1); $this->assertContains('The "does_not_exist" plugin does not exist.', $this->getErrorOutput()); - $this->drush('cdel', ['migrate_plus.migration.invalid_plugin']); + $this->container->get('config.factory')->getEditable('migrate_plus.migration.invalid_plugin')->delete(); // Flush cache so the recently removed invalid migration is cleared. - $this->drush('cr'); + drupal_flush_all_caches(); $this->drush('ms', [], ['format' => 'json']); $expected = [ [ @@ -159,4 +159,41 @@ class DrushCommandsTest extends BrowserTestBase { $this->assertErrorOutputEquals('[notice] Rolled back 3 items - done with \'fruit_terms\''); } + /** + * Tests synced import. + */ + public function testSyncImport() { + $this->drush('mim', ['fruit_terms']); + $expected = [ + '1/3 [=========>------------------] 33%', + ' 2/3 [==================>---------] 66%', + ' 3/3 [============================] 100% [notice] Processed 3 items (3 created, 0 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', + ]; + $this->assertEquals($expected, $this->getErrorOutputAsList()); + $term = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->load(2); + $this->assertEquals('Banana', $term->label()); + $this->assertEquals(3, \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getQuery()->count()->execute()); + $source = $this->container->get('config.factory')->getEditable('migrate_plus.migration.fruit_terms')->get('source'); + unset($source['data_rows'][1]); + $source['data_rows'][] = ['name' => 'Grape']; + $this->container->get('config.factory')->getEditable('migrate_plus.migration.fruit_terms')->set('source', $source)->save(); + // Flush cache so the recently changed migration can be refreshed. + drupal_flush_all_caches(); + $this->drush('mim', ['fruit_terms'], ['sync' => NULL]); + $expected = [ + '1/3 [=========>------------------] 33% [notice] Rolled back 1 item - done with \'fruit_terms\'', + '', + ' 2/3 [==================>---------] 66%', + ' 3/3 [============================] 100%', + ' 4/4 [============================] 100% [notice] Processed 3 items (1 created, 2 updated, 0 failed, 0 ignored) - done with \'fruit_terms\'', + ]; + $this->assertEquals($expected, $this->getErrorOutputAsList()); + $this->assertEquals(3, \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getQuery()->count()->execute()); + $this->assertEmpty(\Drupal::entityTypeManager()->getStorage('taxonomy_term')->load(2)); + + /** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map */ + $id_map = $this->container->get('plugin.manager.migration')->createInstance('fruit_terms')->getIdMap(); + $this->assertCount(3, $id_map); + } + } diff --git a/tests/src/Kernel/DrushTest.php b/tests/src/Kernel/DrushTest.php index 457605f..09ef385 100644 --- a/tests/src/Kernel/DrushTest.php +++ b/tests/src/Kernel/DrushTest.php @@ -44,6 +44,7 @@ class DrushTest extends MigrateTestBase { 'execute-dependencies' => NULL, 'skip-progress-bar' => FALSE, 'continue-on-failure' => FALSE, + 'sync' => FALSE, ]; /**