diff --git a/migrations/d7_field_collection.yml b/migrations/d7_field_collection.yml new file mode 100644 index 0000000..57ab868 --- /dev/null +++ b/migrations/d7_field_collection.yml @@ -0,0 +1,20 @@ +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 + # todo Get the langcode from the parent entity. + # langcode: langcode +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..b8e9d4b --- /dev/null +++ b/migrations/d7_field_collection_revisions.yml @@ -0,0 +1,29 @@ +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 + # todo Get the langcode from the parent entity. + # langcode: langcode +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..d2d3810 --- /dev/null +++ b/migrations/d7_paragraphs.yml @@ -0,0 +1,20 @@ +id: d7_paragraphs +label: Paragraphs +migration_tags: + - Drupal 7 + - Content + - Paragraphs Content +deriver: Drupal\paragraphs\Plugin\migrate\D7ParagraphsItemDeriver +source: + plugin: d7_paragraphs_item +process: + type: bundle + # todo Get the langcode from the parent entity. + # langcode: langcode +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..5910a9a --- /dev/null +++ b/migrations/d7_paragraphs_revisions.yml @@ -0,0 +1,29 @@ +id: d7_paragraphs_revisions +label: Paragraphs Revisions +migration_tags: + - Drupal 7 + - Content + - 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 + # todo Get the langcode from the parent entity. + # langcode: langcode +destination: + plugin: entity_reference_revisions:paragraph + new_revisions: TRUE +migration_dependencies: + required: + - d7_paragraphs diff --git a/migrations/d7_paragraphs_type.yml b/migrations/d7_paragraphs_type.yml index ac38f5b..2d385f7 100644 --- a/migrations/d7_paragraphs_type.yml +++ b/migrations/d7_paragraphs_type.yml @@ -2,6 +2,7 @@ id: d7_paragraphs_type label: Paragraphs type configuration migration_tags: - Drupal 7 + - Configuration source: plugin: d7_paragraphs_type add_description: true diff --git a/paragraphs.module b/paragraphs.module index 53f7d5f..b0b9ecc 100644 --- a/paragraphs.module +++ b/paragraphs.module @@ -5,15 +5,12 @@ * Contains paragraphs.module */ +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\field\FieldStorageConfigInterface; -use Drupal\field\Plugin\migrate\source\d7\Field; -use Drupal\field\Plugin\migrate\source\d7\FieldInstance; -use Drupal\field\Plugin\migrate\source\d7\ViewMode; -use Drupal\migrate_drupal\Plugin\migrate\FieldMigration; use Drupal\paragraphs\Entity\ParagraphsType; -use Drupal\paragraphs\Plugin\migrate\field\FieldCollection; +use Drupal\paragraphs\MigrationPluginsAlterer; use Drupal\Core\Render\Element; use Drupal\Core\Entity\EntityTypeInterface; @@ -103,7 +100,7 @@ function paragraphs_theme_suggestions_paragraph(array $variables) { /** * Implements hook_form_FORM_ID_alter(). */ -function paragraphs_form_entity_form_display_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { +function paragraphs_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) { $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($form['#entity_type'], $form['#bundle']); // Loop over ERR field's display options with paragraph target type. foreach (array_keys($field_definitions) as $field_name) { @@ -120,7 +117,7 @@ function paragraphs_form_entity_form_display_edit_form_alter(&$form, \Drupal\Cor /** * Implements hook_form_FORM_ID_alter(). */ -function paragraphs_form_field_storage_config_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { +function paragraphs_form_field_storage_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) { if ($form_state->getFormObject()->getEntity()->getType() == 'entity_reference') { // Entity Reference fields are no longer supported to reference Paragraphs. unset($form['settings']['target_type']['#options'][(string) t('Content')]['paragraph']); @@ -132,7 +129,7 @@ function paragraphs_form_field_storage_config_edit_form_alter(&$form, \Drupal\Co * * Indicate unsupported multilingual paragraphs field configuration. */ -function paragraphs_form_field_config_edit_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { +function paragraphs_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) { $field = $form_state->getFormObject()->getEntity(); if (!\Drupal::hasService('content_translation.manager')) { @@ -195,7 +192,7 @@ function paragraphs_module_implements_alter(&$implementations, $hook) { * Add a warning that paragraph fields can not be translated. * Switch to error if a paragraph field is marked as translatable. */ -function paragraphs_form_language_content_settings_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) { +function paragraphs_form_language_content_settings_form_alter(&$form, FormStateInterface $form_state, $form_id) { // Without it Paragraphs message are meaningless. if (!\Drupal::hasService('content_translation.manager')) { return; @@ -398,38 +395,9 @@ function paragraphs_preprocess_field_multiple_value_form(&$variables) { * https://www.drupal.org/project/drupal/issues/2904765 is resolved */ function paragraphs_migration_plugins_alter(array &$migrations) { - /** @var \Drupal\migrate\Plugin\MigrationPluginManager $migration_plugin_manager */ - $migration_plugin_manager = \Drupal::service('plugin.manager.migration'); - /** @var \Drupal\migrate\Plugin\MigrateSourcePluginManager $source_plugin_manager */ - $source_plugin_manager = \Drupal::service('plugin.manager.migrate.source'); - - foreach ($migrations as &$migration) { - if (!empty($migration['source'])) { - $configuration = $migration['source']; - $migration_stub = $migration_plugin_manager->createStubMigration($migration); - $source = $source_plugin_manager->createInstance($migration['source']['plugin'], $configuration, $migration_stub); - if (is_a($migration['class'], FieldMigration::class, TRUE)) { - - // Field storage. - if (is_a($source, Field::class)) { - _paragraphs_migration_entity_type_adjust($migration); - } - - // Field instance. - if (is_a($source, FieldInstance::class)) { - _paragraphs_migration_entity_type_adjust($migration); - _paragraphs_migration_bundle_adjust($migration); - $migration['migration_dependencies']['optional']['d7_field_collection_type'] = 'd7_field_collection_type'; - $migration['migration_dependencies']['optional']['d7_paragraphs_type'] = 'd7_paragraphs_type'; - } - } - - // View Modes. - if (is_a($source, ViewMode::class)) { - _paragraphs_migration_entity_type_adjust($migration, 'targetEntityType'); - } - } - } + $migration_plugins_alterer = \Drupal::service('paragraphs.migration_plugins_alterer'); + assert($migration_plugins_alterer instanceof MigrationPluginsAlterer); + $migration_plugins_alterer->alterMigrationPlugins($migrations); } /** @@ -457,62 +425,33 @@ function paragraphs_entity_base_field_info_alter(&$fields, EntityTypeInterface $ * * @param array $migration * The migration configuration to process. + * + * @deprecated in paragraphs:8.x-1.13 and is removed from paragraphs:8.x-2.0. + * Use \Drupal\paragraphs\MigrationPluginsAlterer::paragraphsMigrationBundleAdjust(). + * @see https://www.drupal.org/project/paragraphs/issues/2911244 */ function _paragraphs_migration_bundle_adjust(array &$migration) { - - if (!isset($migration['process']['bundle'])) { - $migration['process']['bundle'] = []; - } - - $bundle_process = $migration['process']['bundle']; - - // Try to play nice with other modules altering this, and don't replace - // it outright unless it's unchanged. - if (array_key_exists('plugin', $bundle_process)) { - $bundle_process = [$bundle_process]; - } - $bundle_process['paragraphs'] = [ - 'plugin' => 'paragraphs_process_on_value', - 'source_value' => 'entity_type', - 'expected_value' => 'field_collection_item', - 'process' => [ - 'plugin' => 'substr', - 'start' => FieldCollection::FIELD_COLLECTION_PREFIX_LENGTH, - ], - ]; - $migration['process']['bundle'] = $bundle_process; + $migration_plugins_alterer = \Drupal::service('paragraphs.migration_plugins_alterer'); + assert($migration_plugins_alterer instanceof MigrationPluginsAlterer); + $migration_plugins_alterer->paragraphsMigrationBundleAdjust($migration); } /** * Map field_collection_item and 'paragraphs_item' fields to 'paragraph'. * * @param array $migration - * Thei migration to process. + * The migration to process. * @param string $destination * The process destination. + * + * @deprecated in paragraphs:8.x-1.13 and is removed from paragraphs:8.x-2.0. + * Use \Drupal\paragraphs\MigrationPluginsAlterer::paragraphsMigrationEntityTypeAdjust(). + * @see https://www.drupal.org/project/paragraphs/issues/2911244 */ function _paragraphs_migration_entity_type_adjust(array &$migration, $destination = 'entity_type') { - $entity_type_process = $migration['process'][$destination]; - - // Try to play with other modules altering this, and don't replace it - // outright unless it's unchanged. - if (!is_array($entity_type_process)) { - $entity_type_process = [ - [ - 'plugin' => 'get', - 'source' => 'entity_type', - ], - ]; - } - $entity_type_process['paragraphs'] = [ - 'plugin' => 'static_map', - 'map' => [ - 'field_collection_item' => 'paragraph', - 'paragraphs_item' => 'paragraph', - ], - 'bypass' => TRUE, - ]; - $migration['process'][$destination] = $entity_type_process; + $migration_plugins_alterer = \Drupal::service('paragraphs.migration_plugins_alterer'); + assert($migration_plugins_alterer instanceof MigrationPluginsAlterer); + $migration_plugins_alterer->paragraphsMigrationEntityTypeAdjust($migration, $destination); } /** diff --git a/paragraphs.services.yml b/paragraphs.services.yml index b8c9666..14f1dd7 100644 --- a/paragraphs.services.yml +++ b/paragraphs.services.yml @@ -8,3 +8,8 @@ services: arguments: ['@cache.bootstrap', '@lock', '@entity_type.manager'] tags: - { name: needs_destruction } + + paragraphs.migration_plugins_alterer: + class: Drupal\paragraphs\MigrationPluginsAlterer + arguments: + - '@logger.factory' diff --git a/src/MigrationPluginsAlterer.php b/src/MigrationPluginsAlterer.php new file mode 100644 index 0000000..08f6416 --- /dev/null +++ b/src/MigrationPluginsAlterer.php @@ -0,0 +1,145 @@ +loggerChannel = $logger_factory->get('paragraphs'); + } + + /** + * Adds field collection and paragraph migration dependencies where needed. + * + * @param array[] $migrations + * An associative array of migrations keyed by migration ID, the same that + * is passed to hook_migration_plugins_alter() hooks. + */ + public function alterMigrationPlugins(array &$migrations) { + foreach ($migrations as &$migration) { + if (!isset($migration['process']) || !is_array($migration['process'])) { + continue; + } + + foreach (['entity_type', 'targetEntityType'] as $process_property) { + if (isset($migration['process'][$process_property])) { + $this->paragraphsMigrationEntityTypeAdjust($migration, $process_property); + $this->paragraphsMigrationBundleAdjust($migration); + $migration['migration_dependencies']['optional'][] = 'd7_field_collection_type'; + $migration['migration_dependencies']['optional'][] = 'd7_paragraphs_type'; + } + } + } + } + + /** + * Map field_collection_item and 'paragraphs_item' fields to 'paragraph'. + * + * @param array $migration + * Thei migration to process. + * @param string $process_property + * The process destination. + */ + public function paragraphsMigrationEntityTypeAdjust(array &$migration, $process_property) { + if (!$this->paragraphsMigrationPrepareProcess($migration['process'], $process_property)) { + return; + } + + $entity_type_process = &$migration['process'][$process_property]; + $entity_type_process[] = [ + 'plugin' => 'static_map', + 'map' => [ + 'field_collection_item' => 'paragraph', + 'paragraphs_item' => 'paragraph', + ], + 'bypass' => TRUE, + ]; + } + + /** + * Remove 'field_' prefix from field collection bundles. + * + * @param array $migration + * The migration configuration to process. + */ + public function paragraphsMigrationBundleAdjust(array &$migration) { + if (!$this->paragraphsMigrationPrepareProcess($migration['process'], 'bundle')) { + return; + } + + $bundle_process = &$migration['process']['bundle']; + $bundle_process[] = [ + 'plugin' => 'paragraphs_process_on_value', + 'source_value' => 'entity_type', + 'expected_value' => 'field_collection_item', + 'process' => [ + 'plugin' => 'substr', + 'start' => FieldCollection::FIELD_COLLECTION_PREFIX_LENGTH, + ], + ]; + } + + /** + * Converts a migration process to array for adding another process elements. + * + * @param array $process + * The array of process definitions of a migration. + * @param string $property + * The property which process definition should me converted to an array of + * process definitions. + * + * @return bool + * TRUE when the action was successful, FALSE otherwise. + */ + public function paragraphsMigrationPrepareProcess(array &$process, $property): bool { + if (!isset($process[$property])) { + return FALSE; + } + + $process_element = &$process[$property]; + + // Try to play with other modules altering this, and don't replace it + // outright unless it's unchanged. + if (is_string($process_element)) { + $process_element = [ + [ + 'plugin' => 'get', + 'source' => $process_element, + ], + ]; + } + elseif (is_array($process_element) && array_key_exists('plugin', $process_element)) { + $process_element = [$process_element]; + } + + if (!is_array($process_element)) { + $this->loggerChannel->error('Unknown migration process element type: @type.', ['@type' => gettype($process_element)]); + return FALSE; + } + + return TRUE; + } + +} diff --git a/src/Plugin/migrate/D7FieldCollectionItemDeriver.php b/src/Plugin/migrate/D7FieldCollectionItemDeriver.php new file mode 100644 index 0000000..6ee8bc9 --- /dev/null +++ b/src/Plugin/migrate/D7FieldCollectionItemDeriver.php @@ -0,0 +1,105 @@ +basePluginId = $base_plugin_id; + $this->fieldDiscovery = $field_discovery; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('migrate_drupal.field_discovery') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $types = static::getSourcePlugin('d7_field_collection_type'); + + try { + $types->checkRequirements(); + } + catch (RequirementsException $e) { + return $this->derivatives; + } + + 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'] = $this->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); + $migration->setProcessOfProperty('parent_id', 'parent_id'); + $migration->setProcessOfProperty('parent_type', 'parent_type'); + $migration->setProcessOfProperty('parent_field_name', 'field_name'); + + $this->fieldDiscovery->addBundleFieldProcesses($migration, 'field_collection_item', $fc_bundle); + $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..1e8e46d --- /dev/null +++ b/src/Plugin/migrate/D7ParagraphsItemDeriver.php @@ -0,0 +1,98 @@ +basePluginId = $base_plugin_id; + $this->fieldDiscovery = $field_discovery; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $base_plugin_id, + $container->get('migrate_drupal.field_discovery') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $types = static::getSourcePlugin('d7_paragraphs_type'); + try { + $types->checkRequirements(); + } + catch (RequirementsException $e) { + return $this->derivatives; + } + + try { + foreach ($types as $row) { + $values = $base_plugin_definition; + $bundle = $row->getSourceProperty('bundle'); + $values['label'] = $this->t('@label (@type)', [ + '@label' => $values['label'], + '@type' => $row->getSourceProperty('name'), + ]); + $values['source']['bundle'] = $bundle; + $values['destination']['default_bundle'] = $bundle; + + /** @var \Drupal\migrate\Plugin\Migration $migration */ + $migration = \Drupal::service('plugin.manager.migration') + ->createStubMigration($values); + $this->fieldDiscovery->addBundleFieldProcesses($migration, 'paragraphs_item', $bundle); + $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 19d9a21..73d8b00 100644 --- a/src/Plugin/migrate/field/FieldCollection.php +++ b/src/Plugin/migrate/field/FieldCollection.php @@ -8,9 +8,6 @@ use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase; /** * Field Plugin for field collection migrations. * - * @todo Implement ::defineValueProcessPipeline() - * @see https://www.drupal.org/project/paragraphs/issues/2911244 - * * @MigrateField( * id = "field_collection", * core = {7}, @@ -23,11 +20,64 @@ 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. + $migration_dependency = 'd7_field_collection:' . substr($field_name, static::FIELD_COLLECTION_PREFIX_LENGTH); + $migration_rev_dependency = 'd7_field_collection_revisions:' . substr($field_name, static::FIELD_COLLECTION_PREFIX_LENGTH); + $dependencies = $migration->getMigrationDependencies() + ['required' => []]; + $dependencies['required'] = array_unique(array_merge(array_values($dependencies['required']), [$migration_dependency])); + $migration->set('migration_dependencies', $dependencies); + + if (strpos($migration->getDestinationPlugin()->getPluginId(), 'entity_revision:') === 0 || strpos($migration->getDestinationPlugin()->getPluginId(), 'entity_complete:') === 0) { + $dependencies['required'] = array_unique(array_merge(array_values($dependencies['required']), [$migration_rev_dependency])); + $migration->set('migration_dependencies', $dependencies); + } + } + /** * {@inheritdoc} */ @@ -42,7 +92,6 @@ class FieldCollection extends FieldPluginBase { public function getFieldFormatterMap() { return [ 'field_collection_view' => 'entity_reference_revisions_entity_view', - // TODO: Change the autogenerated stub. ] + parent::getFieldFormatterMap(); } diff --git a/src/Plugin/migrate/field/Paragraphs.php b/src/Plugin/migrate/field/Paragraphs.php index bb3d821..6c42aa4 100644 --- a/src/Plugin/migrate/field/Paragraphs.php +++ b/src/Plugin/migrate/field/Paragraphs.php @@ -8,9 +8,6 @@ use Drupal\migrate_drupal\Plugin\migrate\field\FieldPluginBase; /** * Field Plugin for paragraphs migrations. * - * @todo Implement ::defineValueProcessPipeline() - * @see https://www.drupal.org/project/paragraphs/issues/2911244 - * * @MigrateField( * id = "paragraphs", * core = {7}, @@ -23,6 +20,68 @@ 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); + + // Add paragraphs migration as a dependency (if this is not a paragraph + // migration). + // @todo: This is a great example why we should consider derive paragraph + // migrations based on parent entity type (and bundle). + if (!in_array('Paragraphs Content', $migration->getMigrationTags(), TRUE)) { + $dependencies = $migration->getMigrationDependencies() + ['required' => []]; + $dependencies['required'] = array_unique(array_merge(array_values($dependencies['required']), [ + 'd7_paragraphs', + ])); + $migration->set('migration_dependencies', $dependencies); + + if (strpos($migration->getDestinationPlugin()->getPluginId(), 'entity_revision:') === 0 || strpos($migration->getDestinationPlugin()->getPluginId(), 'entity_complete:') === 0) { + $dependencies['required'] = array_unique(array_merge(array_values($dependencies['required']), [ + 'd7_paragraphs_revisions', + ])); + $migration->set('migration_dependencies', $dependencies); + } + } + } + /** * {@inheritdoc} */ @@ -45,7 +104,6 @@ class Paragraphs extends FieldPluginBase { public function getFieldFormatterMap() { return [ 'paragraphs_view' => 'entity_reference_revisions_entity_view', - // TODO: Change the autogenerated stub. ] + parent::getFieldFormatterMap(); } @@ -53,15 +111,15 @@ class Paragraphs extends FieldPluginBase { * {@inheritdoc} */ public function getFieldWidgetMap() { - return ['paragraphs_embed' => 'entity_reference_paragraphs'] - + parent::getFieldWidgetMap(); + return [ + 'paragraphs_embed' => 'entity_reference_paragraphs', + ] + parent::getFieldWidgetMap(); } /** * {@inheritdoc} */ public function alterFieldMigration(MigrationInterface $migration) { - $settings = [ 'paragraphs' => [ 'plugin' => 'paragraphs_field_settings', @@ -74,7 +132,6 @@ class Paragraphs extends FieldPluginBase { * {@inheritdoc} */ public function alterFieldInstanceMigration(MigrationInterface $migration) { - $settings = [ 'paragraphs' => [ 'plugin' => 'paragraphs_field_instance_settings', diff --git a/src/Plugin/migrate/process/ParagraphsLookup.php b/src/Plugin/migrate/process/ParagraphsLookup.php new file mode 100644 index 0000000..e1d145e --- /dev/null +++ b/src/Plugin/migrate/process/ParagraphsLookup.php @@ -0,0 +1,234 @@ +migrationPluginManager = $migration_plugin_manager; + $this->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('migrate.lookup'), + $container->get('migrate.stub'), + $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; + $migrations = []; + if (isset($this->configuration['tags'])) { + $tags = (array) $this->configuration['tags']; + foreach ($tags as $tag) { + /** @var \Drupal\migrate\Plugin\MigrationInterface[] $tag_migrations */ + $tag_migrations = $this->migrationPluginManager->createInstancesByTag($tag); + $migrations += $tag_migrations; + if (isset($this->configuration['tag_ids'][$tag])) { + $configuration = ['source' => $this->configuration['tag_ids'][$tag]]; + try { + $get_process_plugin = $this->processPluginManager + ->createInstance('get', $configuration, $this->migration); + } + catch (PluginException $e) { + continue; + } + $value = $get_process_plugin->transform(NULL, $migrate_executable, $row, $destination_property); + } + foreach ($tag_migrations as $migration_id => $migration) { + $source_id_values[$migration_id] = (array) $value; + $destination_ids = $this->lookupDestination($migration, $value); + if ($destination_ids) { + break 2; + } + } + } + } + elseif (!empty($this->configuration['migration'])) { + $destination_ids = parent::transform($value, $migrate_executable, $row, $destination_property); + $migration_ids = $this->configuration['migration']; + if (!is_array($migration_ids)) { + $migration_ids = (array) $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] = (array) $value; + $destination_ids = $this->lookupDestination($migration, $value); + 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']); + assert($migration instanceof MigrationInterface); + } + 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->getPluginId()][$index]; + } + + // @todo use the migration.stub service. + $stub_row = new Row($values + $migration->getSourceConfiguration(), $source_ids, TRUE); + + // 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; + } + } + + throw new MigrateException("Paragraphs lookup wasn't able to find the corresponding property for paragraph with source ID $value for the destination property $destination_property."); + } + + /** + * Look for destination records. + * + * @param \Drupal\migrate\Plugin\MigrationInterface $migration + * The migration that should be checked. + * @param string|string[] $value + * The source ID. + * + * @return array|false + * The array of the destination identifiers, or FALSE if destination cannot + * be determined. + * + * @throws \Drupal\migrate\MigrateException + * @throws \Drupal\migrate\MigrateSkipProcessException + */ + protected function lookupDestination(MigrationInterface $migration, $value) { + $value = (array) $value; + $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/process/ParagraphsProcessOnValue.php b/src/Plugin/migrate/process/ParagraphsProcessOnValue.php index 2e39926..cc77f4a 100644 --- a/src/Plugin/migrate/process/ParagraphsProcessOnValue.php +++ b/src/Plugin/migrate/process/ParagraphsProcessOnValue.php @@ -3,7 +3,6 @@ namespace Drupal\paragraphs\Plugin\migrate\process; use Drupal\migrate\MigrateExecutableInterface; -use Drupal\migrate\MigrateSkipRowException; use Drupal\migrate\Row; /** @@ -43,9 +42,14 @@ class ParagraphsProcessOnValue extends ProcessPluginBase { throw new \InvalidArgumentException("Required argument 'process' not set or invalid for paragraphs_process_on_value plugin"); } $source_value = $row->getSourceProperty($this->configuration['source_value']); + if (is_null($source_value)) { - throw new MigrateSkipRowException('Argument source_value is not valid for ProcessOnValue plugin'); + // This is probably a migration that shouldn't be touched by Paragraphs. + // For example, throwing an exception here would prevent the migration of + // the comment field configurations. + return $value; } + if ($source_value === $this->configuration['expected_value']) { $process = $this->configuration['process']; diff --git a/src/Plugin/migrate/source/d7/FieldCollectionItem.php b/src/Plugin/migrate/source/d7/FieldCollectionItem.php index d96646b..50af07d 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; @@ -94,14 +99,12 @@ class FieldCollectionItem extends FieldableEntity { * {@inheritdoc} */ public function getIds() { - $ids = [ + return [ 'item_id' => [ 'type' => 'integer', 'alias' => 'f', ], ]; - - return $ids; } } diff --git a/src/Plugin/migrate/source/d7/FieldCollectionItemRevision.php b/src/Plugin/migrate/source/d7/FieldCollectionItemRevision.php index d281f5c..bbc2188 100644 --- a/src/Plugin/migrate/source/d7/FieldCollectionItemRevision.php +++ b/src/Plugin/migrate/source/d7/FieldCollectionItemRevision.php @@ -25,14 +25,12 @@ class FieldCollectionItemRevision extends FieldCollectionItem { * {@inheritdoc} */ public function getIds() { - $ids = [ + return [ 'revision_id' => [ 'type' => 'integer', 'alias' => 'fr', ], ]; - - return $ids; } } diff --git a/src/Plugin/migrate/source/d7/ParagraphsItem.php b/src/Plugin/migrate/source/d7/ParagraphsItem.php index 5bd1f6f..ffacff6 100644 --- a/src/Plugin/migrate/source/d7/ParagraphsItem.php +++ b/src/Plugin/migrate/source/d7/ParagraphsItem.php @@ -2,6 +2,7 @@ namespace Drupal\paragraphs\Plugin\migrate\source\d7; +use Drupal\Core\Database\DatabaseExceptionWrapper; use Drupal\migrate\Row; /** @@ -20,9 +21,18 @@ class ParagraphsItem extends FieldableEntity { /** * Join string for getting current revisions. + * + * @var string */ const JOIN = "p.revision_id = pr.revision_id"; + /** + * The prefix of the field table that contains the entity properties. + * + * @var string + */ + const PARENT_FIELD_TABLE_PREFIX = 'field_data_'; + /** * {@inheritdoc} */ @@ -38,7 +48,8 @@ class ParagraphsItem extends FieldableEntity { public function query() { $query = $this->select('paragraphs_item', 'p') ->fields('p', - ['item_id', + [ + 'item_id', 'bundle', 'field_name', 'archived', @@ -58,13 +69,47 @@ class ParagraphsItem extends FieldableEntity { * {@inheritdoc} */ public function prepareRow(Row $row) { + [ + 'item_id' => $paragraph_id, + 'revision_id' => $paragraph_revision_id, + 'field_name' => $paragraph_parent_field_name, + 'bundle' => $bundle, + ] = $row->getSource(); + + if (!$paragraph_parent_field_name || !is_string($paragraph_parent_field_name)) { + return FALSE; + } // Get Field API field values. - $item_id = $row->getSourceProperty('item_id'); - $revision_id = $row->getSourceProperty('revision_id'); + foreach (array_keys($this->getFields('paragraphs_item', $bundle)) as $field_name) { + $row->setSourceProperty($field_name, $this->getFieldValues('paragraphs_item', $field_name, $paragraph_id, $paragraph_revision_id)); + } + + // We have to find the corresponding parent entity (which might be an + // another paragraph). Active revision only. + try { + $parent_data_query = $this->getDatabase()->select(static::PARENT_FIELD_TABLE_PREFIX . $paragraph_parent_field_name, 'fd'); + $parent_data_query->addField('fd', 'entity_type', 'parent_type'); + $parent_data_query->addField('fd', 'entity_id', 'parent_id'); + $parent_data = $parent_data_query + ->condition("fd.{$paragraph_parent_field_name}_value", $paragraph_id) + ->condition("fd.{$paragraph_parent_field_name}_revision_id", $paragraph_revision_id) + ->execute()->fetchAssoc(); + } + catch (DatabaseExceptionWrapper $e) { + // The paragraphs field data|revision table is missing, we cannot get + // the parent entity identifiers. This is a corrupted database. + // @todo Shouldn't we have to throw an exception instead? + return FALSE; + } + + if (!is_iterable($parent_data)) { + // We cannot get the parent entity identifiers. + return FALSE; + } - foreach (array_keys($this->getFields('paragraphs_item', $row->getSourceProperty('bundle'))) as $field) { - $row->setSourceProperty($field, $this->getFieldValues('paragraphs_item', $field, $item_id, $revision_id)); + foreach ($parent_data as $property_name => $property_value) { + $row->setSourceProperty($property_name, $property_value); } return parent::prepareRow($row); @@ -88,14 +133,12 @@ class ParagraphsItem extends FieldableEntity { * {@inheritdoc} */ public function getIds() { - $ids = [ + return [ 'item_id' => [ 'type' => 'integer', 'alias' => 'p', ], ]; - - return $ids; } } diff --git a/src/Plugin/migrate/source/d7/ParagraphsItemRevision.php b/src/Plugin/migrate/source/d7/ParagraphsItemRevision.php index b15610d..4713188 100644 --- a/src/Plugin/migrate/source/d7/ParagraphsItemRevision.php +++ b/src/Plugin/migrate/source/d7/ParagraphsItemRevision.php @@ -17,22 +17,25 @@ namespace Drupal\paragraphs\Plugin\migrate\source\d7; class ParagraphsItemRevision extends ParagraphsItem { /** - * Join string for getting all except the current revisions. + * {@inheritdoc} */ const JOIN = "p.item_id=pr.item_id AND p.revision_id <> pr.revision_id"; + /** + * {@inheritdoc} + */ + const PARENT_FIELD_TABLE_PREFIX = 'field_revision_'; + /** * {@inheritdoc} */ public function getIds() { - $ids = [ + return [ 'revision_id' => [ 'type' => 'integer', 'alias' => 'pr', ], ]; - - return $ids; } } diff --git a/tests/fixtures/drupal7.php b/tests/fixtures/drupal7.php index 1706b21..5a24f0b 100644 --- a/tests/fixtures/drupal7.php +++ b/tests/fixtures/drupal7.php @@ -10471,8 +10471,8 @@ $connection->insert('field_revision_field_images') 'field_images_fid' => '1', 'field_images_alt' => 'alt text', 'field_images_title' => 'title text', - 'field_images_width' => '93', - 'field_images_height' => '93', + 'field_images_width' => '640', + 'field_images_height' => '400', )) ->execute(); @@ -13607,10 +13607,10 @@ $connection->insert('file_managed') ->values(array( 'fid' => '1', 'uid' => '1', - 'filename' => 'cube.jpeg', - 'uri' => 'public://cube.jpeg', + 'filename' => 'yellow.jpg', + 'uri' => 'public://yellow.jpg', 'filemime' => 'image/jpeg', - 'filesize' => '3620', + 'filesize' => '5363', 'status' => '1', 'timestamp' => '1421727515', )) @@ -13620,7 +13620,7 @@ $connection->insert('file_managed') 'filename' => 'ds9.txt', 'uri' => 'public://ds9.txt', 'filemime' => 'text/plain', - 'filesize' => '4720', + 'filesize' => '472', 'status' => '1', 'timestamp' => '1421727516', )) diff --git a/tests/fixtures/sites/default/files/Babylon5.txt b/tests/fixtures/sites/default/files/Babylon5.txt new file mode 100644 index 0000000..aca7279 --- /dev/null +++ b/tests/fixtures/sites/default/files/Babylon5.txt @@ -0,0 +1 @@ +bbln \ No newline at end of file diff --git a/tests/fixtures/sites/default/files/ds9.txt b/tests/fixtures/sites/default/files/ds9.txt new file mode 100644 index 0000000..8cb632d --- /dev/null +++ b/tests/fixtures/sites/default/files/ds9.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut dignissim viverra erat, nec sodales nisl rhoncus ac. Curabitur sit amet sem eget libero ultrices placerat. Nullam a nisl lobortis, imperdiet nisi nec, scelerisque massa. Curabitur varius velit sit amet urna consectetur cursus. Aliquam imperdiet augue faucibus, mattis neque vitae, euismod dolor. Morbi commodo elit risus. Suspendisse sagittis aliquam lorem ac pulvinar. Curabitur id egestas mi. Vestibulum velit. \ No newline at end of file diff --git a/tests/fixtures/sites/default/files/yellow.jpg b/tests/fixtures/sites/default/files/yellow.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b85f5350a420f0d311d6818839ff581f5c86bffa GIT binary patch literal 5363 zcmdT{XIN9q*4`ljL5dJSdJED6k|4cEDAG&lReDFI2ueo;=_N>0dJk0)6hS}~sX>vV zLPS7H1e6v55tV#V&%Nh-zwUE?-r4)v&+N5k&0cHGJMY>8 z0O=hN4~TN}3i5J@3JM5HN{C9UxT93$-@IweE? zf$UGXg4LRb^(6j#^M~?m08k)YeP3bfvt;3;_G8=zH|D!Eu_3VttCux*pN%u?JNLzyweKOKdZ&N+$K4JZ-;?9&w(1A~Pk~NvO6ld7-}_ z^aa;dnZ9Y{S%^;$D=PF)ESb~;07^^M)zp^}RmZG%&v&Lf*s1EItt7WBtsTj{Y$|}mhblcQ~4kJRCguIfY~$vq+pPJzxd8Q+*N^jePkSd)kCGDC>9(l zdDXjOi_mM~RB~vm)z;xPRj7Sh<1;^P?d2$0V%WVBu4=!uqRZp(@0EGVfdFrY-9yhV z+k3fetG$-*6Rj?F?hz(R)@I{@_X)~kIT5&Wr-%`a(XU=bKKXWtSUGcD0PG;X>Er8> z^dCL_=YSY;D%i+8_Wv^hfGEJf69N8}h_!zj{O7(<4)8J-fMS9@D`NA+SwO{}xYO{K z`WG*&^SnV&LY^$6_X~&im$7udgn?AJZR00-r94hRrpi{g!Zz3gdl&G01yK1I{&)0w ziX4Y1BLIScz+lL~*A)z+fKURAOy{7?NLGG?I*S0zwCpd222lbepfFKfwy8a1=>zqI zw46F3y8BLCI;NINp9EB&O<54WZWjO}_V!dL3P06>kMC9Q+OLe$8bdBxzg0o&>hARO z{jj|~%j#n2|Ikk^0tQ!1%bSb9%iaBu?qsX>p;H^pddt&bsbi$%bP;uT(@87JTb;gV z8%qNnDzL^fZ9;nPXstH)>~(Q-SeiUIjQOCz?3^ZRKd+PjNP5z+`8&y3q`fPWyrsWwGW-sU;;Y(Qa8Rg%6nc+d3dxD4vA z7jkMYvrzgBcuTD)tZec$JPumvF}paxi4qAFc3`W33tX#?xPr{j&o5;<6O9~<_sO^< zjXq=EV3)EBh1?sVvNJT5b_*Nk^t_r~B%aC9P{p&RX}!~1)l$BEJad?!If!Epb%pqv z($szl=mcI{}KwHs~;t0@+RClgzYTP&kNayoK5OHiAJ_MZQSJ@jS;xps z!Mtj;oyiKX}dE8KpEd9Kv18uXct#a_?k(RoI*Dy`xSmqovk^Ble)U~P# z|B`oO)vK!?2V^k!_d7!;B;LI~m;7k1P>l<-d(Lw9jB>Mf4DCietf~cj=B|^arML3r zUdPF7IQT#aR#B68+L*?#y36$MpdBe4l-8+fbRXB`1u+uqG_Z3DugYDqRra@=jnunf z?z;yy#^)WQM6U^F3SO8n06A6@%!tG2 z{D6mGFrJ)Tg+Z)(a$6A>h6=Kl&p5lUC5xJH2xiFk0@(hyJG~EX*Aq;{85WCAyJTH8PSnD53J(k=M1X{=N=du=y`*a@@ z{wP2^qe9uT(fO8F$X%{**C@X*QH?2`_atE1k%`;Rs(u7Fo^sK;E9!P-9t%P5a-JC; zxBxBKG|u~}N2LE2Qz}vhoXH-dRp;B4G`7f@CEC5V&fUjYNh5dbo2q?}O}Yl35E09w zyf`tq&g@9IY(m!2p%}*#iwTWL{b6h+Gh|^`AH;FGSsd#MaCSC@OZ#(1%*!$HII)i6 z51Xv$Z<>5-B(tatBS1l>O$daN5}*M8qI(bp0A^woK%k)Lz#IH9mmp@Ox<3Lc3VZ13}7gO%w$va;qfi6K!n46Wh% z#nop6juMwP#diE+RyMvH^0@BhD(heTuzwoc@sPOpR?X|sv|Fg)qqiQ5^U89jFS@JhC_G@(5M-+V$n=e! zdI7kp&7}7|;d#wF{15}Ep&<3aR;0z_!GjM5-PiaK_S6oAVv4OLUal^_k^`609(CO> zVE@rCnAorR@zOVHR+OhSXS3dl2XIkZ9q_PXS!BPV4amd)xu~&s%s*Wg68P%r8mCez14jsH=e9Ae^RzMLtZ7#Y zyf&)f>1&N2x_n8LE9xO)y*Pb``W%Y4j?-P`FHiN|f+<8>z^wI^!fzYJA`6^9U+sxM ztO_eMF1U{=ZdQ86Fj;C+13!`bB~z;u9b3gFl zkbo>%1BakoMLWxb0;Pd<@z9)}en1gk|NcZ=VK^Q%<}!FL@cQ1%*<^IbgZP^6mqE*$ z@;5?Vy!G(PUl3F)4u^Hu(l?*wU)P(BWLufcw;9SxY^*u4&wDa9#!YPR$W{G|u&dx_ zhJa}i{<9n}8(pOiI-gXXOEaPAYSvIS@6|k^`6za)i3Oa6R>)|+l7RXYcz(HQ z|80ACl~2}C@{B0K>!byQIGBHZeopLKvZ=pwh&;qqtnQIf_+Z`rG_Kp80%e3D%ja0N z&|x}qUP2nF?r`|6(rd`X9J^Yi-po;SWVi(h94y7i&?#%Qv&2O&!^_$bH>e($+FIkC zKe1KS;my9%aj?5qu_B!hY`x3NIbFAx|i8e+ja~CG6 zXEQ>mN3BVT+LQe6+Y~CI9(Ge2B_U{i)q7d~z&*sNK=Qdn{erB_SlX#73GM4wI^R&5 z+9uzSpDuNftW2kCqi7T=)_fPWnq6Q%_EG9N<}puY{Th9I?bP~0s`3h~Y;FKuc6x_3 z1j3WnBO7F_triMofkPA1@N=4jpe(V8^jCRa4o zuI4wG9mfSsnVyIZ5&OOP$~1o+R!(_5g?+V2ESL(;w68dKM0$j9UwkI zs61fFv^~lNcyAAPZW;H|TBuKMj0w%Og&>E^)wy&oyN_y;0H<1(oH^khH!DGbtYaQV z?rATchqMV&yFI<<6OB-UW*+HzWz(*ve9E+-Q5gNp08q!|lRNqGGc^6XMrD?e1@lo! z*;g^NeeCtv(W!V#xjXL*a2+paOAW;sk!)#iaEU{^VymXnUIoaOyu*Cn`*Wdd!l>f3 z<;V@bp{eK!iZI+xv_~Blz5YXfgWre9G=H&J`jm=)AJZ@nmcVCE+I%}m<$RX$RaJM9&2{v&shru`c*iT z%*`I18Q^dh+K(6HYcYK+v;9*wy(3gHz!DxHKxs^8;k70(HKSUqSG|c)9-4a{!bta2 zI}tDIU#tb(zQLhi#10anKq5o9IXJ?1K?h!Vz-G~ezT?u6;^9H0#>paHT7wR0!Rhs} zq~TGd&ZnyF#=3atC7*ntHBs*T+{1A?h)sur;#iNSXp7NVpX7NpLIXBO)NtIm@H18e96+}Yb8CC33E%n_GdFQB-Zp*QBrOF>(fsoZWiHy`#sN03kyrz zyS@}RK))2pBV<*wqPzI~UTC-wRaX=HvGB@i=uo2^dvJ-+`7gYyVh^+?4+@Hxw|u*j z<`Yk@JpW!r{t0MNq~@x z@ST>!Z+Rqu=lZwBy8*bOezsRR;c<>m$y3<4L=pBpjAI0(=0*BTdz4!z%+z}9r*PFO z=w~k3z&?{T)c6*UcByo>v7xocnzO=3j@ixv2L9fs+nj^ZwXv?XFqu3>X()r1m)DjG zWf~P|Q4uY=59vvn#hizY7wCxy!`KvL_&Vp9AbKbp2SHvsL_DbQt_h&kE}ymt#V+oT?{I)Kk5}Fg6Lzrrg1L2p+0Wb7>2n zX5W+e&KqA94@a~Ll!vs^!o#8N2v~Ve$>%*uPPdr@HQu3e-*FFX`_Hc$i=YYFpass("Drupal 8.8.x has only the 'classic' node migration."); + return; + } + $this->setClassicNodeMigration($node_migrate_type_classic); + $this->assertMigrateUpgradeViaUi(); + $this->assertParagraphsMigrationResults(); + $this->assertNode8Paragraphs(); + $this->assertNode9Paragraphs(); + $this->assertIcelandicNode9Paragraphs(); + } + + /** + * Provides data and expected results for testing paragraph migrations. + * + * @return bool[][] + * Classic node migration type. + */ + public function providerParagraphsMigrate() { + return [ + ['node_migrate_type_classic' => TRUE], + ['node_migrate_type_classic' => FALSE], + ]; + } + +} diff --git a/tests/src/Functional/Migrate/MigrateUiParagraphsTestBase.php b/tests/src/Functional/Migrate/MigrateUiParagraphsTestBase.php new file mode 100644 index 0000000..3507479 --- /dev/null +++ b/tests/src/Functional/Migrate/MigrateUiParagraphsTestBase.php @@ -0,0 +1,561 @@ + [ + 'block_content.basic.default' => NULL, + 'comment.comment.default' => NULL, + 'comment.comment_forum.default' => NULL, + 'comment.comment_node_article.default' => NULL, + 'comment.comment_node_blog.default' => NULL, + 'comment.comment_node_book.default' => NULL, + 'comment.comment_node_page.default' => NULL, + 'comment.comment_node_paragraphs_test.default' => NULL, + 'comment.comment_node_test_content_type.default' => NULL, + 'node.article.default' => NULL, + 'node.blog.default' => NULL, + 'node.book.default' => NULL, + 'node.forum.default' => NULL, + 'node.page.default' => NULL, + 'node.paragraphs_test.default' => NULL, + 'node.test_content_type.default' => NULL, + 'paragraph.field_collection_test.default' => NULL, + 'paragraph.nested_fc_inner.default' => NULL, + 'paragraph.nested_fc_outer.default' => NULL, + 'paragraph.paragraph_bundle_one.default' => NULL, + 'paragraph.paragraph_bundle_two.default' => NULL, + 'taxonomy_term.test_vocabulary.default' => NULL, + 'user.user.default' => NULL, + ], + 'entity_form_mode' => [ + 'user.register' => 'Register', + ], + 'entity_view_display' => [ + 'block_content.basic.default' => NULL, + 'comment.comment.default' => NULL, + 'comment.comment_forum.default' => NULL, + 'comment.comment_node_article.default' => NULL, + 'comment.comment_node_blog.default' => NULL, + 'comment.comment_node_book.default' => NULL, + 'comment.comment_node_page.default' => NULL, + 'comment.comment_node_paragraphs_test.default' => NULL, + 'comment.comment_node_test_content_type.default' => NULL, + 'node.article.custom' => NULL, + 'node.article.default' => NULL, + 'node.article.rss' => NULL, + 'node.article.teaser' => NULL, + 'node.blog.default' => NULL, + 'node.blog.teaser' => NULL, + 'node.book.default' => NULL, + 'node.book.teaser' => NULL, + 'node.forum.default' => NULL, + 'node.forum.teaser' => NULL, + 'node.page.default' => NULL, + 'node.page.teaser' => NULL, + 'node.paragraphs_test.default' => NULL, + 'node.paragraphs_test.teaser' => NULL, + 'node.test_content_type.default' => NULL, + 'paragraph.field_collection_test.default' => NULL, + 'paragraph.nested_fc_inner.default' => NULL, + 'paragraph.nested_fc_outer.default' => NULL, + 'paragraph.paragraph_bundle_one.default' => NULL, + 'paragraph.paragraph_bundle_one.paragraphs_editor_preview' => NULL, + 'paragraph.paragraph_bundle_two.default' => NULL, + 'taxonomy_term.test_vocabulary.default' => NULL, + 'user.user.compact' => NULL, + 'user.user.default' => NULL, + ], + 'entity_view_mode' => [ + 'block_content.full' => 'Full', + 'comment.full' => 'Full', + 'node.custom' => 'custom', + 'node.full' => 'Full', + 'node.rss' => 'RSS', + 'node.search_index' => 'Search index', + 'node.search_result' => 'Search result highlighting input', + 'node.teaser' => 'Teaser', + 'paragraph.full' => 'Full', + 'paragraph.paragraphs_editor_preview' => 'paragraphs_editor_preview', + 'paragraph.preview' => 'Preview', + 'taxonomy_term.full' => 'Full', + 'user.compact' => 'Compact', + 'user.full' => 'Full', + ], + 'field_storage_config' => [ + 'block_content.body' => 'block_content.body', + 'comment.comment_body' => 'comment.comment_body', + 'comment.field_integer' => 'comment.field_integer', + 'node.body' => 'node.body', + 'node.comment' => 'node.comment', + 'node.comment_forum' => 'node.comment_forum', + 'node.comment_node_article' => 'node.comment_node_article', + 'node.comment_node_blog' => 'node.comment_node_blog', + 'node.comment_node_book' => 'node.comment_node_book', + 'node.comment_node_page' => 'node.comment_node_page', + 'node.comment_node_paragraphs_test' => 'node.comment_node_paragraphs_test', + 'node.comment_node_test_content_type' => 'node.comment_node_test_content_type', + 'node.field_any_paragraph' => 'node.field_any_paragraph', + 'node.field_boolean' => 'node.field_boolean', + 'node.field_date' => 'node.field_date', + 'node.field_date_with_end_time' => 'node.field_date_with_end_time', + 'node.field_date_without_time' => 'node.field_date_without_time', + 'node.field_datetime_without_time' => 'node.field_datetime_without_time', + 'node.field_email' => 'node.field_email', + 'node.field_field_collection_test' => 'node.field_field_collection_test', + 'node.field_file' => 'node.field_file', + 'node.field_float' => 'node.field_float', + 'node.field_image' => 'node.field_image', + 'node.field_images' => 'node.field_images', + 'node.field_integer' => 'node.field_integer', + 'node.field_integer_list' => 'node.field_integer_list', + 'node.field_link' => 'node.field_link', + 'node.field_long_text' => 'node.field_long_text', + 'node.field_nested_fc_outer' => 'node.field_nested_fc_outer', + 'node.field_node_entityreference' => 'node.field_node_entityreference', + 'node.field_paragraph_one_only' => 'node.field_paragraph_one_only', + 'node.field_phone' => 'node.field_phone', + 'node.field_private_file' => 'node.field_private_file', + 'node.field_tags' => 'node.field_tags', + 'node.field_term_entityreference' => 'node.field_term_entityreference', + 'node.field_term_reference' => 'node.field_term_reference', + 'node.field_text' => 'node.field_text', + 'node.field_text_filtered' => 'node.field_text_filtered', + 'node.field_text_list' => 'node.field_text_list', + 'node.field_text_long_filtered' => 'node.field_text_long_filtered', + 'node.field_text_long_plain' => 'node.field_text_long_plain', + 'node.field_text_plain' => 'node.field_text_plain', + 'node.field_text_sum_filtered' => 'node.field_text_sum_filtered', + 'node.field_user_entityreference' => 'node.field_user_entityreference', + 'node.taxonomy_forums' => 'node.taxonomy_forums', + 'paragraph.field_email' => 'paragraph.field_email', + 'paragraph.field_integer_list' => 'paragraph.field_integer_list', + 'paragraph.field_nested_fc_inner' => 'paragraph.field_nested_fc_inner', + 'paragraph.field_text' => 'paragraph.field_text', + 'paragraph.field_text_list' => 'paragraph.field_text_list', + 'taxonomy_term.field_integer' => 'taxonomy_term.field_integer', + 'taxonomy_term.field_term_reference' => 'taxonomy_term.field_term_reference', + 'user.field_file' => 'user.field_file', + 'user.field_integer' => 'user.field_integer', + 'user.user_picture' => 'user.user_picture', + ], + 'field_config' => [ + 'block_content.basic.body' => 'Body', + 'comment.comment.comment_body' => 'Comment', + 'comment.comment_forum.comment_body' => 'Comment', + 'comment.comment_node_article.comment_body' => 'Comment', + 'comment.comment_node_blog.comment_body' => 'Comment', + 'comment.comment_node_book.comment_body' => 'Comment', + 'comment.comment_node_page.comment_body' => 'Comment', + 'comment.comment_node_paragraphs_test.comment_body' => 'Comment', + 'comment.comment_node_test_content_type.comment_body' => 'Comment', + 'comment.comment_node_test_content_type.field_integer' => 'Integer', + 'node.article.body' => 'Body', + 'node.article.comment' => 'Comments', + 'node.article.comment_node_article' => 'Comments', + 'node.article.field_image' => 'Image', + 'node.article.field_link' => 'Link', + 'node.article.field_tags' => 'Tags', + 'node.article.field_text_filtered' => 'Text filtered', + 'node.article.field_text_long_filtered' => 'Text long filtered', + 'node.article.field_text_long_plain' => 'Text long plain', + 'node.article.field_text_plain' => 'Text plain', + 'node.article.field_text_sum_filtered' => 'Text summary filtered', + 'node.blog.body' => 'Body', + 'node.blog.comment_node_blog' => 'Comments', + 'node.blog.field_link' => 'Link', + 'node.book.body' => 'Body', + 'node.book.comment_node_book' => 'Comments', + 'node.forum.body' => 'Body', + 'node.forum.comment_forum' => 'Comments', + 'node.forum.taxonomy_forums' => 'Forums', + 'node.page.body' => 'Body', + 'node.page.comment_node_page' => 'Comments', + 'node.page.field_text_filtered' => 'Text filtered', + 'node.page.field_text_long_filtered' => 'Text long filtered', + 'node.page.field_text_long_plain' => 'Text long plain', + 'node.page.field_text_plain' => 'Text plain', + 'node.page.field_text_sum_filtered' => 'Text summary filtered', + 'node.paragraphs_test.body' => 'Body', + 'node.paragraphs_test.comment_node_paragraphs_test' => 'Comments', + 'node.paragraphs_test.field_any_paragraph' => 'Any Paragraph', + 'node.paragraphs_test.field_field_collection_test' => 'Field Collection Test', + 'node.paragraphs_test.field_nested_fc_outer' => 'Nested FC Outer', + 'node.paragraphs_test.field_paragraph_one_only' => 'Paragraph One Only', + 'node.test_content_type.field_boolean' => 'Boolean', + 'node.test_content_type.comment_node_test_content_type' => 'Comments', + 'node.test_content_type.field_date' => 'Date', + 'node.test_content_type.field_date_with_end_time' => 'Date With End Time', + 'node.test_content_type.field_date_without_time' => 'Date without time', + 'node.test_content_type.field_datetime_without_time' => 'Datetime without time', + 'node.test_content_type.field_email' => 'Email', + 'node.test_content_type.field_file' => 'File', + 'node.test_content_type.field_float' => 'Float', + 'node.test_content_type.field_images' => 'Images', + 'node.test_content_type.field_integer' => 'Integer', + 'node.test_content_type.field_integer_list' => 'Integer List', + 'node.test_content_type.field_link' => 'Link', + 'node.test_content_type.field_long_text' => 'Long text', + 'node.test_content_type.field_node_entityreference' => 'Node Entity Reference', + 'node.test_content_type.field_phone' => 'Phone', + 'node.test_content_type.field_private_file' => 'Private file', + 'node.test_content_type.field_term_entityreference' => 'Term Entity Reference', + 'node.test_content_type.field_term_reference' => 'Term Reference', + 'node.test_content_type.field_text' => 'Text', + 'node.test_content_type.field_text_list' => 'Text List', + 'node.test_content_type.field_user_entityreference' => 'User Entity Reference', + 'paragraph.field_collection_test.field_integer_list' => 'Integer List', + 'paragraph.field_collection_test.field_text' => 'Text', + 'paragraph.nested_fc_inner.field_text' => 'Text', + 'paragraph.nested_fc_outer.field_nested_fc_inner' => 'Nested FC Inner', + 'paragraph.paragraph_bundle_one.field_text' => 'Text', + 'paragraph.paragraph_bundle_one.field_text_list' => 'Text List', + 'paragraph.paragraph_bundle_two.field_email' => 'Email', + 'paragraph.paragraph_bundle_two.field_text' => 'Text', + 'taxonomy_term.test_vocabulary.field_integer' => 'Integer', + 'taxonomy_term.test_vocabulary.field_term_reference' => 'Term Reference', + 'user.user.field_file' => 'File', + 'user.user.field_integer' => 'Integer', + 'user.user.user_picture' => 'Picture', + ], + 'node_type' => [ + 'article' => 'Article', + 'blog' => 'Blog entry', + 'book' => 'Book page', + 'forum' => 'Forum topic', + 'page' => 'Basic page', + 'paragraphs_test' => 'Paragraphs Test', + 'test_content_type' => 'Test content type', + ], + 'node' => [ + 1 => 'A Node', + 2 => 'The thing about Deep Space 9', + 4 => 'is - The thing about Firefly', + 6 => 'Comments are closed :-(', + 7 => 'Comments are open :-)', + 8 => 'Paragraph Migration Test Content UND', + 9 => 'Paragraph Migration Test Content EN', + ], + 'paragraphs_type' => [ + 'field_collection_test' => 'Field collection test', + 'nested_fc_inner' => 'Nested fc inner', + 'nested_fc_outer' => 'Nested fc outer', + 'paragraph_bundle_one' => 'Paragraph Bundle One', + 'paragraph_bundle_two' => 'Paragraph Bundle Two', + ], + // Paragraph IDs and labels with 'complete' migration, where node + // revisions (even the active one) and node translations are migrated in a + // single, complete node migration. The final IDs of the paragraph + // entities aren't the same as the ones migrated with the 'classic' + // migration. + // @see https://www.drupal.org/node/3105503 + 'paragraph' => [ + 1 => 'Paragraph Migration Test Content UND > Field Collection Test', + 2 => 'Paragraph Migration Test Content UND > Field Collection Test', + 3 => 'Paragraph Migration Test Content EN > Field Collection Test', + 4 => 'Paragraph Migration Test Content EN > Field Collection Test (previous revision)', + 5 => 'Paragraph Migration Test Content EN > Field Collection Test (previous revision)', + 6 => 'Paragraph Migration Test Content EN > Field Collection Test (previous revision)', + 7 => 'Paragraph Migration Test Content UND > Nested FC Outer > Nested FC Inner', + 8 => 'Paragraph Migration Test Content UND > Nested FC Outer', + 9 => 'Paragraph Migration Test Content EN > Any Paragraph (previous revision)', + 10 => 'Paragraph Migration Test Content UND > Any Paragraph', + 11 => 'Paragraph Migration Test Content UND > Paragraph One Only', + 12 => 'Paragraph Migration Test Content EN > Any Paragraph', + 13 => 'Paragraph Migration Test Content EN > Paragraph One Only', + 14 => 'Paragraph Migration Test Content EN > Paragraph One Only (previous revision)', + 15 => 'Paragraph Migration Test Content UND > Any Paragraph', + 16 => 'Paragraph Migration Test Content EN > Any Paragraph', + 17 => 'Paragraph Migration Test Content EN > Any Paragraph (previous revision)', + 18 => 'Paragraph Migration Test Content EN > Any Paragraph (previous revision)', + ], + ]; + + // Paragraph IDs and labels with 'classic' node migration (core 8.8.x has + // only this), where nodes, node revisions and node translations are + // migrated separately. + if (Settings::get('migrate_node_migrate_type_classic', FALSE)) { + $expected_entities['paragraph'] = [ + 1 => 'Paragraph Migration Test Content UND > Field Collection Test (previous revision)', + 2 => 'Paragraph Migration Test Content UND > Field Collection Test (previous revision)', + 3 => 'Paragraph Migration Test Content EN > Field Collection Test', + 4 => 'Paragraph Migration Test Content EN > Field Collection Test (previous revision)', + 5 => 'Paragraph Migration Test Content EN > Field Collection Test (previous revision)', + 6 => 'Paragraph Migration Test Content EN > Field Collection Test (previous revision)', + 7 => 'Paragraph Migration Test Content EN > Any Paragraph (previous revision)', + 8 => 'Paragraph Migration Test Content UND > Any Paragraph (previous revision)', + 9 => 'Paragraph Migration Test Content UND > Paragraph One Only (previous revision)', + 10 => 'Paragraph Migration Test Content EN > Any Paragraph', + 11 => 'Paragraph Migration Test Content EN > Paragraph One Only', + 12 => 'Paragraph Migration Test Content EN > Paragraph One Only (previous revision)', + 13 => 'Paragraph Migration Test Content UND > Any Paragraph (previous revision)', + 14 => 'Paragraph Migration Test Content EN > Any Paragraph', + 15 => 'Paragraph Migration Test Content EN > Any Paragraph (previous revision)', + 16 => 'Paragraph Migration Test Content EN > Any Paragraph (previous revision)', + 17 => 'Paragraph Migration Test Content UND > Nested FC Outer > Nested FC Inner', + 18 => 'Paragraph Migration Test Content UND > Nested FC Outer', + ]; + } + + return $expected_entities; + } + + /** + * {@inheritdoc} + */ + protected function getEntityCounts() { + // This is not used. + $entity_counts = []; + + foreach ($this->getExpectedEntities() as $entity_type_id => $expected_entities) { + $entity_counts[$entity_type_id] = count($expected_entities); + } + + return $entity_counts; + } + + /** + * {@inheritdoc} + */ + protected function getEntityCountsIncremental() { + // Unused. + return $this->getEntityCounts(); + } + + /** + * {@inheritdoc} + */ + protected function getAvailablePaths() { + // Unused. + return []; + } + + /** + * {@inheritdoc} + */ + protected function getMissingPaths() { + // Unused. + return []; + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + $this->loadFixture(drupal_get_path('module', 'paragraphs') . '/tests/fixtures/drupal7.php'); + } + + /** + * Executes the upgrade process by the UI and asserts basic expectations. + */ + protected function assertMigrateUpgradeViaUi() { + $connection_options = $this->sourceDatabase->getConnectionOptions(); + $this->drupalGet('/upgrade'); + $session = $this->assertSession(); + $session->responseContains('Upgrade a site by importing its files and the data from its database into a clean and empty new install of Drupal'); + + $this->drupalPostForm(NULL, [], $this->t('Continue')); + $session->pageTextContains('Provide credentials for the database of the Drupal site you want to upgrade.'); + + $driver = $connection_options['driver']; + $connection_options['prefix'] = $connection_options['prefix']['default']; + + // Use the driver connection form to get the correct options out of the + // database settings. This supports all of the databases we test against. + $drivers = drupal_get_database_types(); + $form = $drivers[$driver]->getFormOptions($connection_options); + $connection_options = array_intersect_key($connection_options, $form + $form['advanced_options']); + $version = $this->getLegacyDrupalVersion($this->sourceDatabase); + $edit = [ + $driver => $connection_options, + 'source_private_file_path' => $this->getSourcePrivateFilesPath(), + 'version' => $version, + 'source_base_path' => $this->getSourceBasePath(), + ]; + + if (count($drivers) !== 1) { + $edit['driver'] = $driver; + } + $edits = $this->translatePostValues($edit); + + $this->drupalPostForm(NULL, $edits, $this->t('Review upgrade')); + $session->pageTextNotContains('Resolve all issues below to continue the upgrade.'); + + // ID conflict form. + $session->buttonExists($this->t('I acknowledge I may lose data. Continue anyway.')); + $this->drupalPostForm(NULL, [], $this->t('I acknowledge I may lose data. Continue anyway.')); + $session->statusCodeEquals(200); + + // Perform the upgrade. + $this->drupalPostForm(NULL, [], $this->t('Perform upgrade')); + $session->pageTextContains($this->t('Congratulations, you upgraded Drupal!')); + + // Have to reset all the statics after migration to ensure entities are + // loadable. + $this->resetAll(); + } + + /** + * Checks that migrations have been performed successfully. + */ + protected function assertParagraphsMigrationResults() { + $version = $this->getLegacyDrupalVersion($this->sourceDatabase); + + $this->assertEntities(); + + $plugin_manager = $this->container->get('plugin.manager.migration'); + /** @var \Drupal\migrate\Plugin\Migration[] $all_migrations */ + $all_migrations = $plugin_manager->createInstancesByTag('Drupal ' . $version); + + foreach ($all_migrations as $migration) { + $id_map = $migration->getIdMap(); + foreach ($id_map as $source_id => $map) { + // Convert $source_id into a keyless array so that + // \Drupal\migrate\Plugin\migrate\id_map\Sql::getSourceHash() works as + // expected. + $source_id_values = array_values(unserialize($source_id)); + $row = $id_map->getRowBySource($source_id_values); + $destination = serialize($id_map->currentDestination()); + $message = "Migration of $source_id to $destination as part of the {$migration->id()} migration. The source row status is " . $row['source_row_status']; + // A completed migration should have maps with + // MigrateIdMapInterface::STATUS_IGNORED or + // MigrateIdMapInterface::STATUS_IMPORTED. + $this->assertNotSame(MigrateIdMapInterface::STATUS_FAILED, $row['source_row_status'], $message); + $this->assertNotSame(MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $row['source_row_status'], $message); + } + } + } + + /** + * Pass if the page HTML title is the given string. + * + * @param string $expected_title + * The string the page title should be. + * + * @throws \Behat\Mink\Exception\ExpectationException + * Thrown when element doesn't exist, or the title is a different one. + */ + protected function assertPageTitle($expected_title) { + $page_title_element = $this->getSession()->getPage()->find('css', 'h1.page-title'); + if (!$page_title_element) { + throw new ExpectationException('No page title element found on the page', $this->getSession()->getDriver()); + } + $actual_title = $page_title_element->getText(); + $this->assertSame($expected_title, $actual_title, 'The page title is not the same as expected.'); + } + + /** + * Asserts that the expected entities exist. + */ + protected function assertEntities() { + foreach ($this->getExpectedEntities() as $entity_type_id => $expected_entity_labels) { + if ($storage = $this->getEntityStorage($entity_type_id)) { + $entities = $storage->loadMultiple(); + $actual_labels = array_reduce($entities, function ($carry, EntityInterface $entity) { + $carry[$entity->id()] = (string) $entity->label(); + return $carry; + }); + $this->assertEquals($expected_entity_labels, $actual_labels, sprintf('The expected %s entities are not matching the actual ones.', $entity_type_id)); + } + else { + $this->fail(sprintf('The expected %s entity type is missing.', $entity_type_id)); + } + } + } + + /** + * Returns the specified entity's storage when the entity definition exists. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return \Drupal\Core\Entity\EntityStorageInterface|null + * The embed button's entity storage, or NULL if it does not exist. + */ + protected function getEntityStorage(string $entity_type_id) { + $entity_type_manager = $this->container->get('entity_type.manager'); + assert($entity_type_manager instanceof EntityTypeManagerInterface); + + try { + $storage = $entity_type_manager->getStorage($entity_type_id); + } + catch (PluginNotFoundException $e) { + // The entity type does not exist. + return NULL; + } + + return $storage; + } + + /** + * Sets the type of the node migration. + * + * @param bool $classic_node_migration + * Whether nodes should be migrated with the 'classic' way. If this is + * FALSE, and the current Drupal instance has the 'complete' migration, then + * the complete node migration will be used. + */ + protected function setClassicNodeMigration(bool $classic_node_migration) { + $current_method = Settings::get('migrate_node_migrate_type_classic', FALSE); + + if ($current_method !== $classic_node_migration) { + $settings['settings']['migrate_node_migrate_type_classic'] = (object) [ + 'value' => $classic_node_migration, + 'required' => TRUE, + ]; + $this->writeSettings($settings); + } + } + +} diff --git a/tests/src/Kernel/migrate/ParagraphContentMigrationTest.php b/tests/src/Kernel/migrate/ParagraphContentMigrationTest.php new file mode 100644 index 0000000..52033a4 --- /dev/null +++ b/tests/src/Kernel/migrate/ParagraphContentMigrationTest.php @@ -0,0 +1,113 @@ +installEntitySchema('file'); + $this->installEntitySchema('node'); + $this->installEntitySchema('paragraph'); + $this->installEntitySchema('comment'); + $this->installSchema('node', ['node_access']); + $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:blog' => [], + 'd7_node:book' => [], + 'd7_node:forum' => [], + 'd7_node:test_content_type' => [], + ]); + } + + /** + * Tests the migration of a content with paragraphs and field collections. + * + * @dataProvider providerParagraphContentMigration + */ + public function testParagraphContentMigration($migration_to_run) { + if ($migration_to_run) { + // Drupal 8.8.x only has 'classic' node migrations. + // @see https://www.drupal.org/node/3105503 + if (strpos($migration_to_run, 'd7_node_complete') === 0 && version_compare(\Drupal::VERSION, '8.9', '<')) { + $this->pass("Drupal 8.8.x has only the 'classic' node migration."); + return; + } + + $this->executeMigration($migration_to_run); + } + + $this->assertNode8Paragraphs(); + + $this->assertNode9Paragraphs(); + + $node_9 = Node::load(9); + if ($node_9 instanceof TranslatableInterface && !empty($node_9->getTranslationLanguages(FALSE))) { + $this->assertIcelandicNode9Paragraphs(); + } + } + + /** + * Provides data and expected results for testing paragraph migrations. + * + * @return string[][] + * The node migration to run. + */ + public function providerParagraphContentMigration() { + return [ + ['node_migration' => NULL], + ['node_migration' => 'd7_node_revision:paragraphs_test'], + ['node_migration' => 'd7_node_translation:paragraphs_test'], + ['node_migration' => 'd7_node_complete:paragraphs_test'], + ]; + } + +} diff --git a/tests/src/Kernel/migrate/ParagraphsFieldMigrationTest.php b/tests/src/Kernel/migrate/ParagraphsFieldMigrationTest.php index e4d93d7..41a8e5b 100644 --- a/tests/src/Kernel/migrate/ParagraphsFieldMigrationTest.php +++ b/tests/src/Kernel/migrate/ParagraphsFieldMigrationTest.php @@ -22,16 +22,13 @@ class ParagraphsFieldMigrationTest extends ParagraphsMigrationTestBase { 'comment', 'datetime', 'datetime_range', - 'entity_reference_revisions', 'field', 'file', 'image', 'link', 'menu_ui', - 'migrate_drupal', 'node', 'options', - 'paragraphs', 'system', 'taxonomy', 'telephone', diff --git a/tests/src/Kernel/migrate/ParagraphsItemRevisionSourceTest.php b/tests/src/Kernel/migrate/ParagraphsItemRevisionSourceTest.php index 70c3fa5..a9bd78d 100644 --- a/tests/src/Kernel/migrate/ParagraphsItemRevisionSourceTest.php +++ b/tests/src/Kernel/migrate/ParagraphsItemRevisionSourceTest.php @@ -31,6 +31,8 @@ class ParagraphsItemRevisionSourceTest extends MigrateSqlSourceTestBase { 'field_name' => 'field_paragraphs_field', 'bundle' => 'paragraphs_field', 'archived' => '0', + 'parent_id' => '42', + 'parent_type' => 'taxonomy_term', 'field_text' => [ 0 => [ 'value' => 'PID2R2 text', diff --git a/tests/src/Kernel/migrate/ParagraphsItemSourceTest.php b/tests/src/Kernel/migrate/ParagraphsItemSourceTest.php index 1ab9d2e..562a60a 100644 --- a/tests/src/Kernel/migrate/ParagraphsItemSourceTest.php +++ b/tests/src/Kernel/migrate/ParagraphsItemSourceTest.php @@ -31,6 +31,8 @@ class ParagraphsItemSourceTest extends MigrateSqlSourceTestBase { 'field_name' => 'field_paragraphs_field', 'bundle' => 'paragraphs_field', 'archived' => '0', + 'parent_id' => '5', + 'parent_type' => 'node', 'field_text' => [ 0 => [ 'value' => 'PID1R1 text', @@ -43,6 +45,8 @@ class ParagraphsItemSourceTest extends MigrateSqlSourceTestBase { 'field_name' => 'field_paragraphs_field', 'bundle' => 'paragraphs_field', 'archived' => '0', + 'parent_id' => '42', + 'parent_type' => 'taxonomy_term', 'field_text' => [ 0 => [ 'value' => 'PID2R3 text', diff --git a/tests/src/Kernel/migrate/ParagraphsMigrationTestBase.php b/tests/src/Kernel/migrate/ParagraphsMigrationTestBase.php index 4766443..07876a8 100644 --- a/tests/src/Kernel/migrate/ParagraphsMigrationTestBase.php +++ b/tests/src/Kernel/migrate/ParagraphsMigrationTestBase.php @@ -13,6 +13,16 @@ use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase; */ abstract class ParagraphsMigrationTestBase extends MigrateDrupalTestBase { + /** + * {@inheritdoc} + */ + public static $modules = [ + 'entity_reference_revisions', + 'migrate', + 'migrate_drupal', + 'paragraphs', + ]; + /** * {@inheritdoc} */ @@ -119,8 +129,7 @@ abstract class ParagraphsMigrationTestBase extends MigrateDrupalTestBase { * {@inheritdoc} */ protected function prepareMigration(MigrationInterface $migration) { - - // We want to run the revision migraiton without running all the node + // We want to run the revision migration without running all the node // migrations. if ($migration->id() == 'd7_node_revision:paragraphs_test') { $migration->set('migration_dependencies', [ @@ -128,7 +137,6 @@ abstract class ParagraphsMigrationTestBase extends MigrateDrupalTestBase { 'optional' => [], ]); $migration->set('requirements', ['d7_node:paragraphs_test' => 'd7_node:paragraphs_test']); - } } diff --git a/tests/src/Kernel/migrate/ParagraphsTypeMigrationTest.php b/tests/src/Kernel/migrate/ParagraphsTypeMigrationTest.php index ecb2d28..1586d4d 100644 --- a/tests/src/Kernel/migrate/ParagraphsTypeMigrationTest.php +++ b/tests/src/Kernel/migrate/ParagraphsTypeMigrationTest.php @@ -9,14 +9,6 @@ namespace Drupal\Tests\paragraphs\Kernel\migrate; */ class ParagraphsTypeMigrationTest extends ParagraphsMigrationTestBase { - /** - * {@inheritdoc} - */ - public static $modules = [ - 'migrate', - 'paragraphs', - ]; - /** * Test if the paragraph/fc types were brought over as a paragraph. */ diff --git a/tests/src/Traits/ParagraphsNodeMigrationAssertionsTrait.php b/tests/src/Traits/ParagraphsNodeMigrationAssertionsTrait.php new file mode 100644 index 0000000..ed28675 --- /dev/null +++ b/tests/src/Traits/ParagraphsNodeMigrationAssertionsTrait.php @@ -0,0 +1,136 @@ +getReferencedEntities($node_8, 'field_field_collection_test', 2); + $this->assertEquals('Field Collection Text Data One UND', $node_8_field_collection_field_entities[0]->field_text->value); + $this->assertEquals('1', $node_8_field_collection_field_entities[0]->field_integer_list->value); + $this->assertEquals('Field Collection Text Data Two UND', $node_8_field_collection_field_entities[1]->field_text->value); + $this->assertNull($node_8_field_collection_field_entities[1]->field_integer_list->value); + // Check 'any paragraph' field. + $node_8_field_any_paragraph_entities = $this->getReferencedEntities($node_8, 'field_any_paragraph', 2); + $this->assertEquals('Paragraph Field One Bundle One UND', $node_8_field_any_paragraph_entities[0]->field_text->value); + $this->assertEquals('Some Text', $node_8_field_any_paragraph_entities[0]->field_text_list->value); + $this->assertEquals('Paragraph Field One Bundle Two UND', $node_8_field_any_paragraph_entities[1]->field_text->value); + $this->assertEquals('joe@joe.com', $node_8_field_any_paragraph_entities[1]->field_email->value); + // Check 'paragraph one only' field. + $node_8_field_paragraph_one_only_entities = $this->getReferencedEntities($node_8, 'field_paragraph_one_only', 1); + $this->assertEquals('Paragraph Field Two Bundle One Revision Two UND', $node_8_field_paragraph_one_only_entities[0]->field_text->value); + $this->assertEquals('Some more text', $node_8_field_paragraph_one_only_entities[0]->field_text_list->value); + // Check 'nested fc outer' field. + $node_8_field_nested_fc_outer_entities = $this->getReferencedEntities($node_8, 'field_nested_fc_outer', 1); + assert($node_8_field_nested_fc_outer_entities[0] instanceof ParagraphInterface); + $node_8_inner_nested_fc_0_entities = $this->getReferencedEntities($node_8_field_nested_fc_outer_entities[0], 'field_nested_fc_inner', 1); + $this->assertEquals('Nested FC test text', $node_8_inner_nested_fc_0_entities[0]->field_text->value); + } + + /** + * Assertions of node 9. + */ + protected function assertNode9Paragraphs() { + $node_9 = Node::load(9); + assert($node_9 instanceof NodeInterface); + + if ($this->container->get('module_handler')->moduleExists('content_translation') && $node_9 instanceof TranslatableInterface) { + // Test the default translation. + $node_9 = $node_9->getUntranslated(); + $this->assertSame('en', $node_9->language()->getId()); + } + + // Check 'field collection test' field. + $node_9_field_collection_field_entities = $this->getReferencedEntities($node_9, 'field_field_collection_test', 1); + $this->assertEquals('Field Collection Text Data Two EN', $node_9_field_collection_field_entities[0]->field_text->value); + $this->assertEquals('2', $node_9_field_collection_field_entities[0]->field_integer_list->value); + // Check 'any paragraph' field. + $node_9_field_any_paragraph_entities = $this->getReferencedEntities($node_9, 'field_any_paragraph', 2); + $this->assertEquals('Paragraph Field One Bundle One EN', $node_9_field_any_paragraph_entities[0]->field_text->value); + $this->assertEquals('Some Text', $node_9_field_any_paragraph_entities[0]->field_text_list->value); + $this->assertEquals('Paragraph Field One Bundle Two EN', $node_9_field_any_paragraph_entities[1]->field_text->value); + $this->assertEquals('jose@jose.com', $node_9_field_any_paragraph_entities[1]->field_email->value); + // Check 'paragraph one only' field. + $node_9_field_paragraph_one_only_entities = $this->getReferencedEntities($node_9, 'field_paragraph_one_only', 1); + $this->assertEquals('Paragraph Field Two Bundle One EN', $node_9_field_paragraph_one_only_entities[0]->field_text->value); + $this->assertEquals('Some Text', $node_9_field_paragraph_one_only_entities[0]->field_text_list->value); + // The 'nested fc outer' field should be empty. + $this->getReferencedEntities($node_9, 'field_nested_fc_outer', 0); + } + + /** + * Assertions of the Icelandic translation of node 9. + */ + protected function assertIcelandicNode9Paragraphs() { + // Confirm that the Icelandic translation of node 9 (which was node 10 on + // the source site) has the expected data. + $node_9 = Node::load(9); + assert($node_9 instanceof NodeInterface); + assert($node_9 instanceof TranslatableInterface); + $node_9_translation_languages = $node_9->getTranslationLanguages(FALSE); + $this->assertEquals(['is'], array_keys($node_9_translation_languages)); + $node_9 = $node_9->getTranslation('is'); + $this->assertSame('is', $node_9->language()->getId()); + + // Check 'field collection test' field. + $node_9_field_collection_field_entities = $this->getReferencedEntities($node_9, 'field_field_collection_test', 3); + $this->assertEquals('Field Collection Text Data One IS', $node_9_field_collection_field_entities[0]->field_text->value); + $this->assertEquals('1', $node_9_field_collection_field_entities[0]->field_integer_list->value); + $this->assertEquals('Field Collection Text Data Two IS', $node_9_field_collection_field_entities[1]->field_text->value); + $this->assertEquals('2', $node_9_field_collection_field_entities[1]->field_integer_list->value); + $this->assertEquals('Field Collection Text Data Three IS', $node_9_field_collection_field_entities[2]->field_text->value); + $this->assertEquals('3', $node_9_field_collection_field_entities[2]->field_integer_list->value); + // Check 'any paragraph' field. + $node_9_field_any_paragraph_entities = $this->getReferencedEntities($node_9, 'field_any_paragraph', 3); + $this->assertEquals('Paragraph Field One Bundle One IS', $node_9_field_any_paragraph_entities[0]->field_text->value); + $this->assertEquals('Some Text', $node_9_field_any_paragraph_entities[0]->field_text_list->value); + $this->assertEquals('Paragraph Field One Bundle Two IS', $node_9_field_any_paragraph_entities[1]->field_text->value); + $this->assertEquals('jose@jose.com', $node_9_field_any_paragraph_entities[1]->field_email->value); + $this->assertEquals('Paragraph Field One Bundle Two Delta 3 IS', $node_9_field_any_paragraph_entities[2]->field_text->value); + $this->assertEquals('john@john.com', $node_9_field_any_paragraph_entities[2]->field_email->value); + // Check 'paragraph one only' field. + $node_9_field_paragraph_one_only_entities = $this->getReferencedEntities($node_9, 'field_paragraph_one_only', 1); + $this->assertEquals('Paragraph Field Two Bundle One IS', $node_9_field_paragraph_one_only_entities[0]->field_text->value); + $this->assertEquals('Some more text', $node_9_field_paragraph_one_only_entities[0]->field_text_list->value); + // The 'nested fc outer' field should be empty. + $this->getReferencedEntities($node_9, 'field_nested_fc_outer', 0); + } + + /** + * Get the referred entities. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The parent entity. + * @param string $field_name + * The name of the entity revision reference field. + * @param int $expected_count + * The expected number of the referenced entities. + * + * @return \Drupal\Core\Entity\EntityInterface[] + * An array of entity objects keyed by field item deltas. + */ + protected function getReferencedEntities(ContentEntityInterface $entity, $field_name, int $expected_count) { + $entity_field = $entity->hasField($field_name) ? + $entity->get($field_name) : + NULL; + assert($entity_field instanceof EntityReferenceRevisionsFieldItemList); + $entity_field_entities = $entity_field->referencedEntities(); + $this->assertCount($expected_count, $entity_field_entities); + + return $entity_field_entities; + } + +} diff --git a/tests/src/Traits/ParagraphsSourceData.php b/tests/src/Traits/ParagraphsSourceData.php index 970e0da..aba892a 100644 --- a/tests/src/Traits/ParagraphsSourceData.php +++ b/tests/src/Traits/ParagraphsSourceData.php @@ -16,7 +16,7 @@ trait ParagraphsSourceData { protected function getSourceData() { $data = []; - $data[]['source_data'] = [ + $data[0]['source_data'] = [ 'paragraphs_bundle' => [ [ 'bundle' => 'paragraphs_field', @@ -103,7 +103,31 @@ trait ParagraphsSourceData { 'revision_id' => '3', ], ], + 'field_data_field_paragraphs_field' => [ + [ + 'entity_type' => 'node', + 'entity_id' => '5', + // @todo Don't we have to match also entity revision IDs? + // 'revision_id' => 'something', + 'field_paragraphs_field_value' => '1', + 'field_paragraphs_field_revision_id' => '1', + ], + [ + 'entity_type' => 'taxonomy_term', + 'entity_id' => '42', + 'field_paragraphs_field_value' => '2', + 'field_paragraphs_field_revision_id' => '3', + ], + ], ]; + $data[0]['source_data']['field_revision_field_paragraphs_field'] = array_merge($data[0]['source_data']['field_data_field_paragraphs_field'], [ + [ + 'entity_type' => 'taxonomy_term', + 'entity_id' => '42', + 'field_paragraphs_field_value' => '2', + 'field_paragraphs_field_revision_id' => '2', + ], + ]); return $data; } diff --git a/tests/src/Unit/migrate/MigrationPluginsAltererTest.php b/tests/src/Unit/migrate/MigrationPluginsAltererTest.php new file mode 100644 index 0000000..63eb45e --- /dev/null +++ b/tests/src/Unit/migrate/MigrationPluginsAltererTest.php @@ -0,0 +1,197 @@ +createMock('Drupal\Core\Logger\LoggerChannelInterface'); + $logger_factory = $this->getMockBuilder('Drupal\Core\Logger\LoggerChannelFactory') + ->getMock(); + $logger_factory->expects($this->atLeastOnce()) + ->method('get') + ->with('paragraphs') + ->will($this->returnValue($logger_channel)); + + $this->paragraphsMigrationPluginsAlterer = new MigrationPluginsAlterer($logger_factory); + } + + /** + * Tests that migration processes are transformed to an array of processors. + * + * @dataProvider providerParagraphsMigrationPrepareProcess + * @covers ::paragraphsMigrationPrepareProcess + */ + public function testParagraphsMigrationPrepareProcess(array $input, array $expected) { + ['process' => $process, 'property' => $property] = $input; + $success = $this->paragraphsMigrationPluginsAlterer->paragraphsMigrationPrepareProcess($process, $property); + $this->assertSame($expected['return'], $success); + $this->assertEquals($expected['process'], $process); + } + + /** + * Provides data and expected results for testing the prepare process method. + * + * @return array[] + * Data and expected results. + */ + public function providerParagraphsMigrationPrepareProcess() { + return [ + // Missing property (no change). + [ + 'input' => [ + 'process' => [ + 'catname' => 'Picurka', + 'wont/touch' => 'this', + ], + 'property' => 'missing', + ], + 'expected' => [ + 'return' => FALSE, + 'process' => [ + 'catname' => 'Picurka', + 'wont/touch' => 'this', + ], + ], + ], + // Existing string property. + [ + 'input' => [ + 'process' => [ + 'catname' => 'Picurka', + 'wont/touch' => 'this', + ], + 'property' => 'catname', + ], + 'expected' => [ + 'return' => TRUE, + 'process' => [ + 'catname' => [ + [ + 'plugin' => 'get', + 'source' => 'Picurka', + ], + ], + 'wont/touch' => 'this', + ], + ], + ], + // Single process plugin. + [ + 'input' => [ + 'process' => [ + 'cat' => [ + 'plugin' => 'migration_lookup', + 'migration' => 'cats', + 'source' => 'cat_id', + ], + ], + 'property' => 'cat', + ], + 'expected' => [ + 'return' => TRUE, + 'process' => [ + 'cat' => [ + [ + 'plugin' => 'migration_lookup', + 'migration' => 'cats', + 'source' => 'cat_id', + ], + ], + ], + ], + ], + // Array of process plugins (no change). + [ + 'input' => [ + 'process' => [ + 'catname' => [ + [ + 'plugin' => 'migration_lookup', + 'migration' => 'cats', + 'source' => 'cat_id', + ], + [ + 'plugin' => 'extract', + 'index' => ['name'], + ], + [ + 'plugin' => 'callback', + 'callable' => 'ucfirst', + ], + ], + ], + 'property' => 'catname', + ], + 'expected' => [ + 'return' => TRUE, + 'process' => [ + 'catname' => [ + [ + 'plugin' => 'migration_lookup', + 'migration' => 'cats', + 'source' => 'cat_id', + ], + [ + 'plugin' => 'extract', + 'index' => ['name'], + ], + [ + 'plugin' => 'callback', + 'callable' => 'ucfirst', + ], + ], + ], + ], + ], + // Invalid type. + [ + 'input' => [ + 'process' => [ + 'invalid' => (object) [ + [ + 'not a' => 'kitten', + ], + ], + ], + 'property' => 'invalid', + ], + 'expected' => [ + 'return' => FALSE, + 'process' => [ + 'invalid' => (object) [ + [ + 'not a' => 'kitten', + ], + ], + ], + ], + ], + ]; + } + +}