diff --git a/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php b/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php index 7f7b7ef8cd..938752ad4d 100644 --- a/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php +++ b/core/modules/action/tests/src/Kernel/Plugin/migrate/source/ActionTest.php @@ -15,7 +15,7 @@ class ActionTest extends MigrateSqlSourceTestBase { /** * {@inheritdoc} */ - public static $modules = ['action', 'migrate_drupal']; + public static $modules = ['action', 'migrate_drupal', 'system']; /** * {@inheritdoc} diff --git a/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php b/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php index feece1f7bc..f8a362eef2 100644 --- a/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php +++ b/core/modules/book/tests/src/Kernel/Plugin/migrate/source/d6/BookTest.php @@ -13,7 +13,7 @@ class BookTest extends MigrateSqlSourceTestBase { /** * {@inheritdoc} */ - public static $modules = ['book', 'migrate_drupal']; + public static $modules = ['user', 'node', 'book', 'migrate_drupal']; /** * {@inheritdoc} diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntity.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntity.php new file mode 100644 index 0000000000..f8a83924d6 --- /dev/null +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntity.php @@ -0,0 +1,243 @@ + NULL, + 'exclude_translations' => FALSE, + ]; + + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager) { + if (empty($plugin_definition['entity_type'])) { + throw new InvalidPluginDefinitionException($plugin_id, 'Missing required "entity_type" definition'); + } + $this->entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->entityType = $this->entityTypeManager->getDefinition($plugin_definition['entity_type']); + if (!$this->entityType instanceof ContentEntityTypeInterface) { + throw new InvalidPluginDefinitionException($plugin_id, 'The "content_entity" source plugin only supports content entities.'); + } + if (!empty($this->configuration['bundle']) && !$this->entityType->hasKey('bundle')) { + throw new InvalidPluginDefinitionException('A bundle was provided but the entity type is not bundleable.'); + } + parent::__construct($configuration + $this->defaultConfiguration, $plugin_id, $plugin_definition, $migration); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $migration, + $container->get('entity_type.manager'), + $container->get('entity_field.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function __toString() { + return (string) $this->entityType->getPluralLabel(); + } + + /** + * Initializes the iterator with the source data. + * + * @return \Generator + * An generator of the data for this source. + */ + protected function initializeIterator() { + $ids = $this->query()->execute(); + return $this->yieldEntities($ids); + } + + /** + * Loads and yields entities, one at a time. + * + * @param array $ids + * The entity IDs. + * + * @return \Generator + * And iterable of loaded entities. + */ + protected function yieldEntities(array $ids) { + $storage = $this->entityTypeManager + ->getStorage($this->entityType->id()); + foreach ($ids as $id) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $storage->load($id); + yield $this->toArray($entity); + if (!$this->configuration['exclude_translations']) { + foreach ($entity->getTranslationLanguages(FALSE) as $language) { + yield $this->toArray($entity->getTranslation($language->getId())); + } + } + } + } + + /** + * Converts an entity to an array. + * + * Makes all IDs into flat values. All other values are returned as per + * $entity->toArray(), which is a nested array. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity to convert. + * + * @return array + * The entity, represented as an array. + */ + protected function toArray(ContentEntityInterface $entity) { + $return = $entity->toArray(); + // This is necessary because the IDs must be flat. They cannot be nested for + // the ID map. + foreach (array_keys($this->getIds()) as $id) { + /** @var \Drupal\Core\TypedData\Plugin\DataType\ItemList $value */ + $value = $entity->get($id); + // Force the IDs over top the previous values. + $return[$id] = $value->first()->getString(); + } + return $return; + } + + /** + * Query to retrieve entities. + * + * @return \Drupal\Core\Entity\Query\QueryInterface + * The query. + */ + public function query() { + $query = $this->entityTypeManager + ->getStorage($this->entityType->id()) + ->getQuery(); + if (!empty($this->configuration['bundle'])) { + $query->condition($this->entityType->getKey('bundle'), $this->configuration['bundle']); + } + return $query; + } + + /** + * {@inheritdoc} + */ + public function count($refresh = FALSE) { + // If no translations are included, then a simple query is possible. + if ($this->configuration['exclude_translations']) { + return parent::count($refresh); + } + // @TODO: Determine a better way to retrieve a valid count for translations. + // https://www.drupal.org/project/drupal/issues/2937166 + return -1; + } + + /** + * {@inheritdoc} + */ + protected function doCount() { + return $this->query()->count()->execute(); + } + + /** + * {@inheritdoc} + */ + public function fields() { + $field_definitions = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id()); + if (!empty($this->configuration['bundle'])) { + $field_definitions += $this->entityFieldManager->getFieldDefinitions($this->entityType->id(), $this->configuration['bundle']); + } + $fields = []; + foreach ($field_definitions as $field_name => $definition) { + $fields[$field_name] = (string) $definition->getLabel(); + } + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $id_key = $this->entityType->getKey('id'); + $ids[$id_key] = $this->getDefinitionFromEntity($id_key); + if ($this->entityType->isTranslatable()) { + $langcode_key = $this->entityType->getKey('langcode'); + $ids[$langcode_key] = $this->getDefinitionFromEntity($langcode_key); + } + return $ids; + } + + /** + * Gets the field definition from a specific entity base field. + * + * @param string $key + * The field ID key. + * + * @return array + * An associative array with a structure that contains the field type, keyed + * as 'type', together with field storage settings as they are returned by + * FieldStorageDefinitionInterface::getSettings(). + * + * @see \Drupal\migrate\Plugin\migrate\destination\EntityContentBase::getDefinitionFromEntity() + */ + protected function getDefinitionFromEntity($key) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ + $field_definition = $this->entityFieldManager->getBaseFieldDefinitions($this->entityType->id())[$key]; + return [ + 'alias' => 'b', + 'type' => $field_definition->getType(), + ] + $field_definition->getSettings(); + } + +} diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntityDeriver.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntityDeriver.php new file mode 100644 index 0000000000..ad0962976d --- /dev/null +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/ContentEntityDeriver.php @@ -0,0 +1,60 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $this->derivatives = []; + foreach ($this->entityTypeManager->getDefinitions() as $id => $definition) { + if ($definition instanceof ContentEntityTypeInterface) { + $this->derivatives[$id] = $base_plugin_definition; + // Provide this so the source can be used separate from a deriver. + $this->derivatives[$id]['entity_type'] = $id; + } + } + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php new file mode 100755 index 0000000000..140249ac7e --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/ContentEntityTest.php @@ -0,0 +1,383 @@ +installEntitySchema('node'); + $this->installEntitySchema('file'); + $this->installEntitySchema('media'); + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('taxonomy_vocabulary'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['sequences']); + $this->installSchema('user', 'users_data'); + $this->installSchema('file', 'file_usage'); + $this->installSchema('node', ['node_access']); + $this->installConfig($this->modules); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + + // Create article content type. + $nodeType = NodeType::create(['type' => $this->bundle, 'name' => 'Article']); + $nodeType->save(); + + // Create a vocabulary. + $vocabulary = Vocabulary::create([ + 'name' => $this->vocabulary, + 'description' => $this->vocabulary, + 'vid' => $this->vocabulary, + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + ]); + $vocabulary->save(); + + // Create a term reference field on node. + $this->createEntityReferenceField( + 'node', + $this->bundle, + $this->fieldName, + 'Term reference', + 'taxonomy_term', + 'default', + ['target_bundles' => [$this->vocabulary]], + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + // Create a term reference field on user. + $this->createEntityReferenceField( + 'user', + 'user', + $this->fieldName, + 'Term reference', + 'taxonomy_term', + 'default', + ['target_bundles' => [$this->vocabulary]], + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + + // Create some data. + $this->user = User::create([ + 'name' => 'user123', + 'uid' => 1, + 'mail' => 'example@example.com', + ]); + $this->user->save(); + + $term = Term::create([ + 'vid' => $this->vocabulary, + 'name' => 'Apples', + 'uid' => $this->user->id(), + ]); + $term->save(); + $this->user->set($this->fieldName, $term->id()); + $this->user->save(); + $node = Node::create([ + 'type' => $this->bundle, + 'title' => 'Apples', + $this->fieldName => $term->id(), + 'uid' => $this->user->id(), + ]); + $node->save(); + $node->addTranslation('fr', [ + 'title' => 'Pommes', + $this->fieldName => $term->id(), + ])->save(); + + $this->sourcePluginManager = $this->container->get('plugin.manager.migrate.source'); + $this->migrationPluginManager = $this->container->get('plugin.manager.migration'); + } + + /** + * Tests user source plugin. + */ + public function testUserSource() { + $configuration = [ + 'exclude_translations' => TRUE, + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:user')); + $userSource = $this->sourcePluginManager->createInstance('content_entity:user', $configuration, $migration); + $this->assertSame('user entities', $userSource->__toString()); + $this->assertEquals(1, $userSource->count()); + $ids = $userSource->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('uid', $ids); + $fields = $userSource->fields(); + $this->assertArrayHasKey('name', $fields); + $this->assertArrayHasKey('pass', $fields); + $this->assertArrayHasKey('mail', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('roles', $fields); + $userSource->rewind(); + $values = $userSource->current()->getSource(); + $this->assertEquals('example@example.com', $values['mail'][0]['value']); + $this->assertEquals('user123', $values['name'][0]['value']); + $this->assertEquals(1, $values['uid']); + $this->assertEquals(1, $values['field_entity_reference'][0]['target_id']); + } + + /** + * Tests file source plugin. + */ + public function testFileSource() { + $file = File::create([ + 'filename' => 'foo.txt', + 'uid' => $this->user->id(), + 'uri' => 'public://foo.txt', + ]); + $file->save(); + + $configuration = [ + 'exclude_translations' => TRUE, + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:file')); + $fileSource = $this->sourcePluginManager->createInstance('content_entity:file', $configuration, $migration); + $this->assertSame('file entities', $fileSource->__toString()); + $this->assertEquals(1, $fileSource->count()); + $ids = $fileSource->getIds(); + $this->assertArrayHasKey('fid', $ids); + $fields = $fileSource->fields(); + $this->assertArrayHasKey('fid', $fields); + $this->assertArrayHasKey('filemime', $fields); + $this->assertArrayHasKey('filename', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('uri', $fields); + $fileSource->rewind(); + $values = $fileSource->current()->getSource(); + $this->assertEquals('text/plain', $values['filemime'][0]['value']); + $this->assertEquals('public://foo.txt', $values['uri'][0]['value']); + $this->assertEquals('foo.txt', $values['filename'][0]['value']); + $this->assertEquals(1, $values['fid']); + } + + /** + * Tests node source plugin. + */ + public function testNodeSource() { + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:node')); + $nodeSource = $this->sourcePluginManager->createInstance('content_entity:node', ['bundle' => $this->bundle], $migration); + $this->assertSame('content items', $nodeSource->__toString()); + $ids = $nodeSource->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('nid', $ids); + $fields = $nodeSource->fields(); + $this->assertArrayHasKey('nid', $fields); + $this->assertArrayHasKey('vid', $fields); + $this->assertArrayHasKey('title', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('sticky', $fields); + $nodeSource->rewind(); + $values = $nodeSource->current()->getSource(); + $this->assertEquals($this->bundle, $values['type'][0]['target_id']); + $this->assertEquals(1, $values['nid']); + $this->assertEquals('en', $values['langcode']); + $this->assertEquals(1, $values['status'][0]['value']); + $this->assertEquals('Apples', $values['title'][0]['value']); + $this->assertEquals(1, $values['default_langcode'][0]['value']); + $this->assertEquals(1, $values['field_entity_reference'][0]['target_id']); + $nodeSource->next(); + $values = $nodeSource->current()->getSource(); + $this->assertEquals($this->bundle, $values['type'][0]['target_id']); + $this->assertEquals(1, $values['nid']); + $this->assertEquals('fr', $values['langcode']); + $this->assertEquals(1, $values['status'][0]['value']); + $this->assertEquals('Pommes', $values['title'][0]['value']); + $this->assertEquals(0, $values['default_langcode'][0]['value']); + $this->assertEquals(1, $values['field_entity_reference'][0]['target_id']); + } + + /** + * Tests media source plugin. + */ + public function testMediaSource() { + $values = [ + 'id' => 'image', + 'bundle' => 'image', + 'label' => 'Image', + 'source' => 'test', + 'new_revision' => FALSE, + ]; + $mediaType = $this->createMediaType($values); + $media = Media::create([ + 'name' => 'Foo media', + 'uid' => $this->user->id(), + 'bundle' => $mediaType->id(), + ]); + $media->save(); + + $configuration = [ + 'exclude_translations' => TRUE, + 'bundle' => 'image', + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:media')); + $mediaSource = $this->sourcePluginManager->createInstance('content_entity:media', $configuration, $migration); + $this->assertSame('media items', $mediaSource->__toString()); + $this->assertEquals(1, $mediaSource->count()); + $ids = $mediaSource->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('mid', $ids); + $fields = $mediaSource->fields(); + $this->assertArrayHasKey('bundle', $fields); + $this->assertArrayHasKey('mid', $fields); + $this->assertArrayHasKey('name', $fields); + $this->assertArrayHasKey('status', $fields); + $mediaSource->rewind(); + $values = $mediaSource->current()->getSource(); + $this->assertEquals(1, $values['mid']); + $this->assertEquals('Foo media', $values['name'][0]['value']); + $this->assertEquals('Foo media', $values['thumbnail'][0]['title']); + $this->assertEquals(1, $values['uid'][0]['target_id']); + $this->assertEquals('image', $values['bundle'][0]['target_id']); + } + + /** + * Tests term source plugin. + */ + public function testTermSource() { + $term2 = Term::create([ + 'vid' => $this->vocabulary, + 'name' => 'Granny Smith', + 'uid' => $this->user->id(), + 'parent' => 1, + ]); + $term2->save(); + + $configuration = [ + 'exclude_translations' => TRUE, + 'bundle' => $this->vocabulary, + ]; + $migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition('content_entity:taxonomy_term')); + $termSource = $this->sourcePluginManager->createInstance('content_entity:taxonomy_term', $configuration, $migration); + $this->assertSame('taxonomy term entities', $termSource->__toString()); + $this->assertEquals(2, $termSource->count()); + $ids = $termSource->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('tid', $ids); + $fields = $termSource->fields(); + $this->assertArrayHasKey('vid', $fields); + $this->assertArrayHasKey('tid', $fields); + $this->assertArrayHasKey('name', $fields); + $termSource->rewind(); + $values = $termSource->current()->getSource(); + $this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']); + $this->assertEquals(1, $values['tid']); + $this->assertEquals(0, $values['parent'][0]['target_id']); + $this->assertEquals('Apples', $values['name'][0]['value']); + $termSource->next(); + $values = $termSource->current()->getSource(); + $this->assertEquals($this->vocabulary, $values['vid'][0]['target_id']); + $this->assertEquals(2, $values['tid']); + $this->assertEquals(1, $values['parent'][0]['target_id']); + $this->assertEquals('Granny Smith', $values['name'][0]['value']); + } + + /** + * Get a migration definition. + * + * @param string $plugin_id + * The plugin id. + * + * @return array + * The definition. + */ + protected function migrationDefinition($plugin_id) { + return [ + 'source' => [ + 'plugin' => $plugin_id, + ], + 'process' => [], + 'destination' => [ + 'plugin' => 'null', + ], + ]; + } + +}