Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Drupal 8 Field type schema column change helpers.

TODO:

Current dirty workaround:

/**
 * Helper function for HOOK_Update to update the field schema columns.
 *
 * Based on address.install (thanks to the maintainer!)
 *
 * @param $field_type The field type id.
 * @param array $columns_to_add array of the column names from schema() function.
 */
function _field_type_schema_column_add_helper($field_type, array $columns_to_add = array()) {
  $processed_fields = [];
  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
  $field_definition = $field_type_manager->getDefinition($field_type);
  $field_item_class = $field_definition['class'];

  $schema = \Drupal::database()->schema();
  $entity_type_manager = \Drupal::entityTypeManager();
  $entity_field_manager = \Drupal::service('entity_field.manager');
  $entity_field_map = $entity_field_manager->getFieldMapByFieldType($field_type);
  // The key-value collection for tracking installed storage schema.
  $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql');
  $entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed');

  foreach ($entity_field_map as $entity_type_id => $field_map) {
    $entity_storage = $entity_type_manager->getStorage($entity_type_id);

    // Only SQL storage based entities are supported / throw known exception.
    //    if (!($entity_storage instanceof SqlContentEntityStorage)) {
    //      continue;
    //    }

    $entity_type = $entity_type_manager->getDefinition($entity_type_id);
    $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
    /** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
    $table_mapping = $entity_storage->getTableMapping($field_storage_definitions);
    // Only need field storage definitions of address fields.
    /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */
    foreach (array_intersect_key($field_storage_definitions, $field_map) as $field_storage_definition) {
      $field_name = $field_storage_definition->getName();
      try {
        $table = $table_mapping->getFieldTableName($field_name);
      } catch (SqlContentEntityStorageException $e) {
        // Custom storage? Broken site? No matter what, if there is no table
        // or column, there's little we can do.
        continue;
      }
      // See if the field has a revision table.
      $revision_table = NULL;
      if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) {
        if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
          $revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
        }
        elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
          $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
        }
      }
      // Load the installed field schema so that it can be updated.
      $schema_key = "$entity_type_id.field_schema_data.$field_name";
      $field_schema_data = $entity_storage_schema_sql->get($schema_key);

      $processed_fields[] = [$entity_type_id, $field_name];
      // Loop over each new column and add it as a schema column change.
      foreach ($columns_to_add as $column_id) {
        $column = $table_mapping->getFieldColumnName($field_storage_definition, $column_id);
        // Add `initial_from_field` to the new spec, as this will copy over
        // the entire data.
        $field_schema = $field_item_class::schema($field_storage_definition);
        $spec = $field_schema['columns'][$column_id];

        // Add the new column.
        $schema->addField($table, $column, $spec);
        if ($revision_table) {
          $schema->addField($revision_table, $column, $spec);
        }

        // Add the new column to the installed field schema.
        if ($field_schema_data) {
          $field_schema_data[$table]['fields'][$column] = $field_schema['columns'][$column_id];
          $field_schema_data[$table]['fields'][$column]['not null'] = FALSE;
          if ($revision_table) {
            $field_schema_data[$revision_table]['fields'][$column] = $field_schema['columns'][$column_id];
            $field_schema_data[$revision_table]['fields'][$column]['not null'] = FALSE;
          }
        }
      }

      // Save changes to the installed field schema.
      if ($field_schema_data) {
        $recipient_column = $table_mapping->getFieldColumnName($field_storage_definition, 'recipient');
        unset($field_schema_data[$table]['fields'][$recipient_column]);
        if ($revision_table) {
          unset($field_schema_data[$revision_table]['fields'][$recipient_column]);
        }
        $entity_storage_schema_sql->set($schema_key, $field_schema_data);
      }
      if ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
        $key = "$entity_type_id.field_storage_definitions";
        if ($definitions = $entity_definitions_installed->get($key)) {
          $definitions[$field_name] = $field_storage_definition;
          $entity_definitions_installed->set($key, $definitions);
        }
      }
    }
  }
}
/**
 * Helper function for HOOK_Update to update the field schema columns.
 * List the columns with changed schema values and the helper will handle
 * the content migration and entity update for you.
 *
 * @param $field_type The field type id e.g. "drowl_paragraphs_settings"
 * @param array $columns_to_add array of the column names from schema() function, e.g. ["style_textalign"]
 */
