Series Overview & ToC | Previous Article | Next Article


In this article, we are completing field-related migrations by importing formatter settings. This step builds on our previous work with view modes and field groups, bringing us closer to a functional Drupal 10 site. As we go through the process we will also identify other configuration elements used by field formatters that don’t have an automatic migration.

Remember, if you have been following our series, you have already migrated view modes—a prerequisite for field formatters. The Migrate Skip Fields module will also be used to handle content model changes in the new site.

Before we begin

Familiarity with site building concepts related to fields is assumed. Refer to article 16 for a high level overview. Today's article also touches on view modes and date formats.

Field formatter migrations

A formatter determines how field data is rendered by Drupal. Each field type can be supported by one or more field formatters. For example, an entity reference field can print only the entity ID, its label as plain text or as a link, or the representation of the entity as specified by a view mode. Some field configuration might affect which formatters are available. For example, entity reference fields that point to taxonomy terms can use the RSS category formatter.

Technical note: The FormatterInterface interface includes a isApplicable method that determines if a formatter can be used for a field taking into account its configuration.

We use upgrade_d7_field_formatter_settings to migrate field formatter settings. Copy it from the reference folder into our custom module and rebuild caches for the migration to be detected.

cd drupal10
cp ref_migrations/migrate_plus.migration.upgrade_d7_field_formatter_settings.yml web/modules/custom/tag1_migration/tag1_migration_config/migrations/upgrade_d7_field_formatter_settings.yml
ddev drush cache:rebuild

Note that while copying the file, we also changed its name and placed it in a migrations folder inside our custom module. After copying the file, make the following changes:

  • Remove the following keys: uuid, langcode, status, dependencies, cck_plugin_method, and migration_group. Notice that for field migrations, we preserve the field_plugin_method key.
  • Add two migration tags: component_entity_display and tag1_configuration.
  • Add key: migrate under the source section.
  • Update the required migration_dependencies to include upgrade_d7_field, upgrade_d7_field_instance, upgrade_d7_view_modes, and the migrations that create configuration entities.

In addition to the updates above, remember that the Migrate Skip Fields module is installed on the site. Via hook_migration_plugins_alter, it modifies the field formatter migration to prevent some fields being imported. In particular, those excluded by the migrate_skip_fields_by_name setting as explained in article 17.

For field formatters, the module does not act on the migrate_skip_fields_by_type setting. The original migration already accounts for not importing formatter data for a field whose storage setting was not migrated. This happens in the field_type_exists pipeline of the process section.

We are going to make a small addition here. The last process plugin in this pipeline is skip_on_empty. We will add a message configuration to log when a field is skipped because its storage setting is missing. The logs are added to the migration tables and can be viewed from the command line or the migration messages admin interface.

After the modifications, the upgrade_d7_field_formatter_settings.yml file should look like this:

id: upgrade_d7_field_formatter_settings
class: Drupal\migrate_drupal\Plugin\migrate\FieldMigration
field_plugin_method: alterFieldFormatterMigration
migration_tags:
 - 'Drupal 7'
 - Configuration
 - component_entity_display
 - tag1_configuration
label: 'Field formatter configuration'
source:
 key: migrate
 plugin: d7_field_instance_per_view_mode
 constants:
   third_party_settings: {  }
process:
 # @modified
 field_type_exists:
   -
     plugin: migration_lookup
     migration: upgrade_d7_field
     source:
       - field_name
       - entity_type
   -
     plugin: extract
     index:
       - 0
   -
     plugin: skip_on_empty
     method: row
     message: "Field storage configuration does not exist."
 entity_type:
   -
     plugin: get
     source: entity_type
   -
     plugin: static_map
     map:
       field_collection_item: paragraph
       paragraphs_item: paragraph
     bypass: true
 bundle:
   -
     plugin: migration_lookup
     migration: upgrade_d7_field_instance
     source:
       - entity_type
       - bundle
       - field_name
   -
     plugin: extract
     index:
       - 1
 view_mode:
   -
     plugin: migration_lookup
     migration: upgrade_d7_view_modes
     source:
       - entity_type
       - view_mode
   -
     plugin: extract
     index:
       - 1
   -
     plugin: static_map
     bypass: true
     map:
       full: default
 field_name:
   -
     plugin: get
     source: field_name
 options/label:
   -
     plugin: get
     source: formatter/label
 options/weight:
   -
     plugin: get
     source: formatter/weight
 plugin_id:
   -
     plugin: process_field
     source: type
     method: getPluginId
 formatter_type:
   -
     plugin: process_field
     source: type
     method: getFieldFormatterType
 options/type:
   -
     plugin: static_map
     bypass: true
     source:
       - '@plugin_id'
       - '@formatter_type'
     map:
       taxonomy_term_reference:
         taxonomy_term_reference_link: entity_reference_label
         taxonomy_term_reference_plain: entity_reference_label
         taxonomy_term_reference_rss_category: entity_reference_label
         i18n_taxonomy_term_reference_link: entity_reference_label
         i18n_taxonomy_term_reference_plain: entity_reference_label
         entityreference_entity_view: entity_reference_entity_view
       email:
         email_formatter_default: email_mailto
         email_formatter_contact: basic_string
         email_formatter_plain: basic_string
         email_formatter_spamspan: basic_string
         email_default: email_mailto
         email_contact: basic_string
         email_plain: basic_string
         email_spamspan: basic_string
       field_url:
         url_default: link
         url_plain: link
       field_collection:
         field_collection_view: entity_reference_revisions_entity_view
       addressfield:
         addressfield_default: address_default
       telephone:
         text_plain: string
         telephone_link: telephone_link
       entityreference:
         entityreference_label: entity_reference_label
         entityreference_entity_id: entity_reference_entity_id
         entityreference_entity_view: entity_reference_entity_view
       file:
         default: file_default
         url_plain: file_url_plain
         path_plain: file_url_plain
         image_plain: image
         image_nodelink: image
         image_imagelink: image
       datetime:
         date_default: datetime_default
         format_interval: datetime_time_ago
         date_plain: datetime_plain
   -
     plugin: d7_field_type_defaults
   -
     plugin: skip_on_empty
     method: row
 hidden:
   -
     plugin: static_map
     source: '@options/type'
     map:
       hidden: true
     default_value: false
 options/settings:
   -
     plugin: default_value
     source: formatter/settings
     default_value: {  }
 options/third_party_settings:
   -
     plugin: get
     source: constants/third_party_settings
 options/settings/view_mode:
   field_collection:
     plugin: paragraphs_process_on_value
     source_value: type
     expected_value: field_collection
     process:
       plugin: get
       source: formatter/settings/view_mode
