Wednesday 28 June 2023

Modify a field config and storage with existing data in Drupal

 As a developer, we have faced such situations where we have to change the type of field in content type. If that field is already having data against the field then this is not possible through Drupal interface. And we cannot delete and create that field again as there will be data loss.

To meet this requirement, the proper way is to write hook_update

In my one of account, I faced such scenario where anyhow initially field was created as type "List(Text)" then we have to change it to "Boolean". We have already created a lot of contents on Production site. We don't want to update the all contents again after recreating this field. So I followed the hook_update to change that field config and storage with existing data.

I have created a custom module named mymodule and write down .install file to add hook_update. Code is as per below.


<?php

/**
 * @file
 * Update function to change field with existing data.
 */

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

/**
 * Recreate field_content_access storage so that "drush cim" can run.
 */
function connect_access_update_9001() {
  $fields = [
    'field_content_access' => [
      'table' => 'node__field_content_access',
      'revision_table' => 'node_revision__field_content_access',
    ],
  ];

  $database = \Drupal::database();

  foreach ($fields as $field_name => $f) {
    // Table name from $fields array.
    $table = $f['table'];
    // Revision table from $field array.
    $revision_table = $f['revision_table'];
    // Entity type like node.
    $entity_type = 'node';

    // Step 1.
// Get field storage.
    $field_storage = FieldStorageConfig::loadByName($entity_type, $field_name);

    // Then check if field is not found.
    if (is_null($field_storage)) {
      continue;
    }

    // Step 2.
// Store existing data in $rows and $revision_rows variables.
    $rows = NULL;
    $revision_rows = NULL;
    if ($database->schema()->tableExists($table)) {
      // The table data will be restored from $rows and $revision_rows variable
// after the update is completed.
      $rows = $database->select($table, 'n')->fields('n')->execute()
        ->fetchAll();
      $revision_rows = $database->select($revision_table, 'n')->fields('n')->execute()
        ->fetchAll();
    }

    // Step 3.
// Save new field configs in new variable and delete existing fields.
    $new_fields = [];
    // Traverse every field to assign those values to new field.
    foreach ($field_storage->getBundles() as $bundle => $label) {
      $field = FieldConfig::loadByName($entity_type, $bundle, $field_name);
      $new_field = $field->toArray();
      $new_field['field_type'] = 'boolean';
      $new_fields[] = $new_field;
      // Delete existing field.
      $field->delete();
    }

    // Step 4.
// Create new storage configs from existing configs.
    $new_field_storage = $field_storage->toArray();
    $new_field_storage['type'] = 'boolean';
    $new_field_storage['module'] = 'core';
    $new_field_storage['settings'] = [];

    // Step 5.
// Purge deleted fields data.
// Required to create new fields.
    field_purge_batch(200);

    // Step 6.
// Create new fieldstorage.
    FieldStorageConfig::create($new_field_storage)->save();

    // Step 7.
// Now create new fields for all bundles.
    foreach ($new_fields as $new_field) {
      $new_field = FieldConfig::create($new_field);
      $new_field->save();
    }

    // Step 8.
// Restore existing data in created fields & revision tables.
    if (!is_null($rows)) {
      foreach ($rows as $row) {
        $row = (array) $row;
        $database->insert($table)->fields($row)->execute();
      }
    }
    if (!is_null($revision_rows)) {
      foreach ($revision_rows as $row) {
        $row = (array) $row;
        $database->insert($revision_table)->fields($row)->execute();
      }
    }
  }
}

Follow below steps. 1. Machine name of the field must be same to new field. 2. On `admin/reports/fields` and search field to be changed. List down all the content types using this field. 3. Check table in the DB related with this field and check revision tables also. List down all the records here. Check with below queries either run by drush or from phpmyadmin. drush sql:query "SELECT * FROM node__field_content_access;" drush sql:query "SELECT * FROM node_revision__field_content_access;" 4. Add the above code in mymodule.install. 5. Replace the $entityType and $fieldName variables according to the entity and field have to change. 6. Take DB backup. 7. Run update either by drush updb or by /update.php on your browser. 8. Check if the update was successfully performed. 9. Check field in all content type. 10. Check data in existing content type.