Ride into the Danger Zone: How to Update Drupal 8 Field Settings without Losing any Data

Man coding form fields on laptop

As some of its biggest strengths as a first-in-class Content Management System, Drupal 8’s Entity API and Field API provide for both the creation and maintenance of powerful, structured data architectures with relative ease. This is important as both new and existing Drupal sites require careful planning of how their content is structured, usually in the form of setting up content types and fields.

However, despite clients’ and developers’ best intentions, the data requirements of any given project can often change at any time. Such is the reality of web development. Sometimes, a large portion of a site’s data architecture may need to be reconsidered via a design change. Other times, a client might simply need to modify the available options in a checkbox or radio button field.

This type of scope creep, large or small, can be manageable in specific circumstances. For example, if a brand new site is still unpopulated with actual data/content, we can update entity fields and their settings without much regard to anything. But what about when you’ve already launched your site? Or, even worse, if the site has already been live for weeks, months, or years? Presumably, you have data already populated in every possible field on your production site.

At that point, even the simple case of modifying the available options in a checkbox/radio button field can become an unexpectedly difficult task. For example, if you try to alter the storage settings of a field that already has corresponding data in the database, Drupal will alert you that this is not allowed via the UI. In our case, options on a checkbox/radio button field are considered field storage. So what can we do when this happens?

You might be tempted to avoid the hassle and create an entirely new field that duplicates the functionality of the old one. However, that would then require you to plan a migration of all of the existing data from the old field into the new one. Or, even worse, you could decide to maintain both fields as a way to “archive” the old data, in which case you add another layer of confusing technical debt to your site and now you have to find a way to prevent users from entering data in the old field.

Instead, one potentially straightforward way for us to update our field storage settings is via Drupal’s Update API.

Let’s imagine we have a radio button field that originally had three available options (“red”, “blue”, “green”), but now our client wants the field to have only two options (“red”, “blue”). If we try to update the options via the Drupal admin UI, Drupal will give us a not-so-helpful error message simply telling us that we’re not allowed to do that.

Error: Field Settings Can No Longer Be Changed

Instead, we’ll need to accomplish our field settings update via a custom update hook using Drupal’s Update API. Our update will involve two steps:

  1. Updating any existing field data that uses the old option (“green”) that we are now removing, and replacing it with a valid option (e.g., “blue”).
  2. Updating the field storage configuration itself to now only allow our two options (“red”, “blue”) going forward whenever our nodes are created or edited.

In case you aren’t familiar, update hooks use the following function skeleton:

/**
 * Write a line or two here about what the updates are for.
 * This is shown to users on the update.php page.
 */
function mymodule_update_8001(&$sandbox) {
}

These update hooks are placed inside of your module’s .install file (e.g., mymodule.install for a module named mymodule).

For our example, our update hook would look something like this:

/**
 * Updates allowed values for field_custom_radio_buttons.
 */
function mymodule_update_8001(&$sandbox) {
  // Queries for relevant nodes already using that field.
  $nids = \Drupal::entityQuery('node')
    ->condition('type', 'my_custom_content_type')
    ->condition('field_custom_radio_buttons', 'green')
    ->execute();
  // Loads queried nodes.
  // NOTE: If you have a non-trivial number of nodes to modify,
  // you should use the Batch API to drive your update function instead.
  // @see https://api.drupal.org/api/examples/batch_example%21batch_example.install/function/batch_example_update_8001/8.x-1.x
  $nodes = Node::loadMultiple($nids);
  // Updates field_custom_radio_buttons values.
  // Replaces any “green” values with “blue”.
  $updated_value_map = [
    'green' => 'blue',
  ];
  /** @var \Drupal\node\Entity\Node $node */
  foreach ($nodes as $node) {
    $node
      ->set('field_custom_radio_buttons', $updated_value_map[$node->field_custom_radio_buttons->value])
      ->save();
  }

  // Updates field storage config for field_custom_radio_buttons.
  // @see https://www.drupal.org/node/2012896
  // @see https://www.drupal.org/docs/8/api/update-api/updating-entities-and-fields-in-drupal-8#example-updating-a-field-from-an-obsolete-type-to-a-new-type
  $old_config = FieldStorageConfig::loadByName('node', 'field_custom_radio_buttons');
  $new_config = $old_config->createDuplicate();
  $new_config->original = $new_config;
  $new_config->enforceIsNew(FALSE);
  $new_config
    ->setSetting('allowed_values', [
      'red' => 'red',
      'blue' => 'blue',
    ])
    ->save();
}

Once we have our update hook in place, we can run it via the browser at /update.php or via the command line using the Drush command drush updatedb.

Assuming the update hook was run successfully, we can then update the corresponding YAML file in our config sync folder, to make sure that our hard-coded configuration matches the newest active configuration. (NOTE: Manual updates to YML files are not a best practice in most cases and can be risky if done improperly. Most config changes should be performed directly through Drupal’s UI whenever possible.)

In our example, that file would have a name like field.storage.node.field_custom_radio_buttons.yml. Within that file, we should find the following sequence:

settings:
  allowed_values:
    -
      value: 'red'
      label: 'red'
    -
      value: 'blue'
      label: 'blue'
    -
      value: 'green'
      label: 'green'

To make sure our hard-coded configuration matches our active configuration, we would update that sequence by simply removing the outdated value:

settings:
  allowed_values:
    -
      value: 'red'
      label: 'red'
    -
      value: 'blue'
      label: 'blue'

With that, our field settings should now be updated, without running into any error messages from Drupal!