diff --git a/core/modules/migrate_drupal/src/Plugin/migrate/source/d8/ContentEntity.php b/core/modules/migrate_drupal/src/Plugin/migrate/source/d8/ContentEntity.php new file mode 100644 index 0000000..ec7d95b --- /dev/null +++ b/core/modules/migrate_drupal/src/Plugin/migrate/source/d8/ContentEntity.php @@ -0,0 +1,309 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + parent::__construct($configuration + ['bundle' => NULL], $plugin_id, $plugin_definition, $migration, $state); + } + + /** + * {@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('state'), + $container->get('entity_type.manager'), + $container->get('entity_field.manager') + ); + } + + /** + * Returns all non-deleted field instances attached to a specific entity type. + * + * @param string $entity_type + * The entity type ID. + * @param string|null $bundle + * (optional) The bundle. + * + * @return array[] + * The field instances, keyed by field name. + */ + protected function getFields($entity_type, $bundle = NULL) { + $fieldConfig = $this->select('config', 'c') + ->fields('c') + ->condition('name', 'field.field.' . $entity_type . '.%', 'LIKE') + ->execute() + ->fetchAllAssoc('name'); + + $fields = []; + foreach ($fieldConfig as $config) { + $data = unserialize($config['data']); + // Status of false signifies the field is deleted. We do not return + // deleted data. + if ($data['status']) { + // If requested by configuration, filter by a bundle. Don't filter + // if it isn't configured. + if ($bundle && $data['bundle'] == $bundle) { + $fields[$data['field_name']] = $data; + } + else { + $fields[$data['field_name']] = $data; + } + } + } + + return $fields; + } + + /** + * Retrieves field values for a single field of a single entity. + * + * @param string $entity_type + * Entity type. + * @param string $field_name + * The field name. + * @param int $entity_id + * The entity ID. + * @param int|null $revision_id + * (optional) The entity revision ID. + * + * @throws \Drupal\migrate\MigrateException + * + * @return array + * The raw field values, keyed by delta. + * + * @todo Support multilingual field values. + */ + protected function getFieldValues($entity_type, $field_name, $entity_id, $revision_id = NULL) { + $table = $this->getDedicatedDataTableName($entity_type, $field_name); + + $query = $this->select($table, 't') + ->fields('t') + ->condition('entity_id', $entity_id) + ->condition('deleted', 0); + + if ($revision_id) { + $query->condition('revision_id', $revision_id); + } + + $values = []; + foreach ($query->execute() as $row) { + foreach ($row as $key => $value) { + $delta = $row['delta']; + if (strpos($key, $field_name) === 0) { + $column = substr($key, strlen($field_name) + 1); + $values[$delta][$column] = $value; + } + } + } + return $values; + } + + /** + * Get the table name keeping in mind the hashing logic for long names. + * + * @param string $entityType + * Entity type id. + * @param string $field_name + * Field name. + * @param bool $revision + * If revision table or not. + * + * @see \Drupal\Core\Entity\Sql\DefaultTableMapping::generateFieldTableName + * + * @throws \Drupal\migrate\MigrateException + * + * @return string + * The table name string. + */ + protected function getDedicatedDataTableName($entityType, $field_name, $revision = FALSE) { + $separator = $revision ? '_revision__' : '__'; + $tableName = $entityType . $separator . $field_name; + + // This matches \Drupal\Core\Entity\Sql\DefaultTableMapping where longer + // table revision names get shortened by the Entity API. + if (strlen($tableName) > 48) { + $separator = $revision ? '_r__' : '__'; + + $query = $this->select('config', 'c') + ->fields('c', ['data']) + ->condition('name', "field.storage.{$entityType}.{$field_name}"); + $fieldDefinitionData = $query->execute()->fetchField(); + + if ($fieldDefinitionData) { + $data = unserialize($fieldDefinitionData); + $uuid = $data['uuid']; + } + else { + throw new MigrateException(sprintf('Missing field storage config for field %s', $field_name)); + } + + $entityType = substr($entityType, 0, 34); + $fieldHash = substr(hash('sha256', $uuid), 0, 10); + $tableName = $entityType . $separator . $fieldHash; + } + return $tableName; + } + + /** + * {@inheritdoc} + */ + public function query() { + $entityDefinition = $this->entityTypeManager->getDefinition($this->configuration['entity_type']); + $idKey = $entityDefinition->getKey('id'); + $bundleKey = $entityDefinition->getKey('bundle'); + $baseTable = $entityDefinition->getBaseTable(); + $dataTable = $entityDefinition->getDataTable(); + + if ($dataTable) { + $query = $this->select($dataTable, 'd') + ->fields('d'); + $alias = $query->innerJoin($baseTable, 'b', "b.{$idKey} = d.{$idKey}"); + $query->fields($alias); + if (!empty($this->configuration['bundle'])) { + $query->condition("d.{$bundleKey}", $this->configuration['bundle']); + } + } + else { + $query = $this->select($baseTable, 'b') + ->fields('b'); + if (!empty($this->configuration['bundle'])) { + $query->condition("b.{$bundleKey}", $this->configuration['bundle']); + } + } + + return $query; + + } + + /** + * {@inheritdoc} + */ + public function fields() { + $fieldDefinitions = $this->entityFieldManager->getBaseFieldDefinitions($this->configuration['entity_type']); + $fields = []; + foreach ($fieldDefinitions as $fieldName => $definition) { + $fields[$fieldName] = (string) $definition->getLabel(); + } + return $fields; + } + + /** + * {@inheritdoc} + */ + public function prepareRow(Row $row) { + $entityType = $this->configuration['entity_type']; + // Get Field API field values. + if (!$this->bundleFields) { + $this->bundleFields = $this->getFields($entityType, $this->configuration['bundle']); + } + + $entityDefinition = $this->entityTypeManager->getDefinition($this->configuration['entity_type']); + $idKey = $entityDefinition->getKey('id'); + foreach (array_keys($this->bundleFields) as $field) { + $entityId = $row->getSourceProperty($idKey); + $revisionId = NULL; + if ($entityDefinition->isRevisionable()) { + $revisionKey = $entityDefinition->getKey('revision'); + $revisionId = $row->getSourceProperty($revisionKey); + } + $row->setSourceProperty($field, $this->getFieldValues($entityType, $field, $entityId, $revisionId)); + } + + return parent::prepareRow($row); + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $entityDefinition = $this->entityTypeManager->getDefinition($this->configuration['entity_type']); + $idKey = $entityDefinition->getKey('id'); + $ids[$idKey] = $this->getDefinitionFromEntity($idKey); + + if ($entityDefinition->isTranslatable()) { + $langcodeKey = $entityDefinition->getKey('langcode'); + $ids[$langcodeKey] = $this->getDefinitionFromEntity($langcodeKey); + } + + 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\FieldStorageDefinitionInterface[] $fieldDefinitions */ + $fieldDefinitions = $this->entityFieldManager->getBaseFieldDefinitions($this->configuration['entity_type']); + $fieldDefinition = $fieldDefinitions[$key]; + + return [ + 'alias' => 'b', + 'type' => $fieldDefinition->getType(), + ] + $fieldDefinition->getSettings(); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/d8/ContentEntityTest.php b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/d8/ContentEntityTest.php new file mode 100755 index 0000000..85d0340 --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/d8/ContentEntityTest.php @@ -0,0 +1,98 @@ +migrationDefinition(); + + // User tests. + $definition['source']['entity_type'] = 'user'; + $source = $migrationPluginManager->createStubMigration($definition)->getSourcePlugin(); + $ids = $source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('uid', $ids); + $fields = $source->fields(); + $this->assertArrayHasKey('name', $fields); + $this->assertArrayHasKey('pass', $fields); + $this->assertArrayHasKey('mail', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('roles', $fields); + $source->rewind(); + $values = $source->current()->getSource(); + $this->assertEquals('example@example.com', $values['mail']); + $this->assertEquals('user123', $values['name']); + $this->assertEquals($this->user->id(), $values['uid']); + + // File testing. + $definition['source']['entity_type'] = 'file'; + $source = $migrationPluginManager->createStubMigration($definition)->getSourcePlugin(); + $ids = $source->getIds(); + $this->assertArrayHasKey('fid', $ids); + $fields = $source->fields(); + $this->assertArrayHasKey('fid', $fields); + $this->assertArrayHasKey('filemime', $fields); + $this->assertArrayHasKey('filename', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('uri', $fields); + $source->rewind(); + $values = $source->current()->getSource(); + $this->assertEquals('text/plain', $values['filemime']); + $this->assertEquals('public://foo.txt', $values['uri']); + $this->assertEquals('foo.txt', $values['filename']); + $this->assertEquals(1, $values['fid']); + + // Node tests. + $definition['source']['entity_type'] = 'node'; + $definition['source']['bundle'] = $this->bundle; + $source = $migrationPluginManager->createStubMigration($definition)->getSourcePlugin(); + $ids = $source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('nid', $ids); + $fields = $source->fields(); + $this->assertArrayHasKey('nid', $fields); + $this->assertArrayHasKey('vid', $fields); + $this->assertArrayHasKey('title', $fields); + $this->assertArrayHasKey('uid', $fields); + $this->assertArrayHasKey('sticky', $fields); + $source->rewind(); + $values = $source->current()->getSource(); + $this->assertEquals($this->bundle, $values['type']); + $this->assertEquals('node', $values['entity_type']); + $this->assertEquals($this->node->id(), $values['nid']); + $this->assertEquals(1, $values['status']); + $this->assertEquals('Apples', $values['title']); + + // Media testing. + $definition['source']['entity_type'] = 'media'; + $definition['source']['bundle'] = 'image'; + $source = $migrationPluginManager->createStubMigration($definition)->getSourcePlugin(); + $ids = $source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('mid', $ids); + $fields = $source->fields(); + $this->assertArrayHasKey('bundle', $fields); + $this->assertArrayHasKey('mid', $fields); + $this->assertArrayHasKey('name', $fields); + $this->assertArrayHasKey('status', $fields); + $source->rewind(); + $values = $source->current()->getSource(); + $this->assertEquals(1, $values['mid']); + $this->assertEquals('Foo media', $values['name']); + $this->assertEquals('Foo media', $values['thumbnail__title']); + $this->assertEquals(1, $values['uid']); + $this->assertEquals('image', $values['bundle']); + } + +} diff --git a/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/d8/ContentEntityTestBase.php b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/d8/ContentEntityTestBase.php new file mode 100755 index 0000000..8837bee --- /dev/null +++ b/core/modules/migrate_drupal/tests/src/Kernel/Plugin/migrate/source/d8/ContentEntityTestBase.php @@ -0,0 +1,181 @@ + $this->bundle, 'name' => 'Article']); + $nodeType->save(); + + $this->installEntitySchema('node'); + $this->installEntitySchema('file'); + $this->installEntitySchema('media'); + $this->installEntitySchema('user'); + $this->installSchema('system', ['sequences']); + $this->installSchema('user', 'users_data'); + $this->installSchema('file', 'file_usage'); + + $this->installConfig($this->modules); + + // Create a media type. + $mediaType = $this->createMediaType('test'); + + // Create some data. + $this->user = User::create([ + 'name' => 'user123', + 'uid' => 1, + 'mail' => 'example@example.com', + ]); + $this->user->save(); + $this->node = Node::create([ + 'type' => $this->bundle, + 'title' => 'Apples', + 'uid' => $this->user->id(), + ]); + $this->node->save(); + $file = File::create([ + 'filename' => 'foo.txt', + 'uid' => $this->user->id(), + 'uri' => 'public://foo.txt', + ]); + $file->save(); + $media = Media::create([ + 'name' => 'Foo media', + 'uid' => $this->user->id(), + 'bundle' => $mediaType->id(), + ]); + $media->save(); + } + + /** + * Get a migration definition. + * + * @return array + * The definition. + */ + protected function migrationDefinition() { + return [ + 'source' => [ + 'plugin' => 'd8_content_entity', + 'key' => 'default', + ], + 'process' => [], + 'destination' => [ + 'plugin' => 'null', + ], + ]; + } + + /** + * Create a media type for a source plugin. + * + * @param string $media_source_name + * The name of the media source. + * + * @return \Drupal\media\MediaTypeInterface + * A media type. + */ + protected function createMediaType($media_source_name) { + $id = strtolower($this->randomMachineName()); + $media_type = MediaType::create([ + 'id' => 'image', + 'label' => 'Image', + 'source' => $media_source_name, + 'new_revision' => FALSE, + ]); + $media_type->save(); + $source_field = $media_type->getSource()->createSourceField($media_type); + // The media type form creates a source field if it does not exist yet. The + // same must be done in a kernel test, since it does not use that form. + // @see \Drupal\media\MediaTypeForm::save() + $source_field->getFieldStorageDefinition()->save(); + // The source field storage has been created, now the field can be saved. + $source_field->save(); + $media_type->set('source_configuration', [ + 'source_field' => $source_field->getName(), + ])->save(); + return $media_type; + } + + /** + * Reloads the given entity from the storage and returns it. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to be reloaded. + * + * @return \Drupal\Core\Entity\EntityInterface + * The reloaded entity. + */ + protected function reloadEntity(EntityInterface $entity) { + $controller = $this->container->get('entity_type.manager')->getStorage($entity->getEntityTypeId()); + $controller->resetCache([$entity->id()]); + return $controller->load($entity->id()); + } + +} diff --git a/core/modules/taxonomy/src/Plugin/migrate/source/d8/Term.php b/core/modules/taxonomy/src/Plugin/migrate/source/d8/Term.php new file mode 100644 index 0000000..1f64c72 --- /dev/null +++ b/core/modules/taxonomy/src/Plugin/migrate/source/d8/Term.php @@ -0,0 +1,59 @@ +getSourceProperty('tid'); + $parent = $this->taxonomyTermGetParent($tid); + if ($parent !== FALSE) { + $row->setSourceProperty('parent', $parent); + } + return parent::prepareRow($row); + } + + /** + * The parent value has custom storage, retrieve it directly. + * + * @param int $tid + * The term id. + * + * @return bool|int + * The parent term id or FALSE if there is none. + */ + protected function taxonomyTermGetParent($tid) { + /** @var \Drupal\Core\Database\Query\SelectInterface $query */ + $query = $this->select('taxonomy_term_hierarchy', 'h') + ->fields('h', ['parent']) + ->condition('tid', $tid); + return $query->execute()->fetchField(); + } + +} diff --git a/core/modules/taxonomy/tests/src/Kernel/Plugin/migrate/source/d8/TermTest.php b/core/modules/taxonomy/tests/src/Kernel/Plugin/migrate/source/d8/TermTest.php new file mode 100755 index 0000000..a39ca55 --- /dev/null +++ b/core/modules/taxonomy/tests/src/Kernel/Plugin/migrate/source/d8/TermTest.php @@ -0,0 +1,155 @@ +installConfig(['taxonomy']); + + $this->installEntitySchema('taxonomy_term'); + $this->installEntitySchema('taxonomy_vocabulary'); + $this->installSchema('node', 'node_access'); + + // 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 terms. + $term = Term::create([ + 'vid' => $this->vocabulary, + 'name' => 'Apples', + 'uid' => $this->user->id(), + ]); + $term->save(); + $term2 = Term::create([ + 'vid' => $this->vocabulary, + 'name' => 'Granny Smith', + 'uid' => $this->user->id(), + 'parent' => $term->id(), + ]); + $term2->save(); + $this->user = $this->reloadEntity($this->user); + $this->user->set($this->fieldName, $term->id()); + $this->user->save(); + $this->node = $this->reloadEntity($this->node); + $this->node->set($this->fieldName, $term->id()); + $this->node->save(); + } + + /** + * Tests table destination. + */ + public function testEntitySource() { + /** @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migrationPluginManager */ + $migrationPluginManager = \Drupal::service('plugin.manager.migration'); + $definition = $this->migrationDefinition(); + + // User tests. + $definition['source']['entity_type'] = 'user'; + $source = $migrationPluginManager->createStubMigration($definition)->getSourcePlugin(); + $source->rewind(); + $values = $source->current()->getSource(); + $this->assertEquals(1, $values[$this->fieldName][0]['target_id']); + + // Node tests. + $definition['source']['entity_type'] = 'node'; + $definition['source']['bundle'] = $this->bundle; + $source = $migrationPluginManager->createStubMigration($definition)->getSourcePlugin(); + $source->rewind(); + $values = $source->current()->getSource(); + $this->assertEquals(1, $values[$this->fieldName][0]['target_id']); + + // Term testing. + $definition['source']['plugin'] = 'd8_term'; + $definition['source']['bundle'] = $this->vocabulary; + $source = $migrationPluginManager->createStubMigration($definition)->getSourcePlugin(); + $ids = $source->getIds(); + $this->assertArrayHasKey('langcode', $ids); + $this->assertArrayHasKey('tid', $ids); + $fields = $source->fields(); + $this->assertArrayHasKey('vid', $fields); + $this->assertArrayHasKey('tid', $fields); + $this->assertArrayHasKey('name', $fields); + $source->rewind(); + $values = $source->current()->getSource(); + $this->assertEquals($this->vocabulary, $values['vid']); + $this->assertEquals(1, $values['tid']); + $this->assertEquals(0, $values['parent']); + $this->assertEquals('Apples', $values['name']); + $source->next(); + $values = $source->current()->getSource(); + $this->assertEquals($this->vocabulary, $values['vid']); + $this->assertEquals(2, $values['tid']); + $this->assertEquals(1, $values['parent']); + $this->assertEquals('Granny Smith', $values['name']); + } + +}