Series Overview & ToC | Previous Article | Next Article
Article Content:
In the previous article, we learn about Drupal fields, their underlying data structure at the database level, and some considerations for creating custom field migrations. We also pointed out that migrating fields require at least four different migrations. Today, we will start the multi-step process of migrating fields by taking care of the first two components: storage and instance settings.
We are going to initially run the automated migrations with no significant modifications to familiarize ourselves with the process and to identify some of the errors that come up when migrating fields. Then, we are going to use the Migrate Skip Fields module and perform manual configuration updates to our field configuration to account for content model changes.
Before we begin
Our example project involves many content model changes that require adjustments to field configurations. Refer to article 8 for a high level overview of the migration plan. Also refer to the field_instance
tab of the Drupal Site Audit Template for a summary of the content model changes related to fields.
In article 14, we talked about exporting and committing configuration changes to version control. This is useful in case something goes wrong and you need to revert to a previous state of the site configuration. You could also go one step further and create a full database dump after completing or before starting a migration. Importing a database dump is generally easier than restoring a previous state of the configuration, especially if you have uncommitted configuration files. It also has the added benefit of including any content that exists on the site. Some content migrations can take a very long time to complete. Having a snapshot of the (partially) migrated database makes it easier to start from a known checkpoint in case something goes wrong with the next migration. You can use the following DDEV commands to create, list, restore, and delete database backups.
# Create database backup.
ddev snapshot --name [BACKUP_NAME]
# List database backups.
ddev snapshot --list
# Restore database backup.
ddev snapshot restore [BACKUP_NAME]
# Delete all database backups.
ddev snapshot --cleanup
# Delete single database backup.
ls .ddev/db_snapshots/
rm [FILENAME]
Replace [BACKUP_NAME]
by an identifier using underscores (_
) instead of spaces (
) to separate words. The commands above were provided because the example project is set up with DDEV already. If preferred, you could use other tools to create and restore database dumps. For example, drush sql:dump to create a backup and ddev import-db to restore it.
Technical note: The snapshot command in DDEV backs up the entire state of the database server for the project. If you have two or more databases, all of them will be included in the backup. When restoring, all databases will be recreated based on the content of the backup file. Snapshots are stored in the .ddev/db_snapshots
folder as gzipped files. They can be manually and individually deleted if needed.
Field storage and instance configuration
Drupal fields consist of four different configuration elements: storage, instance, widget, and formatter settings. We are migrating the first two today. As a quick reminder, field storage settings are closely related to the underlying structure Drupal uses to save information at the database level. If you re-use fields across multiple bundles of the same entity, all those fields will share the same storage configuration. On the other hand, field instance settings can vary per entity/bundle combination. In many cases, field instance settings are related to validation logic when collecting data. Refer to the previous article for a more thorough explanation of Drupal fields.
We use upgrade_d7_field
and upgrade_d7_field_instance
to migrate field storage and instance settings respectively. Let's copy these migrations from the reference folder into our custom module and rebuild caches for the migrations to be detected.
cd drupal10
cp ref_migrations/migrate_plus.migration.upgrade_d7_field.yml web/modules/custom/tag1_migration/tag1_migration_config/migrations/upgrade_d7_field.yml
cp ref_migrations/migrate_plus.migration.upgrade_d7_field_instance.yml web/modules/custom/tag1_migration/tag1_migration_config/migrations/upgrade_d7_field_instance.yml
ddev drush cache:rebuild
Note that while copying the files, we also changed their names and placed them in a migrations folder inside our custom module. After copying the files, make the following changes:
- Remove the following keys:
uuid
,langcode
,status
,dependencies
,cck_plugin_method
, andmigration_group
. Notice that for field migrations, we preserve thefield_plugin_method
key. - Add the
field_storage_config
tag to theupgrade_d7_field
migration and thefield_config
to theupgrade_d7_field_instance
. Add thetag1_configuration
tag to both migrations. - Add
key: migrate
under the source section. - Update the required
migration_dependencies
to include the migrations that create entities. Given that fields are attached to entities, we want to be explicit about establishing dependencies among migrations. In this case, addupgrade_d7_node_type
,upgrade_d7_taxonomy_vocabulary
, andupgrade_d7_field_collection_type
to both field storage and instance migrations.
After the modifications, the upgrade_d7_field.yml
file should look like this:
id: upgrade_d7_field
class: Drupal\migrate_drupal\Plugin\migrate\FieldMigration
field_plugin_method: alterFieldMigration
migration_tags:
- 'Drupal 7'
- Configuration
- field_storage_config
- tag1_configuration
label: 'Field configuration'
source:
key: migrate
plugin: d7_field
constants:
status: true
langcode: und
process:
entity_type:
-
plugin: get
source: entity_type
-
plugin: static_map
map:
field_collection_item: paragraph
paragraphs_item: paragraph
bypass: true
status:
-
plugin: get
source: constants/status
langcode:
-
plugin: get
source: constants/langcode
field_name:
-
plugin: get
source: field_name
type:
-
plugin: process_field
source: type
method: getFieldType
map:
d7_text:
d7_text: d7_text
taxonomy_term_reference:
taxonomy_term_reference: taxonomy_term_reference
image:
image: image
email:
email: email
field_url:
field_url: field_url
addressfield:
addressfield: addressfield
telephone:
telephone: telephone
entityreference:
entityreference: entityreference
file:
file: file
list:
list: list
datetime:
datetime: datetime
number_default:
number_default: number_default
cardinality:
-
plugin: get
source: cardinality
settings:
0:
plugin: d7_field_settings
field_collection:
plugin: field_collection_field_settings
destination:
plugin: 'entity:field_storage_config'
migration_dependencies:
required:
- upgrade_d7_field_collection_type
- upgrade_d7_node_type
- upgrade_d7_taxonomy_vocabulary
optional: { }
And the upgrade_d7_field_instance.yml
file should look like this:
id: upgrade_d7_field_instance
class: Drupal\migrate_drupal\Plugin\migrate\FieldMigration
field_plugin_method: alterFieldInstanceMigration
migration_tags:
- 'Drupal 7'
- Configuration
- field_config
- tag1_configuration
label: 'Field instance configuration'
source:
key: migrate
plugin: d7_field_instance
constants:
status: true
comment_node: comment_node_
process:
type:
-
plugin: process_field
source: type
method: getFieldType
entity_type:
-
plugin: get
source: entity_type
-
plugin: static_map
map:
field_collection_item: paragraph
paragraphs_item: paragraph
bypass: true
field_name:
-
plugin: get
source: field_name
bundle_mapped:
-
plugin: static_map
source: bundle
bypass: true
map:
comment_node_forum: comment_forum
-
plugin: paragraphs_process_on_value
source_value: entity_type
expected_value: field_collection_item
process:
plugin: paragraphs_strip_field_prefix
_comment_type:
-
plugin: explode
source: bundle
delimiter: comment_node_
-
plugin: extract
index:
- 1
default: false
-
plugin: skip_on_empty
method: process
-
plugin: migration_lookup
migration: { }
bundle:
-
plugin: field_bundle
source:
- entity_type
- '@bundle_mapped'
label:
-
plugin: get
source: label
description:
-
plugin: get
source: description
required:
-
plugin: get
source: required
status:
-
plugin: get
source: constants/status
allowed_values:
-
plugin: sub_process
source: allowed_vid
process:
-
plugin: migration_lookup
migration: upgrade_d7_taxonomy_vocabulary
source: vid
settings:
0:
plugin: d7_field_instance_settings
source:
- settings
- widget
- field_definition
field_collection:
plugin: field_collection_field_instance_settings
default_value_function:
-
plugin: get
source: ''
default_value:
-
plugin: d7_field_instance_defaults
source:
- default_value
- widget
translatable:
-
plugin: get
source: translatable
destination:
plugin: 'entity:field_config'
migration_dependencies:
required:
- upgrade_d7_field
- upgrade_d7_field_collection_type
- upgrade_d7_node_type
- upgrade_d7_taxonomy_vocabulary
optional: { }
Now, rebuild caches for our changes to be detected and execute the migrations. RWe run migrate:status
to make sure we can connect to Drupal 7. Then, we run migrate:import
to perform the import operations. Notice that both commands allow specifying multiple migrations when separated by a comma without spaces between migration IDs. When you specify multiple migrations to be executed, the command will build a dependency tree and execute the migrations trying to respect what is specified in the migration_dependencies
key.
ddev drush cache:rebuild
ddev drush migrate:status upgrade_d7_field,upgrade_d7_field_instance
ddev drush migrate:import upgrade_d7_field,upgrade_d7_field_instance
After executing the migrate:import
operation, we encounter our first error:
[error] Attempt to create a field storage field_video_recording with no type. (/var/www/html/web/core/modules/field/src/Entity/FieldStorageConfig.php:271)
[notice] Processed 28 items (27 created, 0 updated, 1 failed, 0 ignored) in 0.4 seconds (4578.2/min) - done with 'upgrade_d7_field'
In MigrateRunnerCommands.php line 414:
upgrade_d7_field migration: 1 failed.
While it may sound obvious, it is very important to carefully read all messages logged to the console, especially when there are errors. Notice that the error states a problem with the upgrade_d7_field
migration, but there is no reference to upgrade_d7_field_instance
. If one of the migrations triggers an error, the drush migrate:import
command exits immediately and any subsequent migrations are not executed.
For now, ignore the error and try to import field instances again by running ddev drush migrate:import upgrade_d7_field_instance
. You will get a different set of errors:
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id speaker. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Attempted to create, modify or delete an instance of field with name field_video_recording on entity type node when the field storage does not exist. (/var/www/html/web/core/modules/field/src/Entity/FieldConfig.php:316)
[error] Missing bundle entity, entity type node_type, entity id sponsor. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id sponsor. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id swag. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id swag. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[error] Missing bundle entity, entity type node_type, entity id swag. (/var/www/html/web/core/lib/Drupal/Core/Entity/EntityType.php:916)
[notice] Processed 34 items (20 created, 0 updated, 14 failed, 0 ignored) in 0.3 seconds (6051.3/min) - done with 'upgrade_d7_field_instance'
In MigrateRunnerCommands.php line 414:
upgrade_d7_field_instance migration: 14 failed.
Let's analyze the output of executing both migrations. In the case of upgrade_d7_field
, we get a very useful error message: Attempt to create a field storage field_video_recording with no type.
This means that the field type of the field_video_recording
is not supported in Drupal 10. Technically speaking, in Drupal 10 there is no field plugin with the same machine name as the field type name in Drupal 7 nor a field migrate plugin that maps the field type from the source site to one available in the target site. This migration also produced the following notice: Processed 28 items (27 created, 0 updated, 1 failed, 0 ignored)
. 27 out of 28 fields created without errors is not bad at all. It is important to note that this field storage settings was created for all entities that had field attachments in Drupal 7. In our case, that includes content types and paragraph types.
As for the upgrade_d7_field_instance
migration, the error messages are a bit more cryptic. The error Missing bundle entity, entity type node_type, entity id speaker.
means that we are trying to migrate a field that belongs to an entity/bundle combination that does not exist in Drupal 10. In our example, this is the result of skipping some content types in the upgrade_d7_node_type
migration. Any Drupal 7 field attached to a content type that was skipped in Drupal 10 will yield this error. The other error — Attempted to create, modify or delete an instance of field with name field_video_recording on entity type node when the field storage does not exist.
— is more self explanatory. A field instance setting depends on the corresponding field storage setting to exist. In the upgrade_d7_field
, it was not possible to migrate the storage settings for the field_video_recording
; therefore, it is not possible to migrate the instance settings for a non-existent field. The field instance migration produced the following notice: Processed 34 items (20 created, 0 updated, 14 failed, 0 ignored)
. A higher percentage of failed records, but that is expected given we skipped three content types will all their fields as part of our custom migration. All the fields attached to those content types will yield an error when trying to migrate them.
Making sense of error messages might be challenging. Reading online documentation, asking for support in community channels, following guides like this series, debugging migrations, and a lot of practice will help you understand the different error messages that you will inevitably encounter in real life migration projects.
To get more context on a failed record, check the migration messages logged during the import operation. You can do that using the drush migrate:messages
command or the migration messages admin interface. Executing the ddev drush migrate:messages upgrade_d7_field
command we get... nothing. Even though there were records that failed to process, the migrate API did not log the reasons they failed. The only information available is the error message printed to the console. . To find the source of the error, we can search the codebase for the error message. In this case, the error is produced in the \Drupal\field\Entity\FieldStorage class. While this technical detail might not provide a lot of context, it is always helpful to identify where errors originate. This helps in understanding why it happens and makes it easier to find a solution.
Now we will check if the upgrade_d7_field_instance
migration logged any errors. Feel free to execute the ddev drush migrate:messages upgrade_d7_field_instance
command and see the results in the command line. Alternatively, go to https://migration-drupal10.ddev.site/admin/reports/migration-messages
and click on the link indicating the message count for the upgrade_d7_field_instance
.
The report provides more context as to why an error was thrown. Namely, you can see which entity type / bundle / field name combination triggered the error. Here, all the Missing bundle entity, entity type node_type, entity id
errors are related to fields attached to content types we explicitly skipped: speaker
, session
, and swag
. We will learn how to prevent these errors from happening, but for now they are to be expected.
Before moving forward, let's review the result of our migrations. The migrations reported that most field storage and instance settings were created. You can go to the content types and paragraph types administration pages and review the fields attached to them. For example, below is a screenshot of the fields in the Event
content type.
The good news is that many fields are already added to the content type. The bad news is that we received a couple of cryptic error messages. Again, prior experience and some intuition will go a long way into understanding and fixing these errors. Pay close attention to the Field type
configuration for field_sponsors
. It says it is an entity reference field, allowing references to nodes (content), of type... nothing? If we click on the edit button, we see that there is no content type selected. When we review this field in Drupal 7, we notice that it allows referencing nodes of type Sponsor
, but we skipped that content type altogether in Drupal 10. Sponsor nodes are going to be migrated as taxonomy vocabularies. Therefore, we can update this field configuration to point to taxonomy terms instead of nodes. In the Field Storage
section, change Type of item to reference
to Taxonomy term
. In the Reference type
section select the Sponsor
vocabulary. Save the field configuration and the errors we saw before will be gone.
If you want to practice, fix a similar error in the Session
content type. These errors were the result of a mismatch between the content model in Drupal 7 compared to the content model in Drupal 10. After executing each migration, it is good practice to verify it is producing the expected results. In the case of the upgrade_d7_field
and upgrade_d7_field_instance
migrations, this means reviewing the field attached to all entity/bundle combinations.
Before continuing, let me share one of my favorite administration pages in all of Drupal: the field list report. Go to https://migration-drupal10.ddev.site/admin/reports/fields
and you will be greeted by a lot of errors. At this point, some of these errors should start to feel familiar. Let's ignore the errors for now and have a look at the report itself.
You will see every field attached to every entity type. For each field, there is the field type and the module that provides it. The Used in
column indicates all the bundles of an entity type where a field is used. The Summary
column will include extra information about the field. For example, for entity reference fields, you will see which entity type the field allows to reference. The ideal scenario is that you do not see any blank spaces in the Used in
column. If there is a row without a value in Used in
or if there are commas without text in between like Session, Event, ,
this is a signal that something went wrong with the field instance migrations.
Rolling back migrations
Migrations are an iterative process. It is common to update a migration multiple times until you get the desired results. That process requires rolling back and importing the migration again. Normally, rolling back a migration does not affect content or configuration that was imported by a different migration. But that is not the case with field-related migrations. If we were to rollback the field instance migration, the operation also deletes some of the field storage settings.
Consider the following with regards to rolling back migrations:
- The migration destination plugin determines the action to take when a migration is rolled back. For example, when using the migrating nodes with revisions and translations, the migrate API would not delete a node if there are still revisions or translations in the system. This is done on a per row basis, not for the migration as a whole.
- When rolling back a migration, other Drupal APIs are invoked. In the case of content and configuration migrations, Drupal would call the entity delete operations, where many checks and actions are performed before and after an entity is deleted. For example, the field instance post-delete actions deletes the field storage configuration if the field is not used by other other bundles and it is not marked as persistent.
- Some configuration attached to an entity might prevent it from being deleted. For example, a field storage setting can be marked as persistent meaning it would not be deleted even if it is no longer used. An example of this is the node body field storage, which gets reused when a new content type is added via the user interface.
Getting back to field-related migrations, if you rollback the upgrade_d7_field_instance
migration, we know that at least some field storage settings will also be removed. But, if you were to run ddev drush migrate:status upgrade_d7_field
you would see that it reports that all fields had been migrated and that there are no records to process. This means that field storage settings were deleted from the system, but the Migrate API is not aware of that. This happens because the migration system keeps its own mapping database tables to keep track of the status of each migration. In this case, rolling back the upgrade_d7_field_instance
migration did not affect the mapping tables of the upgrade_d7_field
migration. So, to work around this, we need to rollback both upgrade_d7_field_instance
and upgrade_d7_field
, and import them again. Otherwise, when trying to import field instance settings you will get errors related to missing field storage settings. Trying this out will be left as an exercise to the reader.
Skipping fields in Drupal migrations
The content model changes include dropping a whole content type, changing from one entity to another, and changing from one field type to another. This means that a one-to-one copy of the Drupal 7 field configuration is not possible. To make the transition easier, we are going to prevent some fields from being migrated. Then, we will manually create or update field settings as needed to match the new content model. The Migrate Skip Fields module can be used to prevent migrating fields that are not necessary. Make sure to include the module via Composer and enable it.
ddev composer require 'drupal/migrate_skip_fields:^1.0@alpha'
ddev drush pm:enable migrate_skip_fields
Once enabled, you can skip fields by entity, bundle, name, or type with the following settings:
// Drupal core version of source site. Either '6' or '7'.
$settings['migrate_skip_field_source_version'] = '7';
// Skip by entity type, bundle, and field name. An asterisk serves as a wildcard to skip all values of one component.
$settings['migrate_skip_fields_by_name'] = [
'entity_type:bundle:field_name',
'entity_type:bundle:*',
'entity_type:*:field_name',
'*:*:field_name',
];
// Skip by field type. See hook_field_info. Alternatively, see the content_node_field table in Drupal 6 or the field_config table in Drupal 7.
$settings['migrate_skip_fields_by_type'] = [
'field_type_1',
'field_type_2',
];
The migrate_skip_fields_by_name
setting allows specifying multiple values each using three components (entity type, bundle, and name) separated by a colon (:
) to indicate which fields should be skipped. You can use asterisk (*
) as a wildcard for any of the components. That means that you want to skip fields for all values of that component. For example, node:swag:*
means that you want to skip all fields attached to the swag
bundle (content type) of the node
entity type. For migrate_skip_fields_by_type
, you need to use the machine name of the Drupal 7 field type to exclude. This can be found in the hook_field_info
implementation of the module that provides the field. Alternatively, you can query the field_config
table in the Drupal 7 database to find details on field machine names and their types. For those following the series, article 8 included a Drupal 7 site audit in which we populated a source site template introduced in article 3. You can use the field_instance
tab of that document as a reference of what we need to do in our example project.
The above settings need to be added in settings.php
or in a file included from it. In our example project, we use settings.migrate.php
for migration-related settings. Add the following settings to that file:
// Migrate skip fields settings.
$settings['migrate_skip_fields_source_version'] = '7';
$settings['migrate_skip_fields_by_name'] = [
'node:speaker:*', // Changed to user entity.
'node:sponsor:*', // Changed to taxonomy term entity.
'node:swag:*', // Content type no longer needed.
'*:*:field_image', // Changed to media reference field.
];
$settings['migrate_skip_fields_by_type'] = [
'youtube', // Changed to media reference field.
];
The above translates to skipping all fields attached to the speaker
, sponsor
, and swag
content types; all fields whose machine name is field_image
no matter the entity type or bundle; and all fields of type youtube
. Later in the series, we will use a combination of Drupal recipes and manual updates to bring back some of the fields that were skipped with the updated configuration for Drupal 10.
The Migrate Skip Fields module acts on the import operation of the field-related migrations. Therefore, we need to rollback and import them again.
ddev drush migrate:rollback upgrade_d7_field,upgrade_d7_field_instance
ddev drush cache:rebuild
ddev drush migrate:import upgrade_d7_field,upgrade_d7_field_instance
There should not be any failed records in the import operation report. Use the drush migrate:messages
command or the migration messages admin interface to see helpful log data added by the Migrate Skip Fields module. Then, visit the field list report at https://migration-drupal10.ddev.site/admin/reports/fields
Notice that the number of fields is significantly lower and that there are no error messages at the top of the page. A big win already, but we are not done yet.
Remember manual configuration updates we did to the field_sponsors
field in the Event
content type earlier? Because we rolled back the migration and the field was deleted, we need to do these manual updates again. As mentioned before, after each field migration, you should review the associated entities and bundles to verify the migrated configuration is free of errors and in line with the new content model.
Let's make the following updates:
- In the
Event
content type, edit thefield_sponsors
field. ChangeType of item to reference
toTaxonomy Term
and forVocabulary
selectSponsor
. Then save the field configuration. - In the
Session
content type, edit thefield_speakers
field. ChangeType of item to reference
toUser
and forSort by
selectName
. Then save the field configuration.
With these two manual updates complete, we have finished migrating field storage and instance settings. What a journey! In your own projects, be sure to export the new, fleshed out configuration and commit it to version control. In our next articles, we will walk through migrating field widgets, formatters, and groups.
Image by Valdas Miskinis from Pixabay