diff --git a/core/modules/field/migration_templates/d7_field.yml b/core/modules/field/migration_templates/d7_field.yml index 15079d8..ab715ea 100644 --- a/core/modules/field/migration_templates/d7_field.yml +++ b/core/modules/field/migration_templates/d7_field.yml @@ -15,26 +15,42 @@ process: langcode: 'constants/langcode' field_name: field_name type: - plugin: field_type - source: type - map: - date: datetime - datestamp: datetime - datetime: datetime - email: email - entityreference: entity_reference - file: file - image: image - link_field: link - list_boolean: boolean - list_integer: list_integer - list_text: list_string - number_integer: integer - number_decimal: decimal - number_float: float - phone: telephone - text_long: text_long - text_with_summary: text_with_summary + - + plugin: field_type + source: type + bypass: true + map: + date: datetime + datestamp: datetime + datetime: datetime + email: email + entityreference: entity_reference + file: file + image: image + link_field: link + list_boolean: boolean + list_integer: list_integer + list_text: list_string + number_integer: integer + number_decimal: decimal + number_float: float + phone: telephone + text: text + text_long: text_long + text_with_summary: text_with_summary + string: string + string_long: string_long + - + # If a text or text_long field base has both plain text and filtered text + # instances, the Drupal\field\Plugin\migrate\source\d7\Field source plugin + # will return null so we can skip the row and log a message. + plugin: skip_on_empty + method: row + message: > + This text field base was not migrated because it has both plain text and + filtered text instances. You can either fix this on the source site by + using only plain text instances or only filtered text instances per text + field base and migrate again, or you can create a custom migration. # Translatable is not migrated and the Drupal 8 default of true is used. # If translatable is false in field storage then the field can not be # set to translatable via the UI. diff --git a/core/modules/field/migration_templates/d7_field_instance.yml b/core/modules/field/migration_templates/d7_field_instance.yml index f3518c9..604b756 100644 --- a/core/modules/field/migration_templates/d7_field_instance.yml +++ b/core/modules/field/migration_templates/d7_field_instance.yml @@ -9,6 +9,21 @@ source: constants: status: true process: + type: + # This value is not used for anything other than to check whether the field + # instance that is being migrated is using a text or text_long field base + # that has both plain text and filtered text instances. If so, the + # Drupal\field\Plugin\migrate\source\d7\Field source plugin will return null + # so we can skip the row and log a message. + plugin: skip_on_empty + source: type + method: row + message: > + This text field instance was not migrated because it is using a field base + that has both plain text and filtered text instances. You can either fix + this on the source site by using only plain text instances or only + filtered text instances per text field base and migrate again, or you can + create a custom migration. entity_type: entity_type field_name: field_name bundle: bundle diff --git a/core/modules/field/src/Plugin/migrate/source/d7/Field.php b/core/modules/field/src/Plugin/migrate/source/d7/Field.php index d9aef91..b689b95 100644 --- a/core/modules/field/src/Plugin/migrate/source/d7/Field.php +++ b/core/modules/field/src/Plugin/migrate/source/d7/Field.php @@ -3,7 +3,6 @@ namespace Drupal\field\Plugin\migrate\source\d7; use Drupal\migrate\Row; -use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; /** * Drupal 7 field source from database. @@ -12,21 +11,14 @@ * id = "d7_field" * ) */ -class Field extends DrupalSqlBase { +class Field extends FieldBase { /** * {@inheritdoc} */ public function query() { - $query = $this->select('field_config', 'fc') - ->distinct() - ->fields('fc') - ->fields('fci', ['entity_type']) - ->condition('fc.active', 1) - ->condition('fc.deleted', 0) - ->condition('fc.storage_active', 1); - $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id'); - + $query = parent::query(); + $query->fields('fc', ['module', 'locked', 'data', 'cardinality']); return $query; } @@ -71,4 +63,12 @@ public function getIds() { ]; } + /** + * {@inheritdoc} + */ + protected function instancesRows($instances) { + // Field bases only want one row per instance-set. + return [$instances[0]]; + } + } diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldBase.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldBase.php new file mode 100644 index 0000000..49ec150 --- /dev/null +++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldBase.php @@ -0,0 +1,121 @@ +select('field_config', 'fc'); + $query->join('field_config_instance', 'fci', 'fc.id = fci.field_id'); + $query->fields('fc', ['type', 'field_name']) + ->fields('fci', ['entity_type']) + ->condition('fc.active', 1) + ->condition('fc.deleted', 0) + ->condition('fc.storage_active', 1); + $query->addField('fci', 'data', 'instance_data'); + return $query; + } + + /** + * Determine which rows to yield for each set of instances. + * + * @param array $instances + * The data for the field's instances. + * + * @return array + * A list of row arrays to yield. + */ + abstract protected function instancesRows($instances); + + /** + * Determine the type of a field. + * + * @param array $instances + * The data for the field's instances. + * + * @return string + * The field type. + */ + protected function getType($instances) { + $type = NULL; + $text_field = FALSE; + $plain_text = FALSE; + $filtered_text = FALSE; + + foreach ($instances as $instance) { + if (!isset($type)) { + $type = $instance['type']; + $text_field = in_array($type, ['text', 'text_long']); + } + + // If this is a text or text_long field, check if it has plain text + // instances, filtered text instances, or both. + if ($text_field) { + $data = unserialize($instance['instance_data']); + switch ($data['settings']['text_processing']) { + case '0': + $plain_text = TRUE; + break; + case '1': + $filtered_text = TRUE; + break; + } + } + } + + if ($text_field) { + // If the text or text_long field has only plain text instances, migrate + // it to a string or string_long field respectively. + if ($plain_text && !$filtered_text) { + $type = str_replace(['text', 'text_long'], ['string', 'string_long'], $type); + } + // If the text or text_long field has both plain text and filtered text + // instances, return NULL so we can skip it and log a message. + elseif ($plain_text && $filtered_text) { + $type = NULL; + } + } + + return $type; + } + + /** + * {@inheritdoc} + */ + protected function initializeIterator() { + // Collate fields by D8 instance. + $instanceSets = []; + $results = $this->prepareQuery()->execute(); + foreach ($results as $result) { + $instanceKey = $result['entity_type'] . ':' . $result['field_name']; + $instanceSets[$instanceKey][] = $result; + } + + // Figure out the field type for each instance. + $rows = []; + foreach ($instanceSets as $instances) { + $type = $this->getType($instances); + foreach ($this->instancesRows($instances) as $row) { + $row['type'] = $type; + $rows[] = $row; + } + } + return new \ArrayIterator($rows); + } + + /** + * {@inheritdoc} + */ + public function count() { + return $this->initializeIterator()->count(); + } + +} diff --git a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php index d06336e..458edbc 100644 --- a/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php +++ b/core/modules/field/src/Plugin/migrate/source/d7/FieldInstance.php @@ -3,7 +3,6 @@ namespace Drupal\field\Plugin\migrate\source\d7; use Drupal\migrate\Row; -use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase; /** * Drupal 7 field instances source from database. @@ -13,21 +12,15 @@ * source_provider = "field" * ) */ -class FieldInstance extends DrupalSqlBase { +class FieldInstance extends FieldBase { /** * {@inheritdoc} */ public function query() { - $query = $this->select('field_config_instance', 'fci') - ->fields('fci') - ->condition('fci.deleted', 0) - ->condition('fc.active', 1) - ->condition('fc.deleted', 0) - ->condition('fc.storage_active', 1) - ->fields('fc', ['type']); - - $query->innerJoin('field_config', 'fc', 'fci.field_id = fc.id'); + $query = parent::query(); + $query->fields('fci', ['bundle', 'data']) + ->condition('fci.deleted', 0); $query->addField('fc', 'data', 'field_data'); // Optionally filter by entity type and bundle. @@ -128,4 +121,11 @@ public function getIds() { ]; } + /** + * {@inheritdoc} + */ + protected function instancesRows($instances) { + return $instances; + } + } diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php index 1e3df9e..e843c95 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldInstanceTest.php @@ -144,7 +144,7 @@ public function testFieldInstances() { $this->assertEntity('node.test_content_type.field_integer_list', 'Integer List', 'list_integer', FALSE, FALSE); $this->assertEntity('node.test_content_type.field_long_text', 'Long text', 'text_with_summary', FALSE, FALSE); $this->assertEntity('node.test_content_type.field_term_reference', 'Term Reference', 'entity_reference', FALSE, FALSE); - $this->assertEntity('node.test_content_type.field_text', 'Text', 'text', FALSE, FALSE); + $this->assertEntity('node.test_content_type.field_text', 'Text', 'string', FALSE, FALSE); $this->assertEntity('comment.comment_node_test_content_type.field_integer', 'Integer', 'integer', FALSE, FALSE); $this->assertEntity('user.user.field_file', 'File', 'file', FALSE, FALSE); @@ -152,6 +152,38 @@ public function testFieldInstances() { $this->assertLinkFields('node.test_content_type.field_link', DRUPAL_OPTIONAL); $this->assertLinkFields('node.article.field_link', DRUPAL_DISABLED); $this->assertLinkFields('node.blog.field_link', DRUPAL_REQUIRED); + + // All text and text_long field instances using a field base that has only + // plain text instances should be migrated to string and string_long fields. + $this->assertEntity('node.page.field_text_plain', 'Text plain', 'string', FALSE, FALSE); + $this->assertEntity('node.article.field_text_plain', 'Text plain', 'string', FALSE, TRUE); + $this->assertEntity('node.page.field_long_text_plain', 'Long text plain', 'string_long', FALSE, FALSE); + $this->assertEntity('node.article.field_long_text_plain', 'Long text plain', 'string_long', FALSE, TRUE); + + // All text and text_long field instances using a field base that has only + // filtered text instances should be migrated to text and text_long fields. + $this->assertEntity('node.page.field_text_filtered', 'Text filtered', 'text', FALSE, FALSE); + $this->assertEntity('node.article.field_text_filtered', 'Text filtered', 'text', FALSE, TRUE); + $this->assertEntity('node.page.field_long_text_filtered', 'Long text filtered', 'text_long', FALSE, FALSE); + $this->assertEntity('node.article.field_long_text_filtered', 'Long text filtered', 'text_long', FALSE, TRUE); + + // All text and text_long field instances using a field base that has both + // plain text and filtered text instances should not have been migrated. + $this->assertNull(FieldConfig::load('node.page.field_text_plain_filtered')); + $this->assertNull(FieldConfig::load('node.article.field_text_plain_filtered')); + $this->assertNull(FieldConfig::load('node.page.field_long_text_plain_filtered')); + $this->assertNull(FieldConfig::load('node.article.field_long_text_plain_filtered')); + + // For each text field instances that were skipped, there should be a log + // message with the required steps to fix this. + $migration = $this->getMigration('d7_field_instance'); + $messages = $migration->getIdMap()->getMessageIterator()->fetchAll(); + $this->assertCount(4, $messages); + $message = 'This text field instance was not migrated because it is using a field base that has both plain text and filtered text instances. You can either fix this on the source site by using only plain text instances or only filtered text instances per text field base and migrate again, or you can create a custom migration.'; + $this->assertEquals($message, $messages[0]->message); + $this->assertEquals($message, $messages[1]->message); + $this->assertEquals($message, $messages[2]->message); + $this->assertEquals($message, $messages[3]->message); } } diff --git a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php index ab2fe8e..fdd3acb 100644 --- a/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php +++ b/core/modules/field/tests/src/Kernel/Migrate/d7/MigrateFieldTest.php @@ -91,7 +91,7 @@ public function testFields() { $this->assertEntity('node.field_tags', 'entity_reference', TRUE, -1); $this->assertEntity('node.field_term_reference', 'entity_reference', TRUE, 1); $this->assertEntity('node.taxonomy_forums', 'entity_reference', TRUE, 1); - $this->assertEntity('node.field_text', 'text', TRUE, 1); + $this->assertEntity('node.field_text', 'string', TRUE, 1); $this->assertEntity('node.field_text_list', 'list_string', TRUE, 3); $this->assertEntity('node.field_boolean', 'boolean', TRUE, 1); $this->assertEntity('node.field_email', 'email', TRUE, -1); @@ -122,7 +122,30 @@ public function testFields() { /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ $migration = $this->getMigration('d7_field'); $this->assertSame($migration->getSourcePlugin() - ->count(), $migration->getIdMap()->processedCount()); + ->count(), (int) ($migration->getIdMap()->processedCount())); + + // All text and text_long field bases that have only plain text instances + // should be migrated to string and string_long fields. + $this->assertEntity('node.field_text_plain', 'string', TRUE, 1); + $this->assertEntity('node.field_long_text_plain', 'string_long', TRUE, 1); + + // All text and text_long field bases that have only filtered text instances + // should be migrated to text and text_long fields. + $this->assertEntity('node.field_text_filtered', 'text', TRUE, 1); + $this->assertEntity('node.field_long_text_filtered', 'text_long', TRUE, 1); + + // All text and text_long field bases that have both plain text and filtered + // text instances should not have been migrated. + $this->assertNull(FieldStorageConfig::load('node.field_text_plain_filtered')); + $this->assertNull(FieldStorageConfig::load('node.field_long_text_plain_filtered')); + + // For each text field bases that were skipped, there should be a log + // message with the required steps to fix this. + $messages = $migration->getIdMap()->getMessageIterator()->fetchAll(); + $this->assertCount(2, $messages); + $message = 'This text field base was not migrated because it has both plain text and filtered text instances. You can either fix this on the source site by using only plain text instances or only filtered text instances per text field base and migrate again, or you can create a custom migration.'; + $this->assertEquals($message, $messages[0]->message); + $this->assertEquals($message, $messages[1]->message); } } diff --git a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php index 1d41219..a7ab809 100644 --- a/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php +++ b/core/modules/migrate_drupal_ui/tests/src/Functional/d7/MigrateUpgrade7Test.php @@ -45,8 +45,8 @@ protected function getEntityCounts() { 'configurable_language' => 4, 'contact_form' => 3, 'editor' => 2, - 'field_config' => 52, - 'field_storage_config' => 39, + 'field_config' => 60, + 'field_storage_config' => 43, 'file' => 2, 'filter_format' => 7, 'image_style' => 6,