destination:
 plugin: component_entity_display
migration_dependencies:
 required:
   - upgrade_d7_field
   - upgrade_d7_field_collection_type
   - upgrade_d7_field_instance
   - upgrade_d7_node_type
   - upgrade_d7_taxonomy_vocabulary
   - upgrade_d7_view_modes
 optional: { }

Now, rebuild caches for our changes to be detected and execute the migration. Run migrate:status to make sure we can connect to Drupal 7. Then, run migrate:import to perform the import operations.

ddev drush cache:rebuild
ddev drush migrate:status upgrade_d7_field_formatter_settings
ddev drush migrate:import upgrade_d7_field_formatter_settings

No errors after running the migration. Great! Feel free to have a look at the logs using the drush migrate:messages command.

Similar to other field-related migrations, we should confirm that the result of migrating field formatter settings is correct. There are two places you should look at. Using the Event content type as an example, go to:

  1. The manage display page where you can make adjustments to the migrated field formatter configuration.
  2. The view page for a session node where you can see the current state of the field formatter configuration. At the moment, we have not migrated nodes nor other content entities. Feel free to create some content to see how it is rendered.

This is where having familiarity with site building concepts and past experience will help. Below is a screenshot of an Event node comparing how some of the fields attached to the content type are rendered.

Manage Display page

Notice that the Date field in Drupal 10 shows the time even though the field type is supposed to store a date value only. This is because the Default long date format used to render the field is configured to render a time component. In Drupal 7, the Date field is configured to use Long date format, which can also render a time component; however, it is hidden when the date field is configured to store a date value only. This highlights how formatters can behave differently between Drupal versions and the importance of carefully reviewing the migrated configuration.

For the example above, we could hide the time component for the Date field in multiple ways. One option is to create a new date format which does not print the time. Another option is the Custom date formatter for the date field and use a PHP datetime pattern that does not print the time like l, F j, Y. The first option is recommended if you plan to reuse the same date format in multiple places. The second option can be used if you are certain that there is no need to reuse the pattern.

For the purpose of this example, we are going to go with a third option: use a different date format. Because we are using the Olivero theme, we can use the Olivero Medium date format which will hide the time and the day of the week. An acceptable compromise for the example.

Technical note: Drupal 10 does not provide an automated upgrade path for custom date types and formats from Drupal 7. They are stored in the date_format_type and date_formats tables respectively. You can write a custom migration to read from those Drupal 7 tables and create DateFormat configuration entities in Drupal 10. Or you can recreate them manually on the new site. In either case, you will have to identify where the custom date formats were used in Drupal 7 and manually update the corresponding configuration in Drupal 10. At the time of this writing, this issue contains a patch for migrating custom date formats, but no date types.

Below is a screenshot of an Session node comparing how some of the fields attached to the content type are rendered.

Session Node

Notice that for the Topics field, the label is rendered in bold in Drupal 10, but not in Drupal 7. As for the terms themselves, in Drupal 10 they appear stacked on top of each other and, in Drupal 7, they are listed next to one another. For the Slides field, Drupal 10 shows the file size in parenthesis and not the file icon. In Drupal 7, you see an icon that varies per file type and there is no reference to the file size. These differences are a combination of changes to how formatters are implemented in different versions of Drupal and how themes decide to render some user interface elements.

For our example project, we are not going to take any action here. In real life projects, you are likely going to use a custom theme that will control how interface elements are rendered. Contributed modules like Fences can also manipulate the output of fields. As a rule of thumb, before adding any module to your project, evaluate if the functionality provided outweighs the extra configuration and maintenance required to use it.

Below is a screenshot of a Venue node rendered in Drupal 10.

Venue Node

The Venue content type has a Phone field that is rendered as plain text. This matches the configuration from Drupal 7, but that does not mean that we cannot make improvements as part of the migration. In this case, we can update the field to use the Telephone link formatter to render a link using the tel URL scheme. Users visiting the site with a mobile phone will be able to place the call by clicking on the link. Drupal is producing semantic HTML markup and it is up to the visitor's browser to determine what action to take when the link is clicked. Not obvious from the screenshot above, but the Address field shows the country in Drupal 10. The field formatter in Drupal 7 hides it when the field only allows a single country to be selected. No action to take as part of the example project.

While reviewing the field formatter migration, we identified that date formats are not migrated automatically. Migrations can be complex, and not every Drupal 7 setting has an automated upgrade path. Make sure to thoroughly review the migrated configuration and perform manual updates as needed. To wrap up the migration of fields, remember that storage, instance, widget, and formatter settings are closely interconnected.

Coming up in our step-by-step migration series, we will complete configuration migrations by walking through: media types and image styles, roles and permissions, and text and input formats. Once the remaining configuration is in place, we will be ready to work with content.


Image by Mohamed Hassan from Pixabay