function _field_type_schema_column_spec_change_helper($field_type, $column) {
  $processed_fields = [];
  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
  $field_definition = $field_type_manager->getDefinition($field_type);
  $field_item_class = $field_definition['class'];

  $schema = \Drupal::database()->schema();
  $entity_type_manager = \Drupal::entityTypeManager();
  $entity_field_manager = \Drupal::service('entity_field.manager');
  $entity_field_map = $entity_field_manager->getFieldMapByFieldType($field_type);
  // The key-value collection for tracking installed storage schema.
  $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql');
  $entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed');

  foreach ($entity_field_map as $entity_type_id => $field_map) {
    $entity_storage = $entity_type_manager->getStorage($entity_type_id);
    $entity_type = $entity_type_manager->getDefinition($entity_type_id);
    $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
    /** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
    $table_mapping = $entity_storage->getTableMapping($field_storage_definitions);
    // Only need field storage definitions of address fields.
    /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */
    foreach (array_intersect_key($field_storage_definitions, $field_map) as $field_storage_definition) {
      $field_name = $field_storage_definition->getName();
      $tables = [];
      try {
        $table = $table_mapping->getFieldTableName($field_name);
        $tables[] = $table;
      } catch (SqlContentEntityStorageException $e) {
        // Custom storage? Broken site? No matter what, if there is no table
        // or column, there's little we can do.
        continue;
      }
      // See if the field has a revision table.
      $revision_table = NULL;
      if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) {
        if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
          $revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
          $tables[] = $revision_table;
        }
        elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
          $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
          $tables[] = $revision_table;
        }
      }

      $database = \Drupal::database();
      $existing_data = [];
      foreach ($tables as $table) {
        // Get the old data.
        $existing_data[$table] = $database->select($table)
          ->fields($table)
          ->execute()
          ->fetchAll(PDO::FETCH_ASSOC);

        // Wipe it.
        $database->truncate($table)->execute();
      }

      $manager = \Drupal::entityDefinitionUpdateManager();
      $manager->updateFieldStorageDefinition($manager->getFieldStorageDefinition($field_name, $entity_type_id));


      // Restore the data.
      foreach ($tables as $table) {
        $insert_query = $database
          ->insert($table)
          ->fields(array_keys(end($existing_data[$table])));
        foreach ($existing_data[$table] as $row) {
          $insert_query->values(array_values($row));
        }
        $insert_query->execute();
      }
    }
  }
}
/**
 * Helper function for HOOK_Update to remove columns from the field schema.
 *
 * @param $field_type The field type id e.g. "drowl_paragraphs_settings"
 * @param array $columns_to_remove array of the column names from schema() function, e.g. ["style_textalign"]
 */
function _field_type_schema_column_remove_helper($field_type, array $columns_to_remove = array()) {
  $processed_fields = [];
  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
  $field_definition = $field_type_manager->getDefinition($field_type);
  $field_item_class = $field_definition['class'];

  $schema = \Drupal::database()->schema();
  $entity_type_manager = \Drupal::entityTypeManager();
  $entity_field_manager = \Drupal::service('entity_field.manager');
  $entity_field_map = $entity_field_manager->getFieldMapByFieldType($field_type);
  // The key-value collection for tracking installed storage schema.
  $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql');
  $entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed');

  foreach ($entity_field_map as $entity_type_id => $field_map) {
    $entity_storage = $entity_type_manager->getStorage($entity_type_id);

    // Only SQL storage based entities are supported / throw known exception.
    //    if (!($entity_storage instanceof SqlContentEntityStorage)) {
    //      continue;
    //    }

    $entity_type = $entity_type_manager->getDefinition($entity_type_id);
    $field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
    /** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
    $table_mapping = $entity_storage->getTableMapping($field_storage_definitions);
    // Only need field storage definitions of address fields.
    /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $field_storage_definition */
    foreach (array_intersect_key($field_storage_definitions, $field_map) as $field_storage_definition) {
      $field_name = $field_storage_definition->getName();
      try {
        $table = $table_mapping->getFieldTableName($field_name);
      } catch (SqlContentEntityStorageException $e) {
        // Custom storage? Broken site? No matter what, if there is no table
        // or column, there's little we can do.
        continue;
      }
      // See if the field has a revision table.
      $revision_table = NULL;
      if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) {
        if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
          $revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
        }
        elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
          $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
        }
      }
      // Load the installed field schema so that it can be updated.
      $schema_key = "$entity_type_id.field_schema_data.$field_name";
      $field_schema_data = $entity_storage_schema_sql->get($schema_key);

      $processed_fields[] = [$entity_type_id, $field_name];
      // Loop over each new column and add it as a schema column change.
      foreach ($columns_to_remove as $column_id) {
        $column = $table_mapping->getFieldColumnName($field_storage_definition, $column_id);
        // Add `initial_from_field` to the new spec, as this will copy over
        // the entire data.
        $field_schema = $field_item_class::schema($field_storage_definition);
        $spec = $field_schema['columns'][$column_id];

        // Add the new column.
        $schema->dropField($table, $column);
        if ($revision_table) {
          $schema->dropField($revision_table, $column);
        }

        // Remove the column from the installed field schema.
        if ($field_schema_data) {
          unset($field_schema_data[$table]['fields'][$column]);
          $field_schema_data[$table]['fields'][$column]['not null'] = FALSE;
          if ($revision_table) {
            unset($field_schema_data[$revision_table]['fields'][$column]);
          }
        }
      }

      // Save changes to the installed field schema.
      if ($field_schema_data) {
        $recipient_column = $table_mapping->getFieldColumnName($field_storage_definition, 'recipient');
        unset($field_schema_data[$table]['fields'][$recipient_column]);
        if ($revision_table) {
          unset($field_schema_data[$revision_table]['fields'][$recipient_column]);
        }
        $entity_storage_schema_sql->set($schema_key, $field_schema_data);
      }
      if ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
        $key = "$entity_type_id.field_storage_definitions";
        if ($definitions = $entity_definitions_installed->get($key)) {
          $definitions[$field_name] = $field_storage_definition;
          $entity_definitions_installed->set($key, $definitions);
        }
      }
    }
  }
}
@robbin2012

