Change field type of a field with existing data.

This came up today and did a little bit of digging and testing to find a solution. The bulk of this came from here: https://blog.42mate.com/change-field-type-with-existing-data-on-drupal-8/ but it missed handling revisions data.

A few steps to prepare:

# Backup your dB.

Did I really need to state this?

# Clean up deleted field tables.

Make sure deleted field tables are cleaned up. Google how to do this and note you can't just go in and delete these tables. When I was prepping for this I ran the following (multiple times):

drush eval "field_purge_batch(500)"

drush cron

After running this a few times; run this to check if safe to manually delete any remaining tables:

drush ev "var_dump(\Drupal::state()->get('field.storage.deleted'))"

ensure this is empty and then drop any "deleted field" tables that remain.

# Export your configuration

Its a good idea to export the configuration for the entity types and varoius configurations associated with these (display, form, etc) that go with the field you are about to modify. The reason for this is that when this is run and replaced; the fields will be disabled. I found the easiest way to put it back was to simply import the configuration which had the placement of your field.

drush cex

When you import these configuration files later, it will complain that it can't import because the field type is incorrect. Simply edit the YML file and set it to your new type.

# Run the script

This code would typically be set up as an .install update hook. For now I have it as a procedural function. You can then run this from drush or a devel php window.

<?php

/**
* @file
*/

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

/**
*
*/
function change_field_type() {
$entityType = 'node';
$fieldName = 'field_page_description';
$new_type = 'text_long';

$database = \Drupal::database();
$table = $entityType . '__' . $fieldName;
$rev_table = $entityType . '_revision__' . $fieldName;
$currentRows = NULL;
$newFieldsList = [];
$fieldStorage = FieldStorageConfig::loadByName($entityType, $fieldName);

if (is_null($fieldStorage)) {
return;
}

// Get all current data from DB.
if ($database->schema()->tableExists($table)) {
// The table data to restore after the update is completed.
$currentRows = $database->select($table, 'n')
->fields('n')
->execute()
->fetchAll();
}

// Add for revisions table as well.
if ($database->schema()->tableExists($rev_table)) {
// The table data to restore after the update is completed.
$currentRevRows = $database->select($rev_table, 'n')
->fields('n')
->execute()
->fetchAll();
}

// Use existing field config for new field.
foreach ($fieldStorage->getBundles() as $bundle => $label) {
$field = FieldConfig::loadByName($entityType, $bundle, $fieldName);
$newField = $field->toArray();
$newField['field_type'] = $new_type;
$newField['settings'] = [];
$newFieldsList[] = $newField;
}

// Deleting field storage which will also delete bundles(fields).
$newFieldStorage = $fieldStorage->toArray();
$newFieldStorage['type'] = $new_type;
$newFieldStorage['settings'] = [];

$fieldStorage->delete();

// Purge field data now to allow new field and field_storage with same name
// to be created.
field_purge_batch(50);

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

// Create new fields.
foreach ($newFieldsList as $nfield) {
$nfieldConfig = FieldConfig::create($nfield);
$nfieldConfig->save();
}

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

// And restore existing data in new Revisions table.
if (!is_null($currentRevRows)) {
foreach ($currentRevRows as $row) {
$database->insert($rev_table)
->fields((array) $row)
->execute();
}
}
}

and then:

drush eval "change_field_type();"

# Import configuration

Do this to enable field and place in correct position. You could also do this manually but if you have a lot of view modes; this could be a pain.

I tested this to convert a formatted text field (255 character limit) to a formatted long text field and it seemed to work well. Ideally there needs to be a way to save/restore the placement of the field as this is limiting this solution from being completely containable within an update hook.

Thanks to Luis Gerzenstein for his post above that puts most of this together.

 

 

 

Drupal Version: