diff --git a/core/modules/migrate/migrate.install b/core/modules/migrate/migrate.install index 064b95a..582e5f7 100644 --- a/core/modules/migrate/migrate.install +++ b/core/modules/migrate/migrate.install @@ -5,6 +5,8 @@ * Contains install and update functions for Migrate. */ +use Drupal\Core\Field\BaseFieldDefinition; + /** * Remove load plugin references from existing migrations. */ @@ -16,3 +18,42 @@ function migrate_update_8001() { $migration->save(TRUE); } } + +/** + * Copy of Sql::getFieldSchema() for use by update hooks. + * + * @param array $id_definition + * The definition of the field having the structure as the items returned by + * MigrateSourceInterface or MigrateDestinationInterface::getIds(). + * + * @return array + * The database schema definition. + * + * @see \Drupal\migrate\Plugin\migrate\id_map\Sql::getFieldSchema() + */ +function _migrate_get_field_schema(array $id_definition) { + $type_parts = explode('.', $id_definition['type']); + if (count($type_parts) == 1) { + $type_parts[] = 'value'; + } + unset($id_definition['type']); + + // Get the field storage definition. + $definition = BaseFieldDefinition::create($type_parts[0]); + + // Get a list of setting keys belonging strictly to the field definition. + $default_field_settings = $definition->getSettings(); + // Separate field definition settings from custom settings. Custom settings + // are settings passed in $id_definition that are not part of field storage + // definition settings. + $field_settings = array_intersect_key($id_definition, $default_field_settings); + $custom_settings = array_diff_key($id_definition, $default_field_settings); + + // Resolve schema from field storage definition settings. + $schema = $definition + ->setSettings($field_settings) + ->getColumns()[$type_parts[1]]; + + // Merge back custom settings. + return $schema + $custom_settings; +} diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php index f8d4cae..9ebc4d0 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityContentBase.php @@ -89,7 +89,7 @@ public function import(Row $row, array $old_destination_id_values = []) { } $ids = $this->save($entity, $old_destination_id_values); - if (!empty($this->configuration['translations'])) { + if ($this->isTranslationDestination()) { $ids[] = $entity->language()->getId(); } return $ids; @@ -122,20 +122,42 @@ public function isTranslationDestination() { * {@inheritdoc} */ public function getIds() { - $id_key = $this->getKey('id'); - $ids[$id_key] = $this->getDefinitionFromEntity($id_key); + $ids = []; + $this->addDestinationId($ids, 'id'); if ($this->isTranslationDestination()) { - if (!$langcode_key = $this->getKey('langcode')) { - throw new MigrateException('This entity type does not support translation.'); - } - $ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key); + $this->addDestinationId($ids, 'langcode', new MigrateException('This entity type does not support translation.')); } return $ids; } /** + * Adds an entity key to the destination IDs. + * + * @param array $ids + * The current destination IDs, passed by reference. + * @param string $key + * The entity key to add. See + * \Drupal\Core\Entity\EntityTypeInterface::getKeys() for more info. + * @param \Exception $error + * (optional) An exception to throw if the current entity type does not have + * the specified $key. + * + * @throws \Exception + * If the current entity type does have the specified $key. + */ + protected function addDestinationId(array &$ids, $key, \Exception $error = NULL) { + $key = $this->getKey($key); + if ($key) { + $ids[$key] = $this->getDefinitionFromEntity($key); + } + elseif ($error) { + throw $error; + } + } + + /** * Updates an entity with the new values from row. * * @param \Drupal\Core\Entity\EntityInterface $entity @@ -143,8 +165,8 @@ public function getIds() { * @param \Drupal\migrate\Row $row * The row object to update from. * - * @return \Drupal\Core\Entity\EntityInterface|null - * An updated entity, or NULL if it's the same as the one passed in. + * @return \Drupal\Core\Entity\EntityInterface + * An updated entity from row values. */ protected function updateEntity(EntityInterface $entity, Row $row) { $empty_destinations = $row->getEmptyDestinationProperties(); diff --git a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php index 06c158a..f571cb4 100644 --- a/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php +++ b/core/modules/migrate/src/Plugin/migrate/destination/EntityRevision.php @@ -70,7 +70,9 @@ protected function getEntity(Row $row, array $old_destination_id_values) { $entity->enforceIsNew(FALSE); $entity->setNewRevision(TRUE); } - $this->updateEntity($entity, $row); + // We need to update the entity, so that the destination row IDs are + // correct. + $entity = $this->updateEntity($entity, $row); $entity->isDefaultRevision(FALSE); return $entity; } @@ -87,10 +89,14 @@ protected function save(ContentEntityInterface $entity, array $old_destination_i * {@inheritdoc} */ public function getIds() { - if ($key = $this->getKey('revision')) { - return [$key => $this->getDefinitionFromEntity($key)]; + $ids = []; + $this->addDestinationId($ids, 'revision', new MigrateException('This entity type does not support revisions.')); + + if ($this->isTranslationDestination()) { + $this->addDestinationId($ids, 'langcode', new MigrateException('This entity type does not support translation.')); } - throw new MigrateException('This entity type does not support revisions.'); + + return $ids; } /** diff --git a/core/modules/migrate/tests/src/Kernel/Plugin/EntityRevisionTest.php b/core/modules/migrate/tests/src/Kernel/Plugin/EntityRevisionTest.php new file mode 100644 index 0000000..d15446f --- /dev/null +++ b/core/modules/migrate/tests/src/Kernel/Plugin/EntityRevisionTest.php @@ -0,0 +1,131 @@ +installConfig('node'); + $this->installSchema('node', ['node_access']); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + } + + /** + * Tests that EntityRevision correctly handles revision translations. + */ + public function testRevisionTranslation() { + ConfigurableLanguage::createFromLangcode('fr')->save(); + + /** @var \Drupal\node\NodeInterface $node */ + $node = Node::create([ + 'type' => $this->createContentType()->id(), + 'title' => 'Default 1', + ]); + $node->addTranslation('fr', [ + 'title' => 'French 1', + ]); + $node->save(); + $node->setNewRevision(); + $node->setTitle('Default 2'); + $node->getTranslation('fr')->setTitle('French 2'); + $node->save(); + + $migration = [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'nid' => $node->id(), + 'vid' => $node->getRevisionId(), + 'langcode' => 'fr', + 'title' => 'Titre nouveau, tabarnak!', + ], + ], + 'ids' => [ + 'nid' => [ + 'type' => 'integer', + ], + 'vid' => [ + 'type' => 'integer', + ], + 'langcode' => [ + 'type' => 'string', + ], + ], + ], + 'process' => [ + 'nid' => 'nid', + 'vid' => 'vid', + 'langcode' => 'langcode', + 'title' => 'title', + ], + 'destination' => [ + 'plugin' => 'entity_revision:node', + 'translations' => TRUE, + ], + ]; + + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->container + ->get('plugin.manager.migration') + ->createStubMigration($migration); + + $this->executeMigration($migration); + + // The entity_revision destination uses the revision ID and langcode as its + // keys (the langcode is only used if the destination is configured for + // translation), so we should be able to look up the source IDs by revision + // ID and langcode. + $source_ids = $migration->getIdMap()->lookupSourceID([ + 'vid' => $node->getRevisionId(), + 'langcode' => 'fr', + ]); + $this->assertNotEmpty($source_ids); + $this->assertSame($node->id(), $source_ids['nid']); + $this->assertSame($node->getRevisionId(), $source_ids['vid']); + $this->assertSame('fr', $source_ids['langcode']); + + // Confirm the french revision was used in the migration, instead of the + // default revision. + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ + $entity_type_manager = \Drupal::entityTypeManager(); + $revision = $entity_type_manager->getStorage('node')->loadRevision(1); + $this->assertSame('Default 1', $revision->label()); + $this->assertSame('French 1', $revision->getTranslation('fr')->label()); + $revision = $entity_type_manager->getStorage('node')->loadRevision(2); + $this->assertSame('Default 2', $revision->label()); + $this->assertSame('Titre nouveau, tabarnak!', $revision->getTranslation('fr')->label()); + } + +} diff --git a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php index 8a6fe9a..5a63f06 100644 --- a/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php +++ b/core/modules/migrate/tests/src/Unit/destination/EntityRevisionTest.php @@ -230,6 +230,8 @@ public function save(ContentEntityInterface $entity, array $old_destination_id_v * workings of its implementation which would trickle into mock assertions. An * empty implementation avoids this. */ - protected function updateEntity(EntityInterface $entity, Row $row) {} + protected function updateEntity(EntityInterface $entity, Row $row) { + return $entity; + } }