Published on

Drupal Field Migration

There are multiple ways of migrating data between different field types and this brief walkthrough will shed a light on one possible way of doing so.

Field is a Drupal core module and it’s part of a list of different ways to represent data in Drupal.

The process will include:

  1. Creating a custom module.
  2. Preparing migration variables.
  3. Storing existing data.
  4. Getting field config storage.
  5. Getting field config.
  6. Remove old configurations.
  7. Insert data into the new fields configs.
  8. Run the migration (aka. database updates).

Prerequisites

  • Fresh or existing Drupal 9.0 project.
  • Drush CLI tool (composer require drush/drush)

Things to keep in mind

Creating a custom module

There are abundant list of documentations when it comes to creating custom modules in Drupal. However, the minimum folder structure for our purpose is (and I am calling this custom module field_migration_medium):

field_migration_medium/
  field_migration_medium.info.yml
  field_migration_medium.install

In the field_migration_medium.install file, we will use hook_update_N function to let Drupal know of an existing database update for this custom module.

/**
* hook_update_N()
*/
function field_migration_medium_update_9001() {}

N donates the update ID and Drupal keeps track of this for each module. See the documentation for more info.

Preparing migration variables

We need to identify the current field type and the new field type. In order to identify these values, we need to look at what does Field Config Storage and Field Config mean.

A bundle in Drupal consists of various field types such as Number, Text, Boolean … etc. Each of these field types has a default configuration setting (Field Storage Configuration) and a specific one (Field Configuration).

For example, I have created field_number for three different content types article, page and medium. All of these content types share the same field_number global configuration, however, each content type has it’s own implementation and default settings of field_number.To illustrate these differences, below are the snippets of each of these objects (Field Storage Configuration and Field Configuration):

  • Global Configuration: Field Storage Config object for field_number.
Drupal\field\Entity\FieldStorageConfig Object
  (
      [id:protected] => node.field_number
      [field_name:protected] => field_number
      [entity_type:protected] => node
      [type:protected] => integer
      [module:protected] => core
  • Specific Configuration: Field Config object for article content type.
Drupal\field\Entity\FieldConfig Object
  (
      [deleted:protected] =>
      [fieldStorage:protected] =>
      [id:protected] => node.article.field_number
      [field_name:protected] => field_number
      [field_type:protected] => integer
      [entity_type:protected] =>node
      [bundle:protected] => article
      [label:protected] => Number
      [description:protected] =>
      [settings:protected] => Array
          (
              [min] =>
              [max] =>
              [prefix] =>
              [suffix] =>
          )
  • Specific Configuration: Field Config object for page content type.
pageDrupal\field\Entity\FieldConfig Object
  (
      [deleted:protected] =>
      [fieldStorage:protected] =>
      [id:protected] => node.page.field_number
      [field_name:protected] => field_number
      [field_type:protected] => integer
      [entity_type:protected] => node
      [bundle:protected] => page
      [label:protected] => Number
      [description:protected] =>
      [settings:protected] => Array
          (
              [min] => 0
              [max] => 11
              [prefix] =>
              [suffix] =>
          )
  • Specific Configuration: Field Config object for medium content type.
mediumDrupal\field\Entity\FieldConfig Object
  (
      [deleted:protected] =>
      [fieldStorage:protected] =>
      [id:protected] => node.medium.field_number
      [field_name:protected] => field_number
      [field_type:protected] => integer
      [entity_type:protected] => node
      [bundle:protected] => medium
      [label:protected] => Number Text
      [description:protected] => This is medium number text.
      [settings:protected] => Array
          (
              [min] =>
              [max] =>
              [prefix] =>
              [suffix] =>
          )

There are subtle changes in each content type, such as id, bundle, label, description and settings. The common values in these content types are field_name, field_type and entity_type.

Keeping that in mind, let’s continue on and define the migration variables.

The migration variables are:

  • Entity type — node in this walkthrough.
  • Machine name of the old_field. The machine name can be found in the /admin/reports/fields . I will call this field field_number .

fields_table_list

  • Table name of the old_field in the database. The table name will consist of the entity name and the old_field machine name node__field_number
  • Revision table of the old_field; if exists. That will also have the entity name and the machine name of the old_field. node_revision__field_number
  • An array to store the new fields config
$entityType = 'node';
$oldFieldName = 'field_number';
$table = $entityType. '__' . $oldFieldName;
$revisionTable = $entityType. '_revision__' . $oldFieldName;
$newFieldsArray = [];

Store data and revision data into arrays

$rows = NULL;
$revisionRows = NULL;
if ($database->schema()->tableExists($table)) {
    $rows = $database->select($table, 'n')->fields('n')->execute()->fetchAll();
    $revisionRows = $database->select($revisionTable, 'n')->fields('n')->execute()->fetchAll();
}

Get Field Storage Configuration

  • Since there is a single field storage config, we need to load the configuration by passing $entityType and $oldFieldName to loadByName($entity_type_id, $field_name) method.
$fieldStorage = FieldStorageConfig::loadByName($entityType, $oldFieldName);
  • Define the new field storage configuration
$newFieldStorage = $fieldStorage->toArray();
$newFieldStorage['type'] = 'text';
$newFieldStorage['settings'] = [
   'max_length' => 255
]

Note: To get a list of the bundles that uses the field storage config, getBundles() method can be called like this $fieldStorage->getBundles() as we will see below.

Get Fields Configuration

  • Get all the bundles that has field_number and iterate through them.
  • Load the name of each of these specific field config loadByName($entity_type_id, $bundle, $field_name)
foreach ($fieldStorage->getBundles() as $bundle => $label) {

    // Load the field configuration
    $field = FieldConfig::loadByName($entityType, $bundle, $oldFieldName);

    // Turn the result into an array
    $newField = $field->toArray();

    // Update the field_type from `number` to `text`
    $newField['field_type'] = 'text'; 

    // Update the settings of this new field -- Optional
    $newField['settings'] = [
        'max_length' => 255
    ];

    // Store the result into an array
    $newFieldsArray[] = $newField;
}

Remove old configurations

  • Delete old field storage configuration
$fieldStorage->delete();
  • Purge all fields value in batches
// N is the batch number
field_purge_batch(N);

Insert data into the new fields configs

  • Create the new field storage configuration
$newFieldStorage = FieldStorageConfig::create($newFieldStorage);
$newFieldStorage->save();
  • Create the new fields configurations
foreach ($newFieldsArray as $field) {
   $fieldConfig = FieldConfig::create($field);
   $fieldConfig->save();
}
  • Insert data back into the new fields
// Row data
if (!is_null($rows)) {
  foreach ($rows as $row) {
   $database->insert($table)->fields((array) $row)->execute();
  }
}
// Revision data
if (!is_null($revision_rows)) {
  foreach ($revision_rows as $row) {
    $database->insert($revisionTable)->fields((array) $row)->execute();
  }
}

Run database update

This update can be done either by using Drush or Drupal CLI commands. Note: Backup your database.

// Drupal CLI
bin/drupal upex
// Drush CLI
bin/drush updb

The Full Code Snippet