diff --git a/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php index 62f3d19..3a3487f 100644 --- a/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php +++ b/core/modules/migrate/tests/src/Kernel/Plugin/MigrationPluginListTest.php @@ -41,6 +41,7 @@ class MigrationPluginListTest extends KernelTestBase { 'menu_link_content', 'menu_ui', 'node', + 'options', 'path', 'search', 'shortcut', diff --git a/core/modules/migrate_drupal/migrations/d6_entity_reference_translation.yml b/core/modules/migrate_drupal/migrations/d6_entity_reference_translation.yml new file mode 100644 index 0000000..22a1850 --- /dev/null +++ b/core/modules/migrate_drupal/migrations/d6_entity_reference_translation.yml @@ -0,0 +1,22 @@ +id: d6_entity_reference_translation +label: Entity reference translations +migration_tags: + - Drupal 6 + - i18n Content +deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver +# Supported target types for entity reference translation migrations. The array +# keys are the supported target types and the values are arrays of migrations +# to lookup for the translated entity IDs. +target_types: + node: + - d6_node_translation +# The source plugin will be set by the deriver. +source: + plugin: empty + key: default + target: default +# The process pipeline will be set by the deriver. +process: [] +# The destination plugin will be set by the deriver. +destination: + plugin: null diff --git a/core/modules/migrate_drupal/migrations/d7_entity_reference_translation.yml b/core/modules/migrate_drupal/migrations/d7_entity_reference_translation.yml new file mode 100644 index 0000000..da6c87b --- /dev/null +++ b/core/modules/migrate_drupal/migrations/d7_entity_reference_translation.yml @@ -0,0 +1,22 @@ +id: d7_entity_reference_translation +label: Entity reference translations +migration_tags: + - Drupal 7 + - i18n Content +deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver +# Supported target types for entity reference translation migrations. The array +# keys are the supported target types and the values are arrays of migrations +# to lookup for the translated entity IDs. +target_types: + node: + - d7_node_translation +# The source plugin will be set by the deriver. +source: + plugin: empty + key: default + target: default +# The process pipeline will be set by the deriver. +process: [] +# The destination plugin will be set by the deriver. +destination: + plugin: null diff --git a/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php b/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php index b566c82..39f58d6 100644 --- a/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php +++ b/core/modules/migrate_drupal/src/MigrationConfigurationTrait.php @@ -96,6 +96,11 @@ protected function getMigrations($database_state_key, $drupal_version) { $all_migrations = $plugin_manager->createInstancesByTag($version_tag); $migrations = []; foreach ($all_migrations as $migration) { + // Skip migrations tagged with 'i18n Content', they will be run after all + // other migrations. + if (in_array('i18n Content', $migration->getMigrationTags(), TRUE)) { + continue; + } try { // @todo https://drupal.org/node/2681867 We should be able to validate // the entire migration at this point. diff --git a/core/modules/migrate_drupal/src/Plugin/MigrationFollowUpInterface.php b/core/modules/migrate_drupal/src/Plugin/MigrationFollowUpInterface.php new file mode 100644 index 0000000..93a8504 --- /dev/null +++ b/core/modules/migrate_drupal/src/Plugin/MigrationFollowUpInterface.php @@ -0,0 +1,18 @@ +basePluginId = $base_plugin_id; + $this->entityFieldManager = $entity_field_manager; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('entity_field.manager'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + // Get all entity reference fields. + $field_map = $this->entityFieldManager->getFieldMapByFieldType('entity_reference'); + + foreach ($field_map as $entity_type => $fields) { + foreach ($fields as $field_name => $field) { + foreach ($field['bundles'] as $bundle) { + $field_definitions = $this->entityFieldManager->getFieldDefinitions($entity_type, $bundle); + $target_type = $field_definitions[$field_name]->getSetting('target_type'); + + // If the field's target type is not supported, skip it. + if (!in_array($target_type, array_keys($base_plugin_definition['target_types']), TRUE)) { + continue; + } + + // Key derivatives by entity types and bundles. + $derivative = $entity_type . '__' . $bundle; + + if (!isset($this->derivatives[$derivative])) { + $this->derivatives[$derivative] = $base_plugin_definition; + + // Set the migration label. + $this->derivatives[$derivative]['label'] = $this->t('@label (@derivative)', [ + '@label' => $base_plugin_definition['label'], + '@derivative' => $derivative, + ]); + + // Set the source plugin. + $this->derivatives[$derivative]['source']['plugin'] = 'content_entity:' . $entity_type; + $this->derivatives[$derivative]['source']['bundle'] = $bundle; + + // Set the process pipeline. + $entity_type_definition = $this->entityTypeManager->getDefinition($entity_type); + $id_key = $entity_type_definition->getKey('id'); + $this->derivatives[$derivative]['process'][$id_key] = $id_key; + if ($entity_type_definition->isRevisionable() && $revision_key = $entity_type_definition->getKey('revision')) { + $this->derivatives[$derivative]['process'][$revision_key] = $revision_key; + } + if ($entity_type_definition->isTranslatable() && $langcode_key = $entity_type_definition->getKey('langcode')) { + $this->derivatives[$derivative]['process'][$langcode_key] = $langcode_key; + } + + // Set the destination plugin. + $this->derivatives[$derivative]['destination']['plugin'] = 'entity:' . $entity_type; + $this->derivatives[$derivative]['destination']['default_bundle'] = $bundle; + if ($entity_type_definition->isTranslatable()) { + $this->derivatives[$derivative]['destination']['translations'] = TRUE; + } + } + + // Allow overwriting the entity reference field. + $this->derivatives[$derivative]['destination']['overwrite_properties'][] = $field_name; + + // Add the entity reference field to the process pipeline. + $this->derivatives[$derivative]['process'][$field_name] = [ + 'plugin' => 'sub_process', + 'source' => $field_name, + 'process' => [ + 'target_id' => [ + [ + 'plugin' => 'migration_lookup', + 'source' => 'target_id', + 'migration' => $base_plugin_definition['target_types'][$target_type], + 'no_stub' => TRUE, + ], + [ + 'plugin' => 'skip_on_empty', + 'method' => 'row', + ], + [ + 'plugin' => 'extract', + 'index' => [0], + ], + ], + ], + ]; + } + } + } + + return $this->derivatives; + } + +} diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/field/NodeReference.php b/core/modules/migrate_drupal/src/Plugin/migrate/field/NodeReference.php index e8c2dd5..d8b756b 100644 --- a/core/modules/migrate_drupal/src/Plugin/migrate/field/NodeReference.php +++ b/core/modules/migrate_drupal/src/Plugin/migrate/field/NodeReference.php @@ -26,8 +26,7 @@ public function processFieldValues(MigrationInterface $migration, $field_name, $ 'source' => $field_name, 'process' => [ 'target_id' => [ - 'plugin' => 'migration_lookup', - 'migration' => 'd6_node', + 'plugin' => 'get', 'source' => 'nid', ], ], diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d6/FollowUpMigrationsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d6/FollowUpMigrationsTest.php new file mode 100644 index 0000000..33a1e35 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d6/FollowUpMigrationsTest.php @@ -0,0 +1,70 @@ +executeMigrations([ + 'language', + 'd6_language_content_settings', + 'd6_node', + 'd6_node_translation', + ]); + } + + /** + * Test entity reference translations. + */ + public function testEntityReferenceTranslations() { + // Test the entity reference field before the follow-up migrations. + $node = Node::load(10); + $this->assertSame([0 => ['target_id' => '13']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('fr'); + $this->assertSame([0 => ['target_id' => '20']], $translation->get('field_reference')->getValue()); + + $node = Node::load(12)->getTranslation('en'); + $this->assertSame([0 => ['target_id' => '10']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('fr'); + $this->assertSame([0 => ['target_id' => '11']], $translation->get('field_reference')->getValue()); + + // Run the follow-ups migrations. + $migration_plugin_manager = $this->container->get('plugin.manager.migration'); + $migration_plugin_manager->clearCachedDefinitions(); + $follow_up_migrations = $migration_plugin_manager->createInstances('d6_entity_reference_translation'); + $this->executeMigrations(array_keys($follow_up_migrations)); + + // Test the entity reference field after the follow-up migrations. + $node = Node::load(10); + $this->assertSame([0 => ['target_id' => '12']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('fr'); + $this->assertSame([0 => ['target_id' => '12']], $translation->get('field_reference')->getValue()); + + $node = Node::load(12)->getTranslation('en'); + $this->assertSame([0 => ['target_id' => '10']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('fr'); + $this->assertSame([0 => ['target_id' => '10']], $translation->get('field_reference')->getValue()); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Kernel/d7/FollowUpMigrationsTest.php b/core/modules/migrate_drupal/tests/src/Kernel/d7/FollowUpMigrationsTest.php new file mode 100644 index 0000000..c94faae --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/d7/FollowUpMigrationsTest.php @@ -0,0 +1,97 @@ +fileMigrationSetup(); + + $this->installEntitySchema('node'); + $this->installEntitySchema('comment'); + $this->installEntitySchema('taxonomy_term'); + $this->installConfig(static::$modules); + $this->installSchema('node', ['node_access']); + + $this->executeMigrations([ + 'language', + 'd7_user_role', + 'd7_user', + 'd7_node_type', + 'd7_language_content_settings', + 'd7_comment_type', + 'd7_taxonomy_vocabulary', + 'd7_field', + 'd7_field_instance', + 'd7_node', + 'd7_node_translation', + ]); + } + + /** + * Test entity reference translations. + */ + public function testEntityReferenceTranslations() { + // Test the entity reference field before the follow-up migrations. + $node = Node::load(2); + $this->assertSame([0 => ['target_id' => '5']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('is'); + $this->assertSame([0 => ['target_id' => '4']], $translation->get('field_reference')->getValue()); + + $node = Node::load(4); + $this->assertSame([0 => ['target_id' => '3']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('en'); + $this->assertSame([0 => ['target_id' => '2']], $translation->get('field_reference')->getValue()); + + // Run the follow-ups migrations. + $migration_plugin_manager = $this->container->get('plugin.manager.migration'); + $migration_plugin_manager->clearCachedDefinitions(); + $follow_up_migrations = $migration_plugin_manager->createInstances('d7_entity_reference_translation'); + $this->executeMigrations(array_keys($follow_up_migrations)); + + // Test the entity reference field after the follow-up migrations. + $node = Node::load(2); + $this->assertSame([0 => ['target_id' => '4']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('is'); + $this->assertSame([0 => ['target_id' => '4']], $translation->get('field_reference')->getValue()); + + $node = Node::load(4); + $this->assertSame([0 => ['target_id' => '2']], $node->get('field_reference')->getValue()); + $translation = $node->getTranslation('en'); + $this->assertSame([0 => ['target_id' => '2']], $translation->get('field_reference')->getValue()); + } + +} diff --git a/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php index 12dc5c1..49f1be1 100644 --- a/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php +++ b/core/modules/migrate_drupal_ui/src/Batch/MigrateUpgradeImportBatch.php @@ -9,11 +9,13 @@ use Drupal\migrate\Plugin\MigrationInterface; use Drupal\migrate\Event\MigrateEvents; use Drupal\migrate\Event\MigrateIdMapMessageEvent; +use Drupal\migrate\Event\MigrateImportEvent; use Drupal\migrate\Event\MigrateMapDeleteEvent; use Drupal\migrate\Event\MigrateMapSaveEvent; use Drupal\migrate\Event\MigratePostRowSaveEvent; use Drupal\migrate\Event\MigrateRowDeleteEvent; use Drupal\migrate\MigrateExecutable; +use Drupal\migrate_drupal\Plugin\MigrationFollowUpInterface; /** * Runs a single migration batch. @@ -56,6 +58,13 @@ class MigrateUpgradeImportBatch { protected static $messages; /** + * The follow-up migrations. + * + * @var \Drupal\migrate\Plugin\MigrationInterface[]; + */ + protected static $followUpMigrations; + + /** * Runs a single migrate batch import. * * @param int[] $initial_ids @@ -69,6 +78,7 @@ public static function run($initial_ids, $config, &$context) { if (!static::$listenersAdded) { $event_dispatcher = \Drupal::service('event_dispatcher'); $event_dispatcher->addListener(MigrateEvents::POST_ROW_SAVE, [static::class, 'onPostRowSave']); + $event_dispatcher->addListener(MigrateEvents::POST_IMPORT, [static::class, 'onPostImport']); $event_dispatcher->addListener(MigrateEvents::MAP_SAVE, [static::class, 'onMapSave']); $event_dispatcher->addListener(MigrateEvents::IDMAP_MESSAGE, [static::class, 'onIdMapMessage']); @@ -138,6 +148,15 @@ public static function run($initial_ids, $config, &$context) { \Drupal::logger('migrate_drupal_ui')->notice($message); $context['sandbox']['num_processed'] = 0; $context['results']['successes']++; + if (!empty(static::$followUpMigrations)) { + foreach (static::$followUpMigrations as $migration_id => $migration) { + if (!in_array($migration_id, $context['sandbox']['migration_ids'])) { + $context['sandbox']['migration_ids'][] = $migration_id; + $context['sandbox']['max']++; + unset(static::$followUpMigrations[$migration_id]); + } + } + } break; case MigrationInterface::RESULT_INCOMPLETE: @@ -263,6 +282,19 @@ public static function onPostRowSave(MigratePostRowSaveEvent $event) { } /** + * Adds any follow-up migrations. + * + * @param \Drupal\migrate\Event\MigrateImportEvent $event + * The import event. + */ + public static function onPostImport(MigrateImportEvent $event) { + $migration = $event->getMigration(); + if ($migration instanceof MigrationFollowUpInterface) { + static::$followUpMigrations = $migration->generateFollowUpMigrations(); + } + } + + /** * Reacts to item deletion. * * @param \Drupal\migrate\Event\MigrateRowDeleteEvent $event diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php index 36925d2..eb3ff13 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeExecuteTestBase.php @@ -108,6 +108,7 @@ public function testMigrateUpgradeExecute() { $this->drupalPostForm(NULL, [], t('Perform upgrade')); $this->assertText(t('Congratulations, you upgraded Drupal!')); $this->assertMigrationResults($this->getEntityCounts(), $version); + $this->assertFollowUpMigrationResults($this->getFollowUpMigrationExpectedResults()); \Drupal::service('module_installer')->install(['forum']); \Drupal::service('module_installer')->install(['book']); diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php index af2916a..401a6f2 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeReviewPageTestBase.php @@ -150,4 +150,11 @@ protected function getEntityCountsIncremental() { return []; } + /** + * {@inheritdoc} + */ + protected function getFollowUpMigrationExpectedResults() { + return []; + } + } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php index c09b7ca..45598df 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/MigrateUpgradeTestBase.php @@ -166,6 +166,14 @@ protected function assertUpgradePaths(WebAssert $session, array $available_paths abstract protected function getEntityCountsIncremental(); /** + * Gets expected number of results per follow-up migrations. + * + * @return int[] + * An array of expected counts keyed by migration ID. + */ + abstract protected function getFollowUpMigrationExpectedResults(); + + /** * Helper method to assert the text on the 'Upgrade analysis report' page. * * @param \Drupal\Tests\WebAssert $session @@ -260,4 +268,19 @@ protected function assertMigrationResults(array $expected_counts, $version) { } } + /** + * Checks that follow-up migrations have been performed successfully. + * + * @param array $expected_results + * The follow-up migrations expected results. + */ + protected function assertFollowUpMigrationResults(array $expected_results) { + $plugin_manager = $this->container->get('plugin.manager.migration'); + foreach ($expected_results as $migration_id => $expected_result) { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $plugin_manager->createInstance($migration_id); + $this->assertSame($migration->getIdMap()->importedCount(), $expected_result); + } + } + } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php index 71eebe8..c86f237 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d6/MigrateUpgrade6Test.php @@ -63,8 +63,8 @@ protected function getEntityCounts() { 'contact_form' => 5, 'configurable_language' => 5, 'editor' => 2, - 'field_config' => 84, - 'field_storage_config' => 58, + 'field_config' => 85, + 'field_storage_config' => 59, 'file' => 8, 'filter_format' => 7, 'image_style' => 5, @@ -90,7 +90,7 @@ protected function getEntityCounts() { 'date_format' => 11, 'entity_form_display' => 29, 'entity_form_mode' => 1, - 'entity_view_display' => 53, + 'entity_view_display' => 55, 'entity_view_mode' => 14, 'base_field_override' => 38, ]; @@ -103,7 +103,7 @@ protected function getEntityCountsIncremental() { $counts = $this->getEntityCounts(); $counts['block_content'] = 3; $counts['comment'] = 7; - $counts['entity_view_display'] = 53; + $counts['entity_view_display'] = 55; $counts['entity_view_mode'] = 14; $counts['file'] = 9; $counts['menu_link_content'] = 6; @@ -183,6 +183,15 @@ protected function getMissingPaths() { } /** + * {@inheritdoc} + */ + protected function getFollowUpMigrationExpectedResults() { + return [ + 'd6_entity_reference_translation:node__page' => 4, + ]; + } + + /** * Executes all steps of migrations upgrade. */ public function testMigrateUpgradeExecute() { diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php index 49e793c..2200dc9 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php @@ -65,8 +65,8 @@ protected function getEntityCounts() { 'configurable_language' => 4, 'contact_form' => 3, 'editor' => 2, - 'field_config' => 63, - 'field_storage_config' => 46, + 'field_config' => 64, + 'field_storage_config' => 47, 'file' => 3, 'filter_format' => 7, 'image_style' => 6, @@ -181,6 +181,15 @@ protected function getMissingPaths() { } /** + * {@inheritdoc} + */ + protected function getFollowUpMigrationExpectedResults() { + return [ + 'd7_entity_reference_translation:node__article' => 2, + ]; + } + + /** * Executes all steps of migrations upgrade. */ public function testMigrateUpgradeExecute() { diff --git a/core/modules/node/migrations/d6_node_translation.yml b/core/modules/node/migrations/d6_node_translation.yml index 26cb024..b1345ed 100644 --- a/core/modules/node/migrations/d6_node_translation.yml +++ b/core/modules/node/migrations/d6_node_translation.yml @@ -2,7 +2,9 @@ id: d6_node_translation label: Node translations migration_tags: - Drupal 6 + - translation - Content +class: Drupal\node\Plugin\migrate\D6NodeTranslation deriver: Drupal\node\Plugin\migrate\D6NodeDeriver source: plugin: d6_node diff --git a/core/modules/node/migrations/d7_node_translation.yml b/core/modules/node/migrations/d7_node_translation.yml index 542ab33..b5d488e 100644 --- a/core/modules/node/migrations/d7_node_translation.yml +++ b/core/modules/node/migrations/d7_node_translation.yml @@ -4,6 +4,7 @@ migration_tags: - Drupal 7 - translation - Content +class: Drupal\node\Plugin\migrate\D7NodeTranslation deriver: Drupal\node\Plugin\migrate\D7NodeDeriver source: plugin: d7_node diff --git a/core/modules/node/src/Plugin/migrate/D6NodeTranslation.php b/core/modules/node/src/Plugin/migrate/D6NodeTranslation.php new file mode 100644 index 0000000..c302fd0 --- /dev/null +++ b/core/modules/node/src/Plugin/migrate/D6NodeTranslation.php @@ -0,0 +1,21 @@ +migrationPluginManager->clearCachedDefinitions(); + return $this->migrationPluginManager->createInstances('d6_entity_reference_translation'); + } + +} diff --git a/core/modules/node/src/Plugin/migrate/D7NodeTranslation.php b/core/modules/node/src/Plugin/migrate/D7NodeTranslation.php new file mode 100644 index 0000000..6662780 --- /dev/null +++ b/core/modules/node/src/Plugin/migrate/D7NodeTranslation.php @@ -0,0 +1,21 @@ +migrationPluginManager->clearCachedDefinitions(); + return $this->migrationPluginManager->createInstances('d7_entity_reference_translation'); + } + +}