Commit 7a160733 authored by catch's avatar catch

Issue #2912348 by maxocub, Jo Fitzgerald, masipila, heddn, phenaproxima:...

Issue #2912348 by maxocub, Jo Fitzgerald, masipila, heddn, phenaproxima: Handle entity_references related to Drupal 6 and 7 node translations with different IDs
parent faadb3b2
......@@ -41,6 +41,7 @@ class MigrationPluginListTest extends KernelTestBase {
'menu_link_content',
'menu_ui',
'node',
'options',
'path',
'search',
'shortcut',
......
......@@ -3,3 +3,8 @@
enforce_source_module_tags:
- Drupal 6
- Drupal 7
# Migrations with any of these tags will not be derived and executed with the
# other migrations. They will be derived and executed after the migrations on
# which they depend have been successfully executed.
follow_up_migration_tags:
- Follow-up migration
......@@ -8,3 +8,9 @@ migrate_drupal.settings:
sequence:
type: string
label: 'Tag'
follow_up_migration_tags:
type: sequence
label: 'Follow-up migration tags'
sequence:
type: string
label: 'Tag'
......@@ -14,3 +14,13 @@ function migrate_drupal_update_8501() {
->set('enforce_source_module_tags', ['Drupal 6', 'Drupal 7'])
->save();
}
/**
* Sets the follow-up migration tags.
*/
function migrate_drupal_update_8502() {
\Drupal::configFactory()
->getEditable('migrate_drupal.settings')
->set('follow_up_migration_tags', ['Follow-up migration'])
->save();
}
id: d6_entity_reference_translation
label: Entity reference translations
migration_tags:
- Drupal 6
- Follow-up migration
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
id: d7_entity_reference_translation
label: Entity reference translations
migration_tags:
- Drupal 7
- Follow-up migration
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
......@@ -12,6 +12,13 @@
*/
trait MigrationConfigurationTrait {
/**
* The follow-up migration tags.
*
* @var string[]
*/
protected $followUpMigrationTags;
/**
* Gets the database connection for the source Drupal database.
*
......@@ -96,6 +103,13 @@ 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 any of the follow-up migration tags. They
// will be derived and executed after the migrations on which they depend
// have been successfully executed.
// @see Drupal\migrate_drupal\Plugin\MigrationWithFollowUpInterface
if (!empty(array_intersect($migration->getMigrationTags(), $this->getFollowUpMigrationTags()))) {
continue;
}
try {
// @todo https://drupal.org/node/2681867 We should be able to validate
// the entire migration at this point.
......@@ -119,6 +133,20 @@ protected function getMigrations($database_state_key, $drupal_version) {
return $migrations;
}
/**
* Returns the follow-up migration tags.
*
* @return string[]
*/
protected function getFollowUpMigrationTags() {
if ($this->followUpMigrationTags === NULL) {
$this->followUpMigrationTags = \Drupal::configFactory()
->get('migrate_drupal.settings')
->get('follow_up_migration_tags') ?: [];
}
return $this->followUpMigrationTags;
}
/**
* Determines what version of Drupal the source database contains.
*
......
<?php
namespace Drupal\migrate_drupal\Plugin;
/**
* Interface for migrations with follow-up migrations.
*
* Some migrations need to be derived and executed after other migrations have
* been successfully executed. For example, a migration might need to be derived
* based on previously migrated data. For such a case, the migration dependency
* system is not enough since all migrations would still be derived before any
* one of them has been executed.
*
* Those "follow-up" migrations need to be tagged with the "Follow-up migration"
* tag (or any tag in the "follow_up_migration_tags" configuration) and thus
* they won't be derived with the other migrations.
*
* To get those follow-up migrations derived at the right time, the migrations
* on which they depend must implement this interface and generate them in the
* generateFollowUpMigrations() method.
*
* When the migrations implementing this interface have been successfully
* executed, the follow-up migrations will then be derived having access to the
* now migrated data.
*/
interface MigrationWithFollowUpInterface {
/**
* Generates follow-up migrations.
*
* When the migration implementing this interface has been succesfully
* executed, this method will be used to generate the follow-up migrations
* which depends on the now migrated data.
*
* @return \Drupal\migrate\Plugin\MigrationInterface[]
* The follow-up migrations.
*/
public function generateFollowUpMigrations();
}
<?php
namespace Drupal\migrate_drupal\Plugin\migrate;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Deriver for entity reference field translations.
*
* A migration will be created for every bundle with at least one entity
* reference field that is configured to point to one of the supported target
* entity types. The migrations will update the entity reference fields with
* values found in the mapping tables of the migrations associated with the
* target types.
*
* Example:
*
* @code
* id: d7_entity_reference_translation
* label: Entity reference translations
* migration_tags:
* - Drupal 7
* - Follow-up migration
* deriver: Drupal\migrate_drupal\Plugin\migrate\EntityReferenceTranslationDeriver
* target_types:
* node:
* - d7_node_translation
* source:
* plugin: empty
* key: default
* target: default
* process: []
* destination:
* plugin: null
* @endcode
*
* In this example, the only supported target type is 'node' and the associated
* migration for the mapping table lookup is 'd7_node_translation'.
*/
class EntityReferenceTranslationDeriver extends DeriverBase implements ContainerDeriverInterface {
use StringTranslationTrait;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* EntityReferenceTranslationDeriver constructor.
*
* @param string $base_plugin_id
* The base plugin ID.
* @param \Drupal\core\Entity\EntityFieldManagerInterface $entity_field_manager
* The entity field manager.
* @param \Drupal\core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct($base_plugin_id, EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager) {
$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 (!array_key_exists($target_type, $base_plugin_definition['target_types'])) {
continue;
}
// Key derivatives by entity types and bundles.
$derivative_key = $entity_type . '__' . $bundle;
$derivative = $base_plugin_definition;
// Set the migration label.
$derivative['label'] = $this->t('@label (@derivative)', [
'@label' => $base_plugin_definition['label'],
'@derivative' => $derivative_key,
]);
// Set the source plugin.
$derivative['source']['plugin'] = 'content_entity' . PluginBase::DERIVATIVE_SEPARATOR . $entity_type;
$derivative['source']['bundle'] = $bundle;
// Set the process pipeline.
$entity_type_definition = $this->entityTypeManager->getDefinition($entity_type);
$id_key = $entity_type_definition->getKey('id');
$derivative['process'][$id_key] = $id_key;
if ($entity_type_definition->isRevisionable()) {
$revision_key = $entity_type_definition->getKey('revision');
$derivative['process'][$revision_key] = $revision_key;
}
if ($entity_type_definition->isTranslatable()) {
$langcode_key = $entity_type_definition->getKey('langcode');
$derivative['process'][$langcode_key] = $langcode_key;
}
// Set the destination plugin.
$derivative['destination']['plugin'] = 'entity' . PluginBase::DERIVATIVE_SEPARATOR . $entity_type;
$derivative['destination']['default_bundle'] = $bundle;
if ($entity_type_definition->isTranslatable()) {
$derivative['destination']['translations'] = TRUE;
}
// Allow overwriting the entity reference field so we can update its
// values with the ones found in the mapping table.
$derivative['destination']['overwrite_properties'][$field_name] = $field_name;
// Add the entity reference field to the process pipeline.
$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],
],
],
],
];
if (!isset($this->derivatives[$derivative_key])) {
// If this is a new derivative, add it to the returned derivatives.
$this->derivatives[$derivative_key] = $derivative;
}
else {
// If this is an existing derivative, it means this bundle has more
// than one entity reference field. In that case, we only want to add
// the field to the process pipeline and make it overwritable.
$this->derivatives[$derivative_key]['process'] += $derivative['process'];
$this->derivatives[$derivative_key]['destination']['overwrite_properties'] += $derivative['destination']['overwrite_properties'];
}
}
}
}
return $this->derivatives;
}
}
......@@ -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',
],
],
......
......@@ -2752,6 +2752,30 @@
'active' => '1',
'locked' => '0',
))
->values(array(
'field_name' => 'field_reference',
'type' => 'nodereference',
'global_settings' => 'a:1:{s:19:"referenceable_types";a:11:{s:4:"page";s:4:"page";s:7:"article";i:0;s:7:"company";i:0;s:8:"employee";i:0;s:5:"forum";i:0;s:10:"test_event";i:0;s:9:"test_page";i:0;s:11:"test_planet";i:0;s:10:"test_story";i:0;s:7:"sponsor";i:0;s:5:"story";i:0;}}',
'required' => '0',
'multiple' => '0',
'db_storage' => '1',
'module' => 'nodereference',
'db_columns' => 'a:1:{s:3:"nid";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:0;s:5:"index";b:1;}}',
'active' => '1',
'locked' => '0',
))
->values(array(
'field_name' => 'field_reference_2',
'type' => 'nodereference',
'global_settings' => 'a:1:{s:19:"referenceable_types";a:11:{s:4:"page";s:4:"page";s:7:"article";i:0;s:7:"company";i:0;s:8:"employee";i:0;s:5:"forum";i:0;s:10:"test_event";i:0;s:9:"test_page";i:0;s:11:"test_planet";i:0;s:10:"test_story";i:0;s:7:"sponsor";i:0;s:5:"story";i:0;}}',
'required' => '0',
'multiple' => '0',
'db_storage' => '1',
'module' => 'nodereference',
'db_columns' => 'a:1:{s:3:"nid";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:0;s:5:"index";b:1;}}',
'active' => '1',
'locked' => '0',
))
->values(array(
'field_name' => 'field_test',
'type' => 'text',
......@@ -3134,6 +3158,30 @@
'widget_module' => 'number',
'widget_active' => '1',
))
->values(array(
'field_name' => 'field_reference',
'type_name' => 'page',
'weight' => '31',
'label' => 'Reference',
'widget_type' => 'nodereference_select',
'widget_settings' => 'a:4:{s:18:"autocomplete_match";s:8:"contains";s:4:"size";i:60;s:13:"default_value";a:1:{i:0;a:1:{s:3:"nid";s:0:"";}}s:17:"default_value_php";N;}',
'display_settings' => 'a:5:{s:5:"label";a:2:{s:6:"format";s:5:"above";s:7:"exclude";i:0;}i:5;a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}s:6:"teaser";a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}s:4:"full";a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}i:4;a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}}',
'description' => '',
'widget_module' => 'nodereference',
'widget_active' => '1',
))
->values(array(
'field_name' => 'field_reference_2',
'type_name' => 'page',
'weight' => '32',
'label' => 'Reference',
'widget_type' => 'nodereference_select',
'widget_settings' => 'a:4:{s:18:"autocomplete_match";s:8:"contains";s:4:"size";i:60;s:13:"default_value";a:1:{i:0;a:1:{s:3:"nid";s:0:"";}}s:17:"default_value_php";N;}',
'display_settings' => 'a:5:{s:5:"label";a:2:{s:6:"format";s:5:"above";s:7:"exclude";i:0;}i:5;a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}s:6:"teaser";a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}s:4:"full";a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}i:4;a:2:{s:6:"format";s:7:"default";s:7:"exclude";i:0;}}',
'description' => '',
'widget_module' => 'nodereference',
'widget_active' => '1',
))
->values(array(
'field_name' => 'field_test',
'type_name' => 'story',
......@@ -3493,13 +3541,71 @@
'not null' => FALSE,
'size' => 'normal',
),
'field_reference_nid' => array(
'type' => 'int',
'not null' => FALSE,
'size' => 'normal',
'unsigned' => TRUE,
),
'field_reference_2_nid' => array(
'type' => 'int',
'not null' => FALSE,
'size' => 'normal',
'unsigned' => TRUE,
),
),
'primary key' => array(
'vid',
),
'indexes' => array(
'field_reference_nid' => array(
'field_reference_nid',
),
'field_reference_2_nid' => array(
'field_reference_2_nid',
),
),
'mysql_character_set' => 'utf8',
));
$connection->insert('content_type_page')
->fields(array(
'vid',
'nid',
'field_text_field_value',
'field_reference_nid',
'field_reference_2_nid',
))
->values(array(
'vid' => '13',
'nid' => '10',
'field_text_field_value' => NULL,
'field_reference_nid' => '13',
'field_reference_2_nid' => '13',
))
->values(array(
'vid' => '14',
'nid' => '11',
'field_text_field_value' => NULL,
'field_reference_nid' => '20',
'field_reference_2_nid' => '20',
))
->values(array(
'vid' => '16',
'nid' => '13',
'field_text_field_value' => NULL,
'field_reference_nid' => '10',
'field_reference_2_nid' => '10',
))
->values(array(
'vid' => '23',
'nid' => '20',
'field_text_field_value' => NULL,
'field_reference_nid' => '11',
'field_reference_2_nid' => '11',
))
->execute();
$connection->schema()->createTable('content_type_story', array(
'fields' => array(
'nid' => array(
......@@ -43548,6 +43654,23 @@
'tnid' => '0',
'translate' => '0',
))
->values(array(
'nid' => '20',
'vid' => '23',
'type' => 'page',
'language' => 'fr',
'title' => 'Le peuple zoulou',
'uid' => '1',
'status' => '1',
'created' => '1520613038',
'changed' => '1520613305',
'comment' => '0',
'promote' => '1',
'moderate' => '0',
'sticky' => '0',
'tnid' => '12',
'translate' => '0',
))
->execute();
$connection->schema()->createTable('node_access', array(
......@@ -44206,6 +44329,17 @@
'timestamp' => '1501955771',
'format' => '1',
))
->values(array(
'nid' => '20',
'vid' => '23',
'uid' => '1',
'title' => 'Le peuple zoulou',
'body' => 'Le peuple zoulou.',
'teaser' => 'Le peuple zoulou.',
'log' => '',
'timestamp' => '1520613305',
'format' => '1',
))
->execute();
$connection->schema()->createTable('node_type', array(
......@@ -44472,7 +44606,7 @@
'custom' => '1',
'modified' => '1',
'locked' => '0',
'orig_type' => 'page',
'orig_type' => 'test_page',
))
->values(array(
'type' => 'test_planet',
<?php
namespace Drupal\Tests\migrate_drupal\Kernel\d6;
use Drupal\node\Entity\Node;
use Drupal\Tests\node\Kernel\Migrate\d6\MigrateNodeTestBase;
/**
* Tests follow-up migrations.
*
* @group migrate_drupal
*/
class FollowUpMigrationsTest extends MigrateNodeTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'content_translation',
'language',
'menu_ui',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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('13', $node->get('field_reference')->target_id);
$this->assertSame('13', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('20', $translation->get('field_reference')->target_id);
$this->assertSame('20', $translation->get('field_reference_2')->target_id);
$node = Node::load(12)->getTranslation('en');
$this->assertSame('10', $node->get('field_reference')->target_id);
$this->assertSame('10', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('11', $translation->get('field_reference')->target_id);
$this->assertSame('11', $translation->get('field_reference_2')->target_id);
// Run the follow-up 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('12', $node->get('field_reference')->target_id);
$this->assertSame('12', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('12', $translation->get('field_reference')->target_id);
$this->assertSame('12', $translation->get('field_reference_2')->target_id);
$node = Node::load(12)->getTranslation('en');
$this->assertSame('10', $node->get('field_reference')->target_id);
$this->assertSame('10', $node->get('field_reference_2')->target_id);
$translation = $node->getTranslation('fr');
$this->assertSame('10', $translation->get('field_reference')->target_id);
$this->assertSame('10', $translation->get('field_reference_2')->target_id);
}
}
<?php
namespace Drupal\Tests\migrate_drupal\Kernel\d7;
use Drupal\node\Entity\Node;
use Drupal\Tests\file\Kernel\Migrate\d7\FileMigrationSetupTrait;
/**
* Tests follow-up migrations.
*
* @group migrate_drupal
*/
class FollowUpMigrationsTest extends MigrateDrupal7TestBase {
use FileMigrationSetupTrait;
/**
* {@inheritdoc}
*/
public static $modules = [
'content_translation',
'comment',
'datetime',
'file',
'image',
'language',
'link',
'menu_ui',
'node',
'taxonomy',
'telephone',
'text',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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',