diff --git a/migrations/d7_field_collection.yml b/migrations/d7_field_collection.yml new file mode 100644 index 0000000..4e4e88b --- /dev/null +++ b/migrations/d7_field_collection.yml @@ -0,0 +1,19 @@ +id: d7_field_collection +label: Field Collections +migration_tags: + - Drupal 7 + - Content + - Field Collection Content +deriver: Drupal\paragraphs\Plugin\migrate\D7FieldCollectionItemDeriver +source: + plugin: d7_field_collection_item +process: + type: bundle + +destination: + plugin: entity_reference_revisions:paragraph +migration_dependencies: + required: + - d7_field_collection_type + optional: + - d7_field_instance diff --git a/migrations/d7_field_collection_revisions.yml b/migrations/d7_field_collection_revisions.yml new file mode 100644 index 0000000..7994340 --- /dev/null +++ b/migrations/d7_field_collection_revisions.yml @@ -0,0 +1,28 @@ +id: d7_field_collection_revisions +label: Field Collection Revisions +migration_tags: + - Drupal 7 + - Content + - Field Collection Revisions Content +deriver: Drupal\paragraphs\Plugin\migrate\D7FieldCollectionItemDeriver +source: + plugin: d7_field_collection_item_revision +process: + id: + - + plugin: paragraphs_lookup + tags: + - Field Collection Content + source: item_id + - + plugin: extract + index: + - + id + type: bundle +destination: + plugin: entity_reference_revisions:paragraph + new_revisions: TRUE +migration_dependencies: + required: + - d7_field_collection diff --git a/migrations/d7_field_collection_type.yml b/migrations/d7_field_collection_type.yml index 79227c3..b0cdceb 100644 --- a/migrations/d7_field_collection_type.yml +++ b/migrations/d7_field_collection_type.yml @@ -2,6 +2,7 @@ id: d7_field_collection_type label: Paragraphs - Field Collection type configuration migration_tags: - Drupal 7 + - Configuration source: plugin: d7_field_collection_type add_description: true diff --git a/migrations/d7_paragraphs.yml b/migrations/d7_paragraphs.yml new file mode 100644 index 0000000..ad82d46 --- /dev/null +++ b/migrations/d7_paragraphs.yml @@ -0,0 +1,17 @@ +id: d7_paragraphs +label: Paragraphs +migration_tags: + - Drupal 7 + - Paragraphs Content +deriver: Drupal\paragraphs\Plugin\migrate\D7ParagraphsItemDeriver +source: + plugin: d7_paragraphs_item +process: + type: bundle +destination: + plugin: entity_reference_revisions:paragraph +migration_dependencies: + required: + - d7_paragraphs_type + optional: + - d7_field_instance diff --git a/migrations/d7_paragraphs_revisions.yml b/migrations/d7_paragraphs_revisions.yml new file mode 100644 index 0000000..1ca5f4f --- /dev/null +++ b/migrations/d7_paragraphs_revisions.yml @@ -0,0 +1,27 @@ +id: d7_paragraphs_revisions +label: Paragraphs Revisions +migration_tags: + - Drupal 7 + - Paragraphs Revisions Content +deriver: Drupal\paragraphs\Plugin\migrate\D7ParagraphsItemDeriver +source: + plugin: d7_paragraphs_item_revision +process: + id: + - + plugin: paragraphs_lookup + tags: + - Paragraphs Content + source: item_id + - + plugin: extract + index: + - + id + type: bundle +destination: + plugin: entity_reference_revisions:paragraph + new_revisions: TRUE +migration_dependencies: + required: + - d7_paragraphs diff --git a/src/Plugin/migrate/D7FieldCollectionItemDeriver.php b/src/Plugin/migrate/D7FieldCollectionItemDeriver.php new file mode 100644 index 0000000..ddfb477 --- /dev/null +++ b/src/Plugin/migrate/D7FieldCollectionItemDeriver.php @@ -0,0 +1,169 @@ +basePluginId = $base_plugin_id; + $this->cckPluginManager = $cck_manager; + $this->fieldPluginManager = $field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('plugin.manager.migrate.cckfield'), + $container->get('plugin.manager.migrate.field') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $types = static::getSourcePlugin('d7_field_collection_type'); + + try { + $types->checkRequirements(); + } + catch (RequirementsException $e) { + return $this->derivatives; + } + + $fields = []; + try { + $source_plugin = static::getSourcePlugin('d7_field_instance'); + $source_plugin->checkRequirements(); + + foreach ($source_plugin as $row) { + if ($row->getSourceProperty('entity_type') == 'field_collection_item') { + $fields[$row->getSourceProperty('bundle')][$row->getSourceProperty('field_name')] = $row->getSource(); + } + } + } + catch (RequirementsException $e) { + // No fields, no problem. We can keep going. + } + + try { + foreach ($types as $row) { + /** @var \Drupal\migrate\Row $row */ + $values = $base_plugin_definition; + $fc_bundle = $row->getSourceProperty('field_name'); + $p_bundle = $row->getSourceProperty('bundle'); + $values['label'] = t('@label (@type)', [ + '@label' => $values['label'], + '@type' => $row->getSourceProperty('name'), + ]); + $values['source']['field_name'] = $fc_bundle; + $values['destination']['default_bundle'] = $p_bundle; + + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createStubMigration($values); + if (isset($fields[$fc_bundle])) { + $migration->setProcessOfProperty('parent_id', 'parent_id'); + $migration->setProcessOfProperty('parent_type', 'parent_type'); + $migration->setProcessOfProperty('parent_field_name', 'field_name'); + + foreach ($fields[$fc_bundle] as $field_name => $info) { + $field_type = $info['type']; + try { + $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration); + if (!isset($this->fieldPluginCache[$field_type])) { + $this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, ['core' => 7], $migration); + } + $this->fieldPluginCache[$field_type] + ->processFieldValues($migration, $field_name, $info); + } + catch (PluginNotFoundException $ex) { + try { + $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration); + if (!isset($this->cckPluginCache[$field_type])) { + $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 7], $migration); + } + $this->cckPluginCache[$field_type] + ->processCckFieldValues($migration, $field_name, $info); + } + catch (PluginNotFoundException $ex) { + $migration->setProcessOfProperty($field_name, $field_name); + } + } + } + } + $this->derivatives[$p_bundle] = $migration->getPluginDefinition(); + } + } + catch (DatabaseExceptionWrapper $e) { + // Once we begin iterating the source plugin it is possible that the + // source tables will not exist. This can happen when the + // MigrationPluginManager gathers up the migration definitions but we do + // not actually have a Drupal 7 source database. + } + return $this->derivatives; + } + +} diff --git a/src/Plugin/migrate/D7ParagraphsItemDeriver.php b/src/Plugin/migrate/D7ParagraphsItemDeriver.php new file mode 100644 index 0000000..283481f --- /dev/null +++ b/src/Plugin/migrate/D7ParagraphsItemDeriver.php @@ -0,0 +1,165 @@ +basePluginId = $base_plugin_id; + $this->cckPluginManager = $cck_manager; + $this->fieldPluginManager = $field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('plugin.manager.migrate.cckfield'), + $container->get('plugin.manager.migrate.field') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + + $types = static::getSourcePlugin('d7_paragraphs_type'); + try { + $types->checkRequirements(); + } + catch (RequirementsException $e) { + return $this->derivatives; + } + + $fields = []; + try { + $source_plugin = static::getSourcePlugin('d7_field_instance'); + $source_plugin->checkRequirements(); + + foreach ($source_plugin as $row) { + if ($row->getSourceProperty('entity_type') == 'paragraphs_item') { + $fields[$row->getSourceProperty('bundle')][$row->getSourceProperty('field_name')] = $row->getSource(); + } + } + } + catch (RequirementsException $e) { + + } + + try { + foreach ($types as $row) { + $values = $base_plugin_definition; + $p_field_name = $row->getSourceProperty('field_name'); + $bundle = $row->getSourceProperty('bundle'); + $values['label'] = t('@label (@type)', [ + '@label' => $values['label'], + '@type' => $row->getSourceProperty('name'), + ]); + $values['source']['bundle'] = $p_field_name; + $values['destination']['default_bundle'] = $bundle; + + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createStubMigration($values); + if (isset($fields[$bundle])) { + + foreach ($fields[$bundle] as $field_name => $info) { + $field_type = $info['type']; + try { + $plugin_id = $this->fieldPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration); + if (!isset($this->fieldPluginCache[$field_type])) { + $this->fieldPluginCache[$field_type] = $this->fieldPluginManager->createInstance($plugin_id, ['core' => 7], $migration); + } + $this->fieldPluginCache[$field_type] + ->processFieldValues($migration, $field_name, $info); + } + catch (PluginNotFoundException $ex) { + try { + $plugin_id = $this->cckPluginManager->getPluginIdFromFieldType($field_type, ['core' => 7], $migration); + if (!isset($this->cckPluginCache[$field_type])) { + $this->cckPluginCache[$field_type] = $this->cckPluginManager->createInstance($plugin_id, ['core' => 7], $migration); + } + $this->cckPluginCache[$field_type] + ->processCckFieldValues($migration, $field_name, $info); + } + catch (PluginNotFoundException $ex) { + $migration->setProcessOfProperty($field_name, $field_name); + } + } + } + } + $this->derivatives[$bundle] = $migration->getPluginDefinition(); + } + } + catch (DatabaseExceptionWrapper $e) { + // Once we begin iterating the source plugin it is possible that the + // source tables will not exist. This can happen when the + // MigrationPluginManager gathers up the migration definitions but we do + // not actually have a Drupal 7 source database. + } + return $this->derivatives; + } + +} diff --git a/src/Plugin/migrate/field/FieldCollection.php b/src/Plugin/migrate/field/FieldCollection.php index c21997b..0bc8448 100644 --- a/src/Plugin/migrate/field/FieldCollection.php +++ b/src/Plugin/migrate/field/FieldCollection.php @@ -23,11 +23,65 @@ use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase; */ class FieldCollection extends FieldPluginBase { - /** + /* * Length of the 'field_' prefix that field collection prepends to bundles. */ const FIELD_COLLECTION_PREFIX_LENGTH = 6; + /** + * {@inheritdoc} + */ + public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) { + $process = [ + 'plugin' => 'sub_process', + 'source' => $field_name, + 'process' => [ + 'target_id' => [ + [ + 'plugin' => 'paragraphs_lookup', + 'tags' => 'Field Collection Content', + 'source' => 'value', + ], + [ + 'plugin' => 'extract', + 'index' => ['id'], + ], + ], + 'target_revision_id' => [ + [ + 'plugin' => 'paragraphs_lookup', + 'tags' => [ + 'Field Collection Revisions Content', + 'Field Collection Content', + ], + 'tag_ids' => [ + 'Field Collection Revisions Content' => ['revision_id'], + 'Field Collection Content' => ['value'], + ], + ], + [ + 'plugin' => 'extract', + 'index' => ['revision_id'], + ], + ], + ], + ]; + $migration->setProcessOfProperty($field_name, $process); + + // Add the respective field collection migration as a dependency. + $dependencies = $migration->getMigrationDependencies(); + $migration_dependency = 'd7_field_collection:' . substr($field_name, static::FIELD_COLLECTION_PREFIX_LENGTH); + $dependencies['required'][] = $migration_dependency; + $migration->set('migration_dependencies', $dependencies); + } + + /** + * {@inheritdoc} + */ + public function processFieldValues(MigrationInterface $migration, $field_name, $data) { + $this->defineValueProcessPipeline($migration, $field_name, $data); + } + /** * {@inheritdoc} */ diff --git a/src/Plugin/migrate/field/Paragraphs.php b/src/Plugin/migrate/field/Paragraphs.php index cf7f1ee..4ab0ce2 100644 --- a/src/Plugin/migrate/field/Paragraphs.php +++ b/src/Plugin/migrate/field/Paragraphs.php @@ -23,6 +23,56 @@ use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase; */ class Paragraphs extends FieldPluginBase { + /** + * {@inheritdoc} + */ + public function defineValueProcessPipeline(MigrationInterface $migration, $field_name, $data) { + $process = [ + 'plugin' => 'sub_process', + 'source' => $field_name, + 'process' => [ + 'target_id' => [ + [ + 'plugin' => 'paragraphs_lookup', + 'tags' => 'Paragraphs Content', + 'source' => 'value', + ], + [ + 'plugin' => 'extract', + 'index' => ['id'], + ], + ], + 'target_revision_id' => [ + [ + 'plugin' => 'paragraphs_lookup', + 'tags' => [ + 'Paragraphs Revisions Content', + 'Paragraphs Content', + ], + 'tag_ids' => [ + 'Paragraphs Revisions Content' => ['revision_id'], + 'Paragraphs Content' => ['value'], + ], + // D8.4 Does not like an empty source value, Even when using ids. + 'source' => 'value', + ], + [ + 'plugin' => 'extract', + 'index' => ['revision_id'], + ], + ], + ], + ]; + $migration->setProcessOfProperty($field_name, $process); + } + + /** + * {@inheritdoc} + */ + public function processFieldValues(MigrationInterface $migration, $field_name, $data) { + $this->processFieldValues($migration, $field_name, $data); + } + /** * {@inheritdoc} */ diff --git a/src/Plugin/migrate/process/ParagraphsLookup.php b/src/Plugin/migrate/process/ParagraphsLookup.php new file mode 100644 index 0000000..4bb8446 --- /dev/null +++ b/src/Plugin/migrate/process/ParagraphsLookup.php @@ -0,0 +1,194 @@ +processPluginManager = $process_plugin_manager; + } + + /** + * {@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('plugin.manager.migration'), + $container->get('plugin.manager.migrate.process') + ); + } + + /** + * {@inheritdoc} + */ + public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + $source_id_values = []; + $destination_ids = NULL; + if (isset($this->configuration['tags'])) { + $tags = $this->configuration['tags']; + if (!is_array($tags)) { + $tags = [$tags]; + } + foreach ($tags as $tag) { + /** @var \Drupal\migrate\Plugin\MigrationInterface[] $migrations */ + $migrations = $this->migrationPluginManager->createInstancesByTag($tag); + if (isset($this->configuration['tag_ids'][$tag])) { + $configuration = ['source' => $this->configuration['tag_ids'][$tag]]; + $value = $this->processPluginManager + ->createInstance('get', $configuration, $this->migration) + ->transform(NULL, $migrate_executable, $row, $destination_property); + } + foreach ($migrations as $migration_id => $migration) { + $source_id_values[$migration_id] = is_array($value) ? $value : [$value]; + $destination_ids = $this->lookupDestination($migration, $value, $migrate_executable, $row, $destination_property); + if ($destination_ids) { + break 2; + } + } + } + } + elseif (isset($this->configuration['migration'])) { + $migration_ids = $this->configuration['migration']; + if (!is_array($migration_ids)) { + $migration_ids = [$migration_ids]; + } + /** @var \Drupal\migrate\Plugin\MigrationInterface[] $migrations */ + $migrations = $this->migrationPluginManager->createInstances($migration_ids); + foreach ($migrations as $migration_id => $migration) { + if (isset($this->configuration['source_ids'][$migration_id])) { + $configuration = ['source' => $this->configuration['source_ids'][$migration_id]]; + $value = $this->processPluginManager + ->createInstance('get', $configuration, $this->migration) + ->transform(NULL, $migrate_executable, $row, $destination_property); + } + $source_id_values[$migration_id] = is_array($value) ? $value : [$value]; + $destination_ids = $this->lookupDestination($migration, $value, $migrate_executable, $row, $destination_property); + if ($destination_ids) { + break; + } + } + } + else { + throw new MigrateException("Either Migration or Tags must be defined."); + } + + if (!$destination_ids && !empty($this->configuration['no_stub'])) { + return NULL; + } + + if (!$destination_ids) { + // If the lookup didn't succeed, figure out which migration will do the + // stubbing. + if (isset($this->configuration['stub_id'])) { + $migration = $this->migrationPluginManager->createInstance($this->configuration['stub_id']); + } + else { + $migration = reset($migrations); + } + $destination_plugin = $migration->getDestinationPlugin(TRUE); + // Only keep the process necessary to produce the destination ID. + $process = $migration->getProcess(); + + // We already have the source ID values but need to key them for the Row + // constructor. + $source_ids = $migration->getSourcePlugin()->getIds(); + $values = []; + foreach (array_keys($source_ids) as $index => $source_id) { + $values[$source_id] = $source_id_values[$migration->id()][$index]; + } + + $stub_row = $this->createStubRow($values + $migration->getSourceConfiguration(), $source_ids); + + // Do a normal migration with the stub row. + $migrate_executable->processRow($stub_row, $process); + $destination_ids = []; + $id_map = $migration->getIdMap(); + try { + $destination_ids = $destination_plugin->import($stub_row); + } + catch (\Exception $e) { + $id_map->saveMessage($stub_row->getSourceIdValues(), $e->getMessage()); + } + + if ($destination_ids) { + $id_map->saveIdMapping($stub_row, $destination_ids, MigrateIdMapInterface::STATUS_NEEDS_UPDATE); + } + } + if ($destination_ids) { + if (count($destination_ids) == 1) { + return reset($destination_ids); + } + else { + return $destination_ids; + } + } + } + + /** + * Look for destination records. + */ + protected function lookupDestination(MigrationInterface $migration, $value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) { + if (!is_array($value)) { + $value = [$value]; + } + // TODO: remove after 8.6 support goes away. + if (method_exists($this, 'skipOnEmpty')) { + $this->skipOnEmpty($value); + } + else { + $this->skipInvalid($value); + } + // Break out of the loop as soon as a destination ID is found. + if ($destination_ids = $migration->getIdMap()->lookupDestinationIds($value)) { + $destination_ids = array_combine(array_keys($migration->getDestinationPlugin()->getIds()), reset($destination_ids)); + return $destination_ids; + } + return FALSE; + } + +} diff --git a/src/Plugin/migrate/source/d7/FieldCollectionItem.php b/src/Plugin/migrate/source/d7/FieldCollectionItem.php index ea8dd8e..2dcfb52 100644 --- a/src/Plugin/migrate/source/d7/FieldCollectionItem.php +++ b/src/Plugin/migrate/source/d7/FieldCollectionItem.php @@ -50,6 +50,9 @@ class FieldCollectionItem extends FieldableEntity { // bundles retrieved. if ($this->configuration['field_name']) { $query->condition('f.field_name', $this->configuration['field_name']); + $query->addField('fc', 'entity_type', 'parent_type'); + $query->addField('fc', 'entity_id', 'parent_id'); + $query->innerJoin('field_revision_' . $this->configuration['field_name'], 'fc', 'fc.' . $this->configuration['field_name'] . '_value = f.item_id and fc.' . $this->configuration['field_name'] . '_revision_id = f.revision_id'); } return $query; } @@ -85,6 +88,8 @@ class FieldCollectionItem extends FieldableEntity { 'revision_id' => $this->t('The field_collection_item revision id'), 'bundle' => $this->t('The field_collection bundle'), 'field_name' => $this->t('The field_collection field_name'), + 'parent_type' => $this->t('The type of the parent entity'), + 'parent_id' => $this->t('The identifier of the parent entity'), ]; return $fields; diff --git a/tests/src/Kernel/migrate/ParagraphContentMigrationTest.php b/tests/src/Kernel/migrate/ParagraphContentMigrationTest.php new file mode 100644 index 0000000..980e43c --- /dev/null +++ b/tests/src/Kernel/migrate/ParagraphContentMigrationTest.php @@ -0,0 +1,68 @@ +installEntitySchema('file'); + $this->installEntitySchema('node'); + $this->installEntitySchema('paragraph'); + $this->installEntitySchema('comment'); + $this->installSchema('comment', [ + 'comment_entity_statistics', + ]); + + $this->executeMigrationWithDependencies('d7_field_collection_revisions'); + $this->executeMigrationWithDependencies('d7_paragraphs_revisions'); + $this->executeMigrationWithDependencies('d7_node:paragraphs_test'); + + $this->prepareMigrations([ + 'd7_node:article' => [], + 'd7_node:forum' => [], + 'd7_node:test_content_type' => [], + ]); + + $this->executeMigration('d7_node_revision:paragraphs_test'); + + $node_8 = Node::load(8); + $this->assertEquals('Field Collection Text Data One UND', $node_8->field_field_collection_test->referencedEntities()[0]->field_text->value); + $this->assertEquals('Paragraph Field Two Bundle One Revision Two UND', $node_8->field_paragraph_one_only->referencedEntities()[0]->field_text->value); + } + +}