/** * Tests rolling back configuration and content entities. */ public function testRollback() { // We use vocabularies to demonstrate importing and rolling back // configuration entities. $vocabulary_data_rows = [['id' => '1', 'name' => 'categories', 'weight' => '2'], ['id' => '2', 'name' => 'tags', 'weight' => '1']]; $ids = ['id' => ['type' => 'integer']]; $config = ['id' => 'vocabularies', 'migration_tags' => ['Import and rollback test'], 'source' => ['plugin' => 'embedded_data', 'data_rows' => $vocabulary_data_rows, 'ids' => $ids], 'process' => ['vid' => 'id', 'name' => 'name', 'weight' => 'weight'], 'destination' => ['plugin' => 'entity:taxonomy_vocabulary']]; $vocabulary_migration = Migration::create($config); $vocabulary_id_map = $vocabulary_migration->getIdMap(); $this->assertTrue($vocabulary_migration->getDestinationPlugin()->supportsRollback()); // Import and validate vocabulary config entities were created. $vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this); $vocabulary_executable->import(); foreach ($vocabulary_data_rows as $row) { /** @var Vocabulary $vocabulary */ $vocabulary = Vocabulary::load($row['id']); $this->assertTrue($vocabulary); $map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]); $this->assertNotNull($map_row['destid1']); } // We use taxonomy terms to demonstrate importing and rolling back // content entities. $term_data_rows = [['id' => '1', 'vocab' => '1', 'name' => 'music'], ['id' => '2', 'vocab' => '2', 'name' => 'Bach'], ['id' => '3', 'vocab' => '2', 'name' => 'Beethoven']]; $ids = ['id' => ['type' => 'integer']]; $config = ['id' => 'terms', 'migration_tags' => ['Import and rollback test'], 'source' => ['plugin' => 'embedded_data', 'data_rows' => $term_data_rows, 'ids' => $ids], 'process' => ['tid' => 'id', 'vid' => 'vocab', 'name' => 'name'], 'destination' => ['plugin' => 'entity:taxonomy_term'], 'migration_dependencies' => ['required' => ['vocabularies']]]; $term_migration = Migration::create($config); $term_id_map = $term_migration->getIdMap(); $this->assertTrue($term_migration->getDestinationPlugin()->supportsRollback()); // Import and validate term entities were created. $term_executable = new MigrateExecutable($term_migration, $this); $term_executable->import(); foreach ($term_data_rows as $row) { /** @var Term $term */ $term = Term::load($row['id']); $this->assertTrue($term); $map_row = $term_id_map->getRowBySource(['id' => $row['id']]); $this->assertNotNull($map_row['destid1']); } // Rollback and verify the entities are gone. $term_executable->rollback(); foreach ($term_data_rows as $row) { $term = Term::load($row['id']); $this->assertNull($term); $map_row = $term_id_map->getRowBySource(['id' => $row['id']]); $this->assertFalse($map_row); } $vocabulary_executable->rollback(); foreach ($vocabulary_data_rows as $row) { $term = Vocabulary::load($row['id']); $this->assertNull($term); $map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]); $this->assertFalse($map_row); } // Test that simple configuration is not rollbackable. $term_setting_rows = [['id' => 1, 'override_selector' => '0', 'terms_per_page_admin' => '10']]; $ids = ['id' => ['type' => 'integer']]; $config = ['id' => 'taxonomy_settings', 'migration_tags' => ['Import and rollback test'], 'source' => ['plugin' => 'embedded_data', 'data_rows' => $term_setting_rows, 'ids' => $ids], 'process' => ['override_selector' => 'override_selector', 'terms_per_page_admin' => 'terms_per_page_admin'], 'destination' => ['plugin' => 'config', 'config_name' => 'taxonomy.settings'], 'migration_dependencies' => ['required' => ['vocabularies']]]; $settings_migration = Migration::create($config); $this->assertFalse($settings_migration->getDestinationPlugin()->supportsRollback()); }
/** * Test importing and rolling back our data. */ public function testMigrations() { /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ $storage = $this->container->get('entity.manager')->getStorage('node'); $this->assertEquals(0, count($storage->loadMultiple())); // Run the migrations. $migration_ids = ['external_translated_test_node', 'external_translated_test_node_translation']; $this->executeMigrations($migration_ids); $this->assertEquals(3, count($storage->loadMultiple())); $node = $storage->load(1); $this->assertEquals('en', $node->language()->getId()); $this->assertEquals('Cat', $node->title->value); $this->assertEquals('Chat', $node->getTranslation('fr')->title->value); $this->assertEquals('Gato', $node->getTranslation('es')->title->value); $node = $storage->load(2); $this->assertEquals('en', $node->language()->getId()); $this->assertEquals('Dog', $node->title->value); $this->assertEquals('Chien', $node->getTranslation('fr')->title->value); $this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 2"); $node = $storage->load(3); $this->assertEquals('en', $node->language()->getId()); $this->assertEquals('Monkey', $node->title->value); $this->assertFalse($node->hasTranslation('fr'), "No french translation for node 3"); $this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 3"); $this->assertNull($storage->load(4), "No node 4 migrated"); // Roll back the migrations. foreach ($migration_ids as $migration_id) { $migration = $this->getMigration($migration_id); $executable = new MigrateExecutable($migration, $this); $executable->rollback(); } $this->assertEquals(0, count($storage->loadMultiple())); }
/** * Runs a single migration batch. * * @param int[] $initial_ids * The full set of migration IDs to import. * @param string $operation * The operation to perform, 'import' or 'rollback'. * @param array $context * The batch context. */ public static function run($initial_ids, $operation, &$context) { if (!static::$listenersAdded) { $event_dispatcher = \Drupal::service('event_dispatcher'); if ($operation == 'import') { $event_dispatcher->addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']); $event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']); $event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']); } else { $event_dispatcher->addListener(MigrateEvents::POST_ROW_DELETE, [static::class, 'onPostRowDelete']); $event_dispatcher->addListener(MigrateEvents::MAP_DELETE, [static::class, 'onMapDelete']); } static::$maxExecTime = ini_get('max_execution_time'); if (static::$maxExecTime <= 0) { static::$maxExecTime = 60; } // Set an arbitrary threshold of 3 seconds (e.g., if max_execution_time is // 45 seconds, we will quit at 42 seconds so a slow item or cleanup // overhead don't put us over 45). static::$maxExecTime -= 3; static::$listenersAdded = TRUE; } if (!isset($context['sandbox']['migration_ids'])) { $context['sandbox']['max'] = count($initial_ids); $context['sandbox']['current'] = 1; // Total number processed for this migration. $context['sandbox']['num_processed'] = 0; // migration_ids will be the list of IDs remaining to run. $context['sandbox']['migration_ids'] = $initial_ids; $context['sandbox']['messages'] = []; $context['results']['failures'] = 0; $context['results']['successes'] = 0; $context['results']['operation'] = $operation; } // Number processed in this batch. static::$numProcessed = 0; $migration_id = reset($context['sandbox']['migration_ids']); /** @var \Drupal\migrate\Entity\Migration $migration */ $migration = Migration::load($migration_id); if ($migration) { static::$messages = new MigrateMessageCapture(); $executable = new MigrateExecutable($migration, static::$messages); $migration_name = $migration->label() ? $migration->label() : $migration_id; try { if ($operation == 'import') { $migration_status = $executable->import(); } else { $migration_status = $executable->rollback(); } } catch (\Exception $e) { static::logger()->error($e->getMessage()); $migration_status = MigrationInterface::RESULT_FAILED; } switch ($migration_status) { case MigrationInterface::RESULT_COMPLETED: // Store the number processed in the sandbox. $context['sandbox']['num_processed'] += static::$numProcessed; if ($operation == 'import') { $message = static::getTranslation()->formatPlural($context['sandbox']['num_processed'], 'Upgraded @migration (processed 1 item total)', 'Upgraded @migration (processed @num_processed items total)', ['@migration' => $migration_name, '@num_processed' => $context['sandbox']['num_processed']]); } else { $message = static::getTranslation()->formatPlural($context['sandbox']['num_processed'], 'Rolled back @migration (processed 1 item total)', 'Rolled back @migration (processed @num_processed items total)', ['@migration' => $migration_name, '@num_processed' => $context['sandbox']['num_processed']]); $migration->delete(); } $context['sandbox']['messages'][] = $message; static::logger()->notice($message); $context['sandbox']['num_processed'] = 0; $context['results']['successes']++; break; case MigrationInterface::RESULT_INCOMPLETE: $context['sandbox']['messages'][] = static::getTranslation()->formatPlural(static::$numProcessed, 'Continuing with @migration (processed 1 item)', 'Continuing with @migration (processed @num_processed items)', ['@migration' => $migration_name, '@num_processed' => static::$numProcessed]); $context['sandbox']['num_processed'] += static::$numProcessed; break; case MigrationInterface::RESULT_STOPPED: $context['sandbox']['messages'][] = t('Operation stopped by request'); break; case MigrationInterface::RESULT_FAILED: $context['sandbox']['messages'][] = t('Operation on @migration failed', ['@migration' => $migration_name]); $context['results']['failures']++; static::logger()->error('Operation on @migration failed', ['@migration' => $migration_name]); break; case MigrationInterface::RESULT_SKIPPED: $context['sandbox']['messages'][] = t('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]); static::logger()->error('Operation on @migration skipped due to unfulfilled dependencies', ['@migration' => $migration_name]); break; case MigrationInterface::RESULT_DISABLED: // Skip silently if disabled. break; } // Unless we're continuing on with this migration, take it off the list. if ($migration_status != MigrationInterface::RESULT_INCOMPLETE) { array_shift($context['sandbox']['migration_ids']); $context['sandbox']['current']++; } // Add and log any captured messages. foreach (static::$messages->getMessages() as $message) { $context['sandbox']['messages'][] = $message; static::logger()->error($message); } // Only display the last MESSAGE_LENGTH messages, in reverse order. $message_count = count($context['sandbox']['messages']); $context['message'] = ''; for ($index = max(0, $message_count - self::MESSAGE_LENGTH); $index < $message_count; $index++) { $context['message'] = $context['sandbox']['messages'][$index] . "<br />\n" . $context['message']; } if ($message_count > self::MESSAGE_LENGTH) { // Indicate there are earlier messages not displayed. $context['message'] .= '…'; } // At the top of the list, display the next one (which will be the one // that is running while this message is visible). if (!empty($context['sandbox']['migration_ids'])) { $migration_id = reset($context['sandbox']['migration_ids']); $migration = Migration::load($migration_id); $migration_name = $migration->label() ? $migration->label() : $migration_id; if ($operation == 'import') { $context['message'] = t('Currently upgrading @migration (@current of @max total tasks)', ['@migration' => $migration_name, '@current' => $context['sandbox']['current'], '@max' => $context['sandbox']['max']]) . "<br />\n" . $context['message']; } else { $context['message'] = t('Currently rolling back @migration (@current of @max total tasks)', ['@migration' => $migration_name, '@current' => $context['sandbox']['current'], '@max' => $context['sandbox']['max']]) . "<br />\n" . $context['message']; } } } else { array_shift($context['sandbox']['migration_ids']); $context['sandbox']['current']++; } $context['finished'] = 1 - count($context['sandbox']['migration_ids']) / $context['sandbox']['max']; }
/** * Tests rolling back configuration and content entities. */ public function testRollback() { // We use vocabularies to demonstrate importing and rolling back // configuration entities. $vocabulary_data_rows = [['id' => '1', 'name' => 'categories', 'weight' => '2'], ['id' => '2', 'name' => 'tags', 'weight' => '1']]; $ids = ['id' => ['type' => 'integer']]; $definition = ['id' => 'vocabularies', 'migration_tags' => ['Import and rollback test'], 'source' => ['plugin' => 'embedded_data', 'data_rows' => $vocabulary_data_rows, 'ids' => $ids], 'process' => ['vid' => 'id', 'name' => 'name', 'weight' => 'weight'], 'destination' => ['plugin' => 'entity:taxonomy_vocabulary']]; $vocabulary_migration = new Migration([], uniqid(), $definition); $vocabulary_id_map = $vocabulary_migration->getIdMap(); $this->assertTrue($vocabulary_migration->getDestinationPlugin()->supportsRollback()); // Import and validate vocabulary config entities were created. $vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this); $vocabulary_executable->import(); foreach ($vocabulary_data_rows as $row) { /** @var Vocabulary $vocabulary */ $vocabulary = Vocabulary::load($row['id']); $this->assertTrue($vocabulary); $map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]); $this->assertNotNull($map_row['destid1']); } // We use taxonomy terms to demonstrate importing and rolling back content // entities. $term_data_rows = [['id' => '1', 'vocab' => '1', 'name' => 'music'], ['id' => '2', 'vocab' => '2', 'name' => 'Bach'], ['id' => '3', 'vocab' => '2', 'name' => 'Beethoven']]; $ids = ['id' => ['type' => 'integer']]; $definition = ['id' => 'terms', 'migration_tags' => ['Import and rollback test'], 'source' => ['plugin' => 'embedded_data', 'data_rows' => $term_data_rows, 'ids' => $ids], 'process' => ['tid' => 'id', 'vid' => 'vocab', 'name' => 'name'], 'destination' => ['plugin' => 'entity:taxonomy_term'], 'migration_dependencies' => ['required' => ['vocabularies']]]; $term_migration = new Migration([], uniqid(), $definition); $term_id_map = $term_migration->getIdMap(); $this->assertTrue($term_migration->getDestinationPlugin()->supportsRollback()); // Pre-create a term, to make sure it isn't deleted on rollback. $preserved_term_ids[] = 1; $new_term = Term::create(['tid' => 1, 'vid' => 1, 'name' => 'music']); $new_term->save(); // Import and validate term entities were created. $term_executable = new MigrateExecutable($term_migration, $this); $term_executable->import(); // Also explicitly mark one row to be preserved on rollback. $preserved_term_ids[] = 2; $map_row = $term_id_map->getRowBySource(['id' => 2]); $dummy_row = new Row(['id' => 2], $ids); $term_id_map->saveIdMapping($dummy_row, [$map_row['destid1']], $map_row['source_row_status'], MigrateIdMapInterface::ROLLBACK_PRESERVE); foreach ($term_data_rows as $row) { /** @var Term $term */ $term = Term::load($row['id']); $this->assertTrue($term); $map_row = $term_id_map->getRowBySource(['id' => $row['id']]); $this->assertNotNull($map_row['destid1']); } // Rollback and verify the entities are gone. $term_executable->rollback(); foreach ($term_data_rows as $row) { $term = Term::load($row['id']); if (in_array($row['id'], $preserved_term_ids)) { $this->assertNotNull($term); } else { $this->assertNull($term); } $map_row = $term_id_map->getRowBySource(['id' => $row['id']]); $this->assertFalse($map_row); } $vocabulary_executable->rollback(); foreach ($vocabulary_data_rows as $row) { $term = Vocabulary::load($row['id']); $this->assertNull($term); $map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]); $this->assertFalse($map_row); } // Test that simple configuration is not rollbackable. $term_setting_rows = [['id' => 1, 'override_selector' => '0', 'terms_per_page_admin' => '10']]; $ids = ['id' => ['type' => 'integer']]; $definition = ['id' => 'taxonomy_settings', 'migration_tags' => ['Import and rollback test'], 'source' => ['plugin' => 'embedded_data', 'data_rows' => $term_setting_rows, 'ids' => $ids], 'process' => ['override_selector' => 'override_selector', 'terms_per_page_admin' => 'terms_per_page_admin'], 'destination' => ['plugin' => 'config', 'config_name' => 'taxonomy.settings'], 'migration_dependencies' => ['required' => ['vocabularies']]]; $settings_migration = new Migration([], uniqid(), $definition); $this->assertFalse($settings_migration->getDestinationPlugin()->supportsRollback()); }