diff --git a/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php b/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php new file mode 100644 index 0000000..bb7d0c2 --- /dev/null +++ b/src/Plugin/Derivative/MigrateEntityReferenceRevisions.php @@ -0,0 +1,30 @@ +entityDefinitions as $entityType => $entityInfo) { + if ($entityInfo->getKey('revision')) { + $this->derivatives[$entityType] = [ + 'id' => "entity_reference_revisions:$entityType", + 'class' => EntityReferenceRevisions::class, + 'requirements_met' => 1, + 'provider' => $entityInfo->getProvider(), + ]; + } + } + return $this->derivatives; + } + +} diff --git a/src/Plugin/migrate/destination/EntityReferenceRevisions.php b/src/Plugin/migrate/destination/EntityReferenceRevisions.php new file mode 100644 index 0000000..d41f27a --- /dev/null +++ b/src/Plugin/migrate/destination/EntityReferenceRevisions.php @@ -0,0 +1,147 @@ +save(); + + return [ + $this->getKey('id') => $entity->id(), + $this->getKey('revision') => $entity->getRevisionId(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + if ($revision_key = $this->getKey('revision')) { + $id_key = $this->getKey('id'); + $ids[$id_key]['type'] = 'integer'; + + // TODO: Improve after https://www.drupal.org/node/2783715 is finished. + $ids[$revision_key]['type'] = 'integer'; + + if ($this->isTranslationDestination()) { + if ($revision_key = $this->getKey('langcode')) { + $ids[$revision_key]['type'] = 'string'; + } + else { + throw new MigrateException('This entity type does not support translation.'); + } + } + + return $ids; + } + throw new MigrateException('This entity type does not support revisions.'); + } + + /** + * {@inheritdoc} + */ + protected function getEntity(Row $row, array $oldDestinationIdValues) { + $revision_id = $oldDestinationIdValues ? + array_pop($oldDestinationIdValues) : + $row->getDestinationProperty($this->getKey('revision')); + if (!empty($revision_id) && ($entity = $this->storage->loadRevision($revision_id))) { + $entity->setNewRevision(FALSE); + } + else { + // Attempt to ensure we always have a bundle. + if ($bundle = $this->getBundle($row)) { + $row->setDestinationProperty($this->getKey('bundle'), $bundle); + } + + // Stubs might need some required fields filled in. + if ($row->isStub()) { + $this->processStubRow($row); + } + $entity = $this->storage->create($row->getDestination()) + ->enforceIsNew(TRUE); + $entity->setNewRevision(TRUE); + } + $entity = $this->updateEntity($entity, $row) ?: $entity; + $this->rollbackAction = MigrateIdMapInterface::ROLLBACK_DELETE; + return $entity; + } + + /** + * {@inheritdoc} + */ + public function rollback(array $destination_identifiers) { + if ($this->isTranslationDestination()) { + $this->rollbackTranslation($destination_identifiers); + } + else { + $this->rollbackNonTranslation($destination_identifiers); + } + } + + /** + * Rollback translation destinations. + * + * @param array $destination_identifiers + * The IDs of the destination object to delete. + */ + protected function rollbackTranslation(array $destination_identifiers) { + $entity = $this->storage->loadRevision(array_pop($destination_identifiers)); + if ($entity && $entity instanceof TranslatableInterface) { + if ($key = $this->getKey('langcode')) { + if (isset($destination_identifier[$key])) { + $langcode = $destination_identifier[$key]; + if ($entity->hasTranslation($langcode)) { + // Make sure we don't remove the default translation. + $translation = $entity->getTranslation($langcode); + if (!$translation->isDefaultTranslation()) { + $entity->removeTranslation($langcode); + $entity->save(); + } + } + } + } + } + } + + /** + * Rollback non-translation destinations. + * + * @param array $destination_identifiers + * The IDs of the destination object to delete. + */ + protected function rollbackNonTranslation(array $destination_identifiers) { + $entity = $this->storage->loadRevision(array_pop($destination_identifiers)); + if ($entity) { + $entity->delete(); + } + } +} diff --git a/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php b/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php new file mode 100644 index 0000000..e68cf05 --- /dev/null +++ b/tests/src/Kernel/Plugin/Derivative/EntityReferenceRevisionsDeriverTest.php @@ -0,0 +1,46 @@ +installConfig($this->modules); + } + + /** + * Tests deriver. + * + * @covers ::getDerivativeDefinitions + */ + public function testDestinationDeriver() { + /** @var MigrateDestinationPluginManager $migrationDestinationManager */ + $migrationDestinationManager = \Drupal::service('plugin.manager.migrate.destination'); + + $destination = $migrationDestinationManager->getDefinition('entity_reference_revisions:entity_test_composite'); + $this->assertEquals(EntityReferenceRevisions::class, $destination['class']); + } + + + +} diff --git a/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php b/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php new file mode 100644 index 0000000..703a991 --- /dev/null +++ b/tests/src/Kernel/Plugin/migrate/destination/EntityReferenceRevisionsDestinationTest.php @@ -0,0 +1,486 @@ +installEntitySchema('entity_test_composite'); + $this->installSchema('system', ['sequences']); + $this->installConfig($this->modules); + + $this->migrationPluginManager = \Drupal::service('plugin.manager.migration'); + } + + /** + * Tests get entity type id. + * + * @dataProvider getEntityTypeIdDataProvider + * + * @covers ::getEntityTypeId + */ + public function testGetEntityTypeId(array $definition, $expected) { + /** @var Migration $migration */ + $migration = $this->migrationPluginManager->createStubMigration($definition); + /** @var EntityReferenceRevisions $destination */ + $destination = $migration->getDestinationPlugin(); + + /** @var EntityStorageBase $storage */ + $storage = $this->readAttribute($destination, 'storage'); + $actual = $this->readAttribute($storage, 'entityTypeId'); + + $this->assertEquals($expected, $actual); + } + + /** + * Provides multiple migration definitions for "getEntityTypeId" test. + */ + public function getEntityTypeIdDataProvider() { + $datas = $this->getEntityDataProvider(); + + foreach ($datas as &$data) { + $data['expected'] = 'entity_test_composite'; + } + + return $datas; + } + + /** + * Tests get entity. + * + * @dataProvider getEntityDataProvider + * + * @covers ::getEntity + * @covers ::rollback + * @covers ::rollbackNonTranslation + */ + public function testGetEntity(array $definition, array $expected) { + /** @var Migration $migration */ + $migration = $this->migrationPluginManager->createStubMigration($definition); + $migrationExecutable = (new MigrateExecutable($migration, $this)); + /** @var EntityStorageBase $storage */ + $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); + // Test inserting and updating by looping twice. + for ($i = 0; $i < 2; $i++) { + $migrationExecutable->import(); + $migration->getIdMap()->prepareUpdate(); + foreach ($expected as $data) { + $entity = $storage->loadRevision($data['id']); + $this->assertEquals($data['label'], $entity->label()); + } + } + $migrationExecutable->rollback(); + foreach ($expected as $data) { + $entity = $storage->loadRevision($data['id']); + $this->assertEmpty($entity); + } + } + + /** + * Provides multiple migration definitions for "getEntity" test. + */ + public function getEntityDataProvider() { + return [ + 'without keys' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'name' => 'content item 1a'], + ['id' => 1, 'name' => 'content item 1b'], + ['id' => 2, 'name' => 'content item 2'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'name', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'label' => 'content item 1a'], + ['id' => 2, 'label' => 'content item 1b'], + ['id' => 3, 'label' => 'content item 2'], + ], + ], + 'with keys' => [ + 'definition' => [ + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + ['id' => 1, 'revision_id' => 1, 'name' => 'content item 1'], + ['id' => 2, 'revision_id' => 2, 'name' => 'content item 2'], + ['id' => 3, 'revision_id' => 3, 'name' => 'content item 3'], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'text'], + ], + ], + 'process' => [ + 'id' => 'id', + 'revision_id' => 'revision_id', + 'name' => 'name', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'label' => 'content item 1'], + ['id' => 2, 'label' => 'content item 2'], + ['id' => 3, 'label' => 'content item 3'], + ], + ], + ]; + } + + /** + * Tests multi-value and single-value destination field linkage. + * + * @dataProvider destinationFieldMappingDataProvider + * + * @covers ::getEntity + * @covers ::rollback + * @covers ::rollbackNonTranslation + */ + public function testDestinationFieldMapping(array $datas) { + $this->enableModules(['node', 'field']); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('node', ['node_access']); + + // Create new content type. + $values = ['type' => 'article', 'name' => 'Article']; + $node_type = NodeType::create($values); + $node_type->save(); + + // Add the field_err_single field to the node type. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_err_single', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + 'cardinality' => 1, + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + // Add the field_err_multiple field to the node type. + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'field_err_multiple', + 'entity_type' => 'node', + 'type' => 'entity_reference_revisions', + 'settings' => [ + 'target_type' => 'entity_test_composite' + ], + 'cardinality' => -1, + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => 'article', + ]); + $field->save(); + + $definitions = []; + $instances = []; + foreach ($datas as $data) { + $definitions[$data['definition']['id']] = $data['definition']; + $instances[$data['definition']['id']] = $this->migrationPluginManager->createStubMigration($data['definition']); + } + + // Reflection is easier than mocking. We need to use createInstance for + // purposes of registering the migration for the migration process plugin. + $reflector = new \ReflectionObject($this->migrationPluginManager); + $property = $reflector->getProperty('definitions'); + $property->setAccessible(TRUE); + $property->setValue($this->migrationPluginManager, $definitions); + $this->container->set('plugin.manager.migration', $this->migrationPluginManager); + + foreach ($datas as $data) { + $migration = $this->migrationPluginManager->createInstance($data['definition']['id']); + $migrationExecutable = (new MigrateExecutable($migration, $this)); + /** @var EntityStorageBase $storage */ + $storage = $this->readAttribute($migration->getDestinationPlugin(), 'storage'); + $migrationExecutable->import(); + foreach ($data['expected'] as $expected) { + $entity = $storage->load($expected['id']); + $properties = array_diff_key($expected, array_flip(['id'])); + $nodes = node_load_multiple(); + foreach ($properties as $property => $value) { + if (is_array($value)) { + foreach ($value as $delta => $text) { + $this->assertEquals($text, $entity->{$property}[$delta]->entity->label()); + } + } + else { + $this->assertEquals($expected[$property], $entity->label()); + } + } + } + } + } + + /** + * Provides multiple migration definitions for "getEntity" test. + */ + public function destinationFieldMappingDataProvider() { + return [ + 'scenario 1' => [ + [ + 'single err' => [ + 'definition' => [ + 'id' => 'single_err', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'photo' => 'Photo1 here', + ], + [ + 'id' => 2, + 'photo' => 'Photo2 here', + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'name' => 'photo', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 1, 'name' => 'Photo1 here'], + ['id' => 2, 'name' => 'Photo2 here'], + ], + ], + 'multiple err author1' => [ + 'definition' => [ + 'id' => 'multiple_err_author1', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'author' => 'Author 1', + ], + [ + 'id' => 2, + 'author' => 'Author 2', + ], + ], + 'ids' => [ + 'author' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'author', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 3, 'name' => 'Author 1'], + ['id' => 4, 'name' => 'Author 2'], + ], + ], + 'multiple err author 2' => [ + 'definition' => [ + 'id' => 'multiple_err_author2', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'author' => 'Author 3', + ], + [ + 'id' => 2, + 'author' => 'Author 4', + ], + ], + 'ids' => [ + 'author' => ['type' => 'text'], + ], + ], + 'process' => [ + 'name' => 'author', + ], + 'destination' => [ + 'plugin' => 'entity_reference_revisions:entity_test_composite', + ], + ], + 'expected' => [ + ['id' => 5, 'name' => 'Author 3'], + ['id' => 6, 'name' => 'Author 4'], + ], + ], + 'destination entity' => [ + 'definition' => [ + 'id' => 'node_migration', + 'class' => Migration::class, + 'source' => [ + 'plugin' => 'embedded_data', + 'data_rows' => [ + [ + 'id' => 1, + 'title' => 'Article 1', + 'photo' => 'Photo1 here', + 'author' => ['Author 1', 'Author 3'], + ], + [ + 'id' => 2, + 'title' => 'Article 2', + 'photo' => 'Photo2 here', + 'author' => ['Author 2', 'Author 4'], + ], + ], + 'ids' => [ + 'id' => ['type' => 'integer'], + ], + ], + 'process' => [ + 'title' => 'title', + 'type' => [ + 'plugin' => 'default_value', + 'default_value' => 'article', + ], + 'field_err_single/target_id' => [ + [ + 'plugin' => 'migration', + 'migration' => ['single_err'], + 'no_stub' => TRUE, + 'source' => 'id', + ], + [ + 'plugin' => 'extract', + 'index' => [ + '0', + ], + ], + ], + 'field_err_single/target_revision_id' => [ + [ + 'plugin' => 'migration', + 'migration' => ['single_err'], + 'no_stub' => TRUE, + 'source' => 'id', + ], + [ + 'plugin' => 'extract', + 'index' => [ + 1, + ], + ], + ], + 'field_err_multiple' => [ + [ + 'plugin' => 'migration', + 'migration' => [ + 'multiple_err_author1', + 'multiple_err_author2', + ], + 'no_stub' => TRUE, + 'source' => 'author', + ], + [ + 'plugin' => 'iterator', + 'process' => [ + 'target_id' => '0', + 'target_revision_id' => '1', + ], + ], + ], + ], + 'destination' => [ + 'plugin' => 'entity:node', + ], + ], + 'expected' => [ + [ + 'id' => 1, + 'title' => 'Article 1', + 'field_err_single' => ['Photo1 here'], + 'field_err_multiple' => ['Author 1', 'Author 3'], + ], + [ + 'id' => 2, + 'title' => 'Article 2', + 'field_err_single' => ['Photo2 here'], + 'field_err_multiple' => ['Author 2', 'Author 4'], + ], + ], + ], + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function display($message, $type = 'status') { + $this->assertTrue($type == 'status', $message); + } + +}