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:
- Creating a custom module.
- Preparing migration variables.
- Storing existing data.
- Getting field config storage.
- Getting field config.
- Remove old configurations.
- Insert data into the new fields configs.
- 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
- This walkthrough will try to convert a field from Number (Integer) to Text (Formatted). See documentation for list of field types.
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
andsettings
. The common values in these content types arefield_name
,field_type
andentity_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 fieldfield_number
.
- 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)