This comment has been minimized.

Copy link

commented Apr 24, 2018

I pasted my worked code, pease reference for others.
DON'T use field_definition_add_helper and change schema->dropField to drop a field, because it works.
`
/**

  • Add a new column for fieldType
  • @param string $field_type
  • @param sring $new_property
    */
    function field_definition_add_helper($field_type, $new_property) {

$manager = \Drupal::entityDefinitionUpdateManager();
$field_map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType($field_type);

foreach ($field_map as $entity_type_id => $fields) {

foreach (array_keys($fields) as $field_name) {
  $field_storage_definition = $manager->getFieldStorageDefinition($field_name, $entity_type_id);
  $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
  
  if ($storage instanceof \Drupal\Core\Entity\Sql\SqlContentEntityStorage) {
    $table_mapping = $storage->getTableMapping([
      $field_name => $field_storage_definition,
    ]);
    $table_names = $table_mapping->getDedicatedTableNames();
    $columns = $table_mapping->getColumnNames($field_name);
    
    foreach ($table_names as $table_name) {
      $field_schema = $field_storage_definition->getSchema();
      $schema = \Drupal::database()->schema();
      $field_exists = $schema->fieldExists($table_name, $columns[$new_property]);
      $table_exists = $schema->tableExists($table_name);
      
      if (!$field_exists && $table_exists) {
        $schema->addField($table_name, $columns[$new_property], $field_schema['columns'][$new_property]);
      }
    }
  }
  $manager->updateFieldStorageDefinition($field_storage_definition);
}

}

}

/**

  • Remove a column of field_type
  • @param string $field_type FieldTypeId in your definition
  • @param string $property column name
    */
    function field_definition_delete_helper($field_type, $property) {
    $field_map = \Drupal::service('entity_field.manager')->getFieldMapByFieldType($field_type);
    foreach ($field_map as $entity_type_id => $fields) {
    foreach (array_keys($fields) as $field_name) {
    $storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
    _field_property_definition_delete($entity_type_id, $field_name, $property);
    }
    }

}

/**

  • Inner function, called by field_definition_delete_helper
  • @param string $entity_type_id
  • @param string $field_name
  • @param string $property
    */
    function _field_property_definition_delete($entity_type_id, $field_name, $property) {
    $entity_type_manager = \Drupal::entityTypeManager();
    $entity_update_manager = \Drupal::entityDefinitionUpdateManager();
    $entity_storage_schema_sql = \Drupal::keyValue('entity.storage_schema.sql');
    $entity_definitions_installed = \Drupal::keyValue('entity.definitions.installed');

$entity_type = $entity_type_manager->getDefinition($entity_type_id);
//$field_storage_definitions = $entity_field_manager->getFieldStorageDefinitions($entity_type_id);
$field_storage_definition = $entity_update_manager->getFieldStorageDefinition($field_name, $entity_type_id);
$entity_storage = \Drupal::entityTypeManager()->getStorage($entity_type_id);
/** @var Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
$table_mapping = $entity_storage->getTableMapping([
$field_name => $field_storage_definition,
]);

// Load the installed field schema so that it can be updated.
$schema_key = "$entity_type_id.field_schema_data.$field_name";
$field_schema_data = $entity_storage_schema_sql->get($schema_key);

//get table name and revision table name, getFieldTableName NOT WORK, so use getDedicatedDataTableName
$table = $table_mapping->getDedicatedDataTableName($field_storage_definition);
//try/catch
$revision_table = NULL;
if ($entity_type->isRevisionable() && $field_storage_definition->isRevisionable()) {
if ($table_mapping->requiresDedicatedTableStorage($field_storage_definition)) {
$revision_table = $table_mapping->getDedicatedRevisionTableName($field_storage_definition);
}
elseif ($table_mapping->allowsSharedTableStorage($field_storage_definition)) {
$revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable();
}
}

// Save changes to the installed field schema.
if ($field_schema_data) {
$_column = $table_mapping->getFieldColumnName($field_storage_definition, $property);
//Update schema definition in database
unset($field_schema_data[$table]['fields'][$_column]);
if ($revision_table) {
unset($field_schema_data[$revision_table]['fields'][$_column]);
}
$entity_storage_schema_sql->set($schema_key, $field_schema_data);
//Try to drop field data
\Drupal::database()->schema()->dropField($table, $_column);
}
}
`

@dpi

This comment has been minimized.

Copy link

commented Dec 14, 2018

Another field type schema upgrade example can be found in date_recur https://www.drupal.org/project/date_recur/issues/3018101

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.