Writing a BaseFieldDefinition hook_update_N

Before I wrote this code the “documentation” on this was in one of the two treasure troves (the other being the actual Drupal code itself) of the change records , specifically Write update functions for entity schema updates, automation removed .

As often happens the change record didn’t quite cover what I wanted to do and it specifically didn’t mention a couple of mind bending gotchas.

Link to this section Trust the process

If you need to update the type of a custom BaseFieldDefinition the general process is:

  1. Store the existing values for the field
  2. Clear out the values from the field (required to uninstall a field)
  3. Uninstall the field
  4. Create a new BaseFieldDefinition
  5. Install the new definition
  6. Restore the values from step 1

For example, changing the type from boolean to string for which the code is:

$entity_type_manager = \Drupal::entityTypeManager();
$bundle_of = 'node';

$storage = $ntity_type_manager->getStorage($bundle_of);
$bundle_definition = $entity_type_manager->getDefinition($bundle_of);
// Sometimes the primary key isn't 'id'. e.g. 'eid' or 'item_id'.
$id_key = $bundle_definition->getKey('id');
// If there is no data table defined then use the base table.
$table_name = $storage->getDataTable() ?? $storage->getBaseTable();
$database = \Drupal::database();
$definition_manager = \Drupal::entityDefinitionUpdateManager();

// Store the existing values.
$status_values = $database->select($table_name)
  ->fields($table_name, [$id_key, 'status_field'])
  ->execute()
  ->fetchAllKeyed();

// Clear out the values.
$database->update($table_name)
  ->fields(['status_field' => NULL])
  ->execute();

// Uninstall the field.
$field_storage_definition = $definition_manager->getFieldStorageDefinition('status_field', $bundle_of);
$definition_manager->uninstallFieldStorageDefinition($field_storage_definition);

// Create a new field definition.
$new_status_field = BaseFieldDefinition::create('string')
  ->setLabel(t('Status field'))
  ->setDescription(t('The status - either no, yes or skip.'))
  ->setDefaultValue('no')
  ->setRevisionable(FALSE)
  ->setTranslatable(FALSE);

// Install the new definition.
$definition_manager->installFieldStorageDefinition('status_field', $bundle_of, $bundle_of, $new_status_field);

// Restore the values.
$value_map = [
  '1' => 'yes',
  '0' => 'no',
];
foreach ($status_values as $id => $value) {
  $database->update($table_name)
    ->fields(['status_field' => $value_map[$value]])
    ->condition($id_key, $id)
    ->execute();
  }
}

Phew! I truly pity you if you have to do this for multiple defs and types.

Link to this section Gotchas

So how about dem gotchas:

  • The primary key id for an entity probably differs in name
  • The name of the table that stores your BaseFieldDefinition probably differs in name pattern
  • You can’t uninstall a definition until the field is cleared out

Bon chance, Drupalistas!