DEV Community

Cover image for Drupal — Field Type Migration
Mustafa Abdalle
Mustafa Abdalle

Posted on

Drupal — Field Type 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

1- 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.

<?php
/**
* 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.

2- 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.

Field List Table

  • 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 = [];

3- 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();
}

4- 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'] = array(
   '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.

5- 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) {
    $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'] = array(
           'max_length' => 255
    );
    // Store the result into an array
    $newFieldsArray[] = $newField;
}

6- Remove old configurations

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

7- 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();
  }
}

8- 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

<?php

use \Drupal\field\Entity\FieldConfig;
use \Drupal\field\Entity\FieldStorageConfig;

/**
 * Change field_number from Number (integer) to Text (formatted)
 */
function field_migration_medium_update_9001() {

  $entityType = 'node';
  $oldFieldName = 'field_number';
  $table = $entityType . '__' . $oldFieldName;
  $revisionTable = $entityType . '_revision__' . $oldFieldName;
  $newFieldsArray = [];

  $database = \Drupal::database();

  $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 config.
  $fieldStorage = FieldStorageConfig::loadByName($entityType, $oldFieldName);

  // Check if field storage config exist.
  if (is_null($fieldStorage)) {
    return t('Field Storage does not exist');
  }

  $newFieldStorage = $fieldStorage->toArray();
  $newFieldStorage['type'] = 'text';
  $newFieldStorage['settings'] = array(
    'max_length' => 255
  );

  foreach ($fieldStorage->getBundles() as $bundle => $label) {
    $field = FieldConfig::loadByName($entityType, $bundle, $oldFieldName);

    $newField = $field->toArray();
    $newField['field_type'] = 'text';
    $newField['settings'] = array(
      'max_length' => 255
    );
    $newFieldsArray[] = $newField;
  }

  $fieldStorage->delete();
  field_purge_batch(40);

  // Create new field storage.
  $newFieldStorage = FieldStorageConfig::create($newFieldStorage);
  $newFieldStorage->save();

  // Create new fields.
  foreach ($newFieldsArray as $field) {
    $fieldConfig = FieldConfig::create($field);
    $fieldConfig->save();
  }

  // Restore existing data in new fields.
  if (!is_null($rows)) {
    foreach ($rows as $row) {
    $database->insert($table)
             ->fields((array) $row)
             ->execute();
    }
  }

  if (!is_null($revisionRows)) {
    foreach ($revisionRows as $row) {
      $database->insert($revisionTable)
               ->fields((array) $row)
               ->execute();
     }
  }
}

Final Thoughts
When the database update runs, Drupal will keep track of the Update ID for each module. This Update ID can be set to specific version by using Drush. Use it carefully:

drush ev "drupal_set_installed_schema_version(module, version)"

Happy coding :)

Top comments (0)