View source
<?php
namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\DatabaseException;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldException;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\FieldStorageConfigInterface;
class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorageSchemaInterface {
use DependencySerializationTrait;
protected $entityManager;
protected $entityType;
protected $fieldStorageDefinitions;
protected $originalDefinitions;
protected $storage;
protected $schema;
protected $database;
protected $installedStorageSchema;
public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, SqlContentEntityStorage $storage, Connection $database) {
$this->entityManager = $entity_manager;
$this->entityType = $entity_type;
$this->fieldStorageDefinitions = $entity_manager
->getFieldStorageDefinitions($entity_type
->id());
$this->storage = $storage;
$this->database = $database;
}
protected function installedStorageSchema() {
if (!isset($this->installedStorageSchema)) {
$this->installedStorageSchema = \Drupal::keyValue('entity.storage_schema.sql');
}
return $this->installedStorageSchema;
}
public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
return $this
->hasSharedTableStructureChange($entity_type, $original) || $this
->getEntitySchemaData($entity_type, $this
->getEntitySchema($entity_type, TRUE)) != $this
->loadEntitySchemaData($original);
}
protected function hasSharedTableStructureChange(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
return $entity_type
->isRevisionable() != $original
->isRevisionable() || $entity_type
->isTranslatable() != $original
->isTranslatable() || $this
->hasSharedTableNameChanges($entity_type, $original);
}
protected function hasSharedTableNameChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
return $entity_type
->getBaseTable() != $original
->getBaseTable() || $entity_type
->getDataTable() != $original
->getDataTable() || $entity_type
->getRevisionTable() != $original
->getRevisionTable() || $entity_type
->getRevisionDataTable() != $original
->getRevisionDataTable();
}
public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
$table_mapping = $this->storage
->getTableMapping();
if ($storage_definition
->hasCustomStorage() != $original
->hasCustomStorage() || $storage_definition
->getSchema() != $original
->getSchema() || $storage_definition
->isRevisionable() != $original
->isRevisionable() || $table_mapping
->allowsSharedTableStorage($storage_definition) != $table_mapping
->allowsSharedTableStorage($original) || $table_mapping
->requiresDedicatedTableStorage($storage_definition) != $table_mapping
->requiresDedicatedTableStorage($original)) {
return TRUE;
}
if ($storage_definition
->hasCustomStorage()) {
return FALSE;
}
return $this
->getSchemaFromStorageDefinition($storage_definition) != $this
->loadFieldSchemaData($original);
}
protected function getSchemaFromStorageDefinition(FieldStorageDefinitionInterface $storage_definition) {
assert('!$storage_definition->hasCustomStorage();');
$table_mapping = $this->storage
->getTableMapping();
$schema = [];
if ($table_mapping
->requiresDedicatedTableStorage($storage_definition)) {
$schema = $this
->getDedicatedTableSchema($storage_definition);
}
elseif ($table_mapping
->allowsSharedTableStorage($storage_definition)) {
$field_name = $storage_definition
->getName();
foreach (array_diff($table_mapping
->getTableNames(), $table_mapping
->getDedicatedTableNames()) as $table_name) {
if (in_array($field_name, $table_mapping
->getFieldNames($table_name))) {
$column_names = $table_mapping
->getColumnNames($storage_definition
->getName());
$schema[$table_name] = $this
->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
}
}
}
return $schema;
}
public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
$original_storage_class = $original
->getStorageClass();
if (!class_exists($original_storage_class)) {
return TRUE;
}
if (!$this
->hasSharedTableStructureChange($entity_type, $original)) {
return FALSE;
}
$original_storage = $this->entityManager
->createHandlerInstance($original_storage_class, $original);
return $original_storage
->hasData();
}
public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
return !$this->storage
->countFieldData($original, TRUE);
}
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
$this
->checkEntityType($entity_type);
$schema_handler = $this->database
->schema();
$schema = $this
->getEntitySchema($entity_type, TRUE);
foreach ($schema as $table_name => $table_schema) {
if (!$schema_handler
->tableExists($table_name)) {
$schema_handler
->createTable($table_name, $table_schema);
}
}
$field_storage_definitions = $this->entityManager
->getFieldStorageDefinitions($entity_type
->id());
$table_mapping = $this->storage
->getTableMapping($field_storage_definitions);
foreach ($field_storage_definitions as $field_storage_definition) {
if ($table_mapping
->requiresDedicatedTableStorage($field_storage_definition)) {
$this
->createDedicatedTableSchema($field_storage_definition);
}
elseif ($table_mapping
->allowsSharedTableStorage($field_storage_definition)) {
$this
->createSharedTableSchema($field_storage_definition, TRUE);
}
}
$this
->saveEntitySchemaData($entity_type, $schema);
}
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
$this
->checkEntityType($entity_type);
$this
->checkEntityType($original);
if (!$this
->requiresEntityStorageSchemaChanges($entity_type, $original)) {
return;
}
if ($this
->requiresEntityDataMigration($entity_type, $original)) {
throw new EntityStorageException('The SQL storage cannot change the schema for an existing entity type (' . $entity_type
->id() . ') with data.');
}
if ($this
->isTableEmpty($this->storage
->getBaseTable())) {
if ($this->database
->supportsTransactionalDDL()) {
$transaction = $this->database
->startTransaction();
}
try {
$this
->onEntityTypeDelete($original);
$this
->onEntityTypeCreate($entity_type);
} catch (\Exception $e) {
if ($this->database
->supportsTransactionalDDL()) {
$transaction
->rollback();
}
else {
$this
->onEntityTypeCreate($original);
}
throw $e;
}
}
else {
$this
->deleteEntitySchemaIndexes($this
->loadEntitySchemaData($entity_type));
$entity_schema = $this
->getEntitySchema($entity_type, TRUE);
$this
->createEntitySchemaIndexes($entity_schema);
$this
->saveEntitySchemaData($entity_type, $entity_schema);
}
}
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this
->checkEntityType($entity_type);
$schema_handler = $this->database
->schema();
$actual_definition = $this->entityManager
->getDefinition($entity_type
->id());
$this->storage
->setEntityType($entity_type);
foreach ($this
->getEntitySchemaTables() as $table_name) {
if ($schema_handler
->tableExists($table_name)) {
$schema_handler
->dropTable($table_name);
}
}
$field_storage_definitions = $this->entityManager
->getLastInstalledFieldStorageDefinitions($entity_type
->id());
$this->originalDefinitions = $field_storage_definitions;
$table_mapping = $this->storage
->getTableMapping($field_storage_definitions);
foreach ($field_storage_definitions as $field_storage_definition) {
if ($table_mapping
->requiresDedicatedTableStorage($field_storage_definition)) {
$this
->deleteDedicatedTableSchema($field_storage_definition);
}
elseif ($table_mapping
->allowsSharedTableStorage($field_storage_definition)) {
$this
->deleteFieldSchemaData($field_storage_definition);
}
}
$this->originalDefinitions = NULL;
$this->storage
->setEntityType($actual_definition);
$this
->deleteEntitySchemaData($entity_type);
}
public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) {
$this
->performFieldSchemaOperation('create', $storage_definition);
}
public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
$this->originalDefinitions = $this->fieldStorageDefinitions;
$this->originalDefinitions[$original
->getName()] = $original;
$this
->performFieldSchemaOperation('update', $storage_definition, $original);
$this->originalDefinitions = NULL;
}
public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) {
try {
if (!$storage_definition instanceof FieldStorageConfigInterface && $this->storage
->countFieldData($storage_definition, TRUE)) {
throw new FieldStorageDefinitionUpdateForbiddenException('Unable to delete a field (' . $storage_definition
->getName() . ' in ' . $storage_definition
->getTargetEntityTypeId() . ' entity) with data that cannot be purged.');
}
} catch (DatabaseException $e) {
return;
}
$table_mapping = $this->storage
->getTableMapping($this->entityManager
->getLastInstalledFieldStorageDefinitions($this->entityType
->id()));
if ($table_mapping
->requiresDedicatedTableStorage($storage_definition)) {
$table = $table_mapping
->getDedicatedDataTableName($storage_definition);
$new_table = $table_mapping
->getDedicatedDataTableName($storage_definition, TRUE);
$this->database
->schema()
->renameTable($table, $new_table);
if ($this->entityType
->isRevisionable()) {
$revision_table = $table_mapping
->getDedicatedRevisionTableName($storage_definition);
$revision_new_table = $table_mapping
->getDedicatedRevisionTableName($storage_definition, TRUE);
$this->database
->schema()
->renameTable($revision_table, $revision_new_table);
}
}
if (!$storage_definition instanceof FieldStorageConfigInterface) {
$this
->performFieldSchemaOperation('delete', $storage_definition);
}
}
public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) {
$this
->performFieldSchemaOperation('delete', $storage_definition);
}
protected function checkEntityType(EntityTypeInterface $entity_type) {
if ($entity_type
->id() != $this->entityType
->id()) {
throw new EntityStorageException("Unsupported entity type {$entity_type->id()}");
}
return TRUE;
}
protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) {
$this
->checkEntityType($entity_type);
$entity_type_id = $entity_type
->id();
if (!isset($this->schema[$entity_type_id]) || $reset) {
$actual_definition = $this->entityManager
->getDefinition($entity_type_id);
$this->storage
->setEntityType($entity_type);
$tables = $this
->getEntitySchemaTables();
$schema[$tables['base_table']] = $this
->initializeBaseTable($entity_type);
if (isset($tables['revision_table'])) {
$schema[$tables['revision_table']] = $this
->initializeRevisionTable($entity_type);
}
if (isset($tables['data_table'])) {
$schema[$tables['data_table']] = $this
->initializeDataTable($entity_type);
}
if (isset($tables['revision_data_table'])) {
$schema[$tables['revision_data_table']] = $this
->initializeRevisionDataTable($entity_type);
}
$table_mapping = $this->storage
->getTableMapping();
$table_names = array_diff($table_mapping
->getTableNames(), $table_mapping
->getDedicatedTableNames());
$storage_definitions = $this->entityManager
->getFieldStorageDefinitions($entity_type_id);
foreach ($table_names as $table_name) {
if (!isset($schema[$table_name])) {
$schema[$table_name] = array();
}
foreach ($table_mapping
->getFieldNames($table_name) as $field_name) {
if (!isset($storage_definitions[$field_name])) {
throw new FieldException("Field storage definition for '{$field_name}' could not be found.");
}
elseif ($table_mapping
->allowsSharedTableStorage($storage_definitions[$field_name])) {
$column_names = $table_mapping
->getColumnNames($field_name);
$storage_definition = $storage_definitions[$field_name];
$schema[$table_name] = array_merge_recursive($schema[$table_name], $this
->getSharedTableFieldSchema($storage_definition, $table_name, $column_names));
}
}
}
$this
->processBaseTable($entity_type, $schema[$tables['base_table']]);
if (isset($tables['revision_table'])) {
$this
->processRevisionTable($entity_type, $schema[$tables['revision_table']]);
}
if (isset($tables['data_table'])) {
$this
->processDataTable($entity_type, $schema[$tables['data_table']]);
}
if (isset($tables['revision_data_table'])) {
$this
->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]);
}
$this->schema[$entity_type_id] = $schema;
$this->storage
->setEntityType($actual_definition);
}
return $this->schema[$entity_type_id];
}
protected function getEntitySchemaTables() {
return array_filter(array(
'base_table' => $this->storage
->getBaseTable(),
'revision_table' => $this->storage
->getRevisionTable(),
'data_table' => $this->storage
->getDataTable(),
'revision_data_table' => $this->storage
->getRevisionDataTable(),
));
}
protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, array $schema) {
$entity_type_id = $entity_type
->id();
$field_schema_identifiers = [];
$storage_definitions = $this->entityManager
->getFieldStorageDefinitions($entity_type_id);
$table_mapping = $this->storage
->getTableMapping($storage_definitions);
foreach ($storage_definitions as $field_name => $storage_definition) {
if ($table_mapping
->allowsSharedTableStorage($storage_definition)) {
$name = $this
->getFieldSchemaIdentifierName($entity_type_id, $field_name);
$field_schema_identifiers[$name] = $name;
foreach ($storage_definition
->getColumns() as $key => $columns) {
$name = $this
->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
$field_schema_identifiers[$name] = $name;
}
}
}
$schema_data = [];
$keys = [
'indexes',
'unique keys',
];
$unused_keys = array_flip([
'description',
'fields',
'foreign keys',
]);
foreach ($schema as $table_name => $table_schema) {
$table_schema = array_diff_key($table_schema, $unused_keys);
foreach ($keys as $key) {
if ($field_schema_identifiers && !empty($table_schema[$key])) {
$table_schema[$key] = array_diff_key($table_schema[$key], $field_schema_identifiers);
}
}
$schema_data[$table_name] = array_filter($table_schema);
}
return $schema_data;
}
protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) {
return $this
->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes');
}
protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) {
return $this
->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys');
}
protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) {
$data = array();
foreach ($field_schema[$schema_key] as $key => $columns) {
$entity_type_id = $this->entityType
->id();
$real_key = $this
->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key);
foreach ($columns as $column) {
if (is_array($column)) {
list($column_name, $length) = $column;
$data[$real_key][] = array(
$column_mapping[$column_name],
$length,
);
}
else {
$data[$real_key][] = $column_mapping[$column];
}
}
}
return $data;
}
protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key = NULL) {
$real_key = isset($key) ? "{$entity_type_id}_field__{$field_name}__{$key}" : "{$entity_type_id}_field__{$field_name}";
if (strlen($real_key) > 48) {
$entity_type = substr($entity_type_id, 0, 36);
$field_hash = substr(hash('sha256', $real_key), 0, 10);
$real_key = $entity_type . '__' . $field_hash;
}
return $real_key;
}
protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) {
$foreign_keys = array();
foreach ($field_schema['foreign keys'] as $specifier => $specification) {
$entity_type_id = $this->entityType
->id();
$real_specifier = "{$entity_type_id}_field__{$field_name}__{$specifier}";
$foreign_keys[$real_specifier]['table'] = $specification['table'];
foreach ($specification['columns'] as $column => $referenced) {
$foreign_keys[$real_specifier]['columns'][$column_mapping[$column]] = $referenced;
}
}
return $foreign_keys;
}
protected function loadEntitySchemaData(EntityTypeInterface $entity_type) {
return $this
->installedStorageSchema()
->get($entity_type
->id() . '.entity_schema_data', array());
}
protected function saveEntitySchemaData(EntityTypeInterface $entity_type, $schema) {
$data = $this
->getEntitySchemaData($entity_type, $schema);
$this
->installedStorageSchema()
->set($entity_type
->id() . '.entity_schema_data', $data);
}
protected function deleteEntitySchemaData(EntityTypeInterface $entity_type) {
$this
->installedStorageSchema()
->delete($entity_type
->id() . '.entity_schema_data');
}
protected function loadFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
return $this
->installedStorageSchema()
->get($storage_definition
->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition
->getName(), array());
}
protected function saveFieldSchemaData(FieldStorageDefinitionInterface $storage_definition, $schema) {
$this
->installedStorageSchema()
->set($storage_definition
->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition
->getName(), $schema);
}
protected function deleteFieldSchemaData(FieldStorageDefinitionInterface $storage_definition) {
$this
->installedStorageSchema()
->delete($storage_definition
->getTargetEntityTypeId() . '.field_schema_data.' . $storage_definition
->getName());
}
protected function initializeBaseTable(ContentEntityTypeInterface $entity_type) {
$entity_type_id = $entity_type
->id();
$schema = array(
'description' => "The base table for {$entity_type_id} entities.",
'primary key' => array(
$entity_type
->getKey('id'),
),
'indexes' => array(),
'foreign keys' => array(),
);
if ($entity_type
->hasKey('revision')) {
$revision_key = $entity_type
->getKey('revision');
$key_name = $this
->getEntityIndexName($entity_type, $revision_key);
$schema['unique keys'][$key_name] = array(
$revision_key,
);
$schema['foreign keys'][$entity_type_id . '__revision'] = array(
'table' => $this->storage
->getRevisionTable(),
'columns' => array(
$revision_key => $revision_key,
),
);
}
$this
->addTableDefaults($schema);
return $schema;
}
protected function initializeRevisionTable(ContentEntityTypeInterface $entity_type) {
$entity_type_id = $entity_type
->id();
$id_key = $entity_type
->getKey('id');
$revision_key = $entity_type
->getKey('revision');
$schema = array(
'description' => "The revision table for {$entity_type_id} entities.",
'primary key' => array(
$revision_key,
),
'indexes' => array(),
'foreign keys' => array(
$entity_type_id . '__revisioned' => array(
'table' => $this->storage
->getBaseTable(),
'columns' => array(
$id_key => $id_key,
),
),
),
);
$schema['indexes'][$this
->getEntityIndexName($entity_type, $id_key)] = array(
$id_key,
);
$this
->addTableDefaults($schema);
return $schema;
}
protected function initializeDataTable(ContentEntityTypeInterface $entity_type) {
$entity_type_id = $entity_type
->id();
$id_key = $entity_type
->getKey('id');
$schema = array(
'description' => "The data table for {$entity_type_id} entities.",
'primary key' => array(
$id_key,
$entity_type
->getKey('langcode'),
),
'indexes' => array(
$entity_type_id . '__id__default_langcode__langcode' => array(
$id_key,
$entity_type
->getKey('default_langcode'),
$entity_type
->getKey('langcode'),
),
),
'foreign keys' => array(
$entity_type_id => array(
'table' => $this->storage
->getBaseTable(),
'columns' => array(
$id_key => $id_key,
),
),
),
);
if ($entity_type
->hasKey('revision')) {
$key = $entity_type
->getKey('revision');
$schema['indexes'][$this
->getEntityIndexName($entity_type, $key)] = array(
$key,
);
}
$this
->addTableDefaults($schema);
return $schema;
}
protected function initializeRevisionDataTable(ContentEntityTypeInterface $entity_type) {
$entity_type_id = $entity_type
->id();
$id_key = $entity_type
->getKey('id');
$revision_key = $entity_type
->getKey('revision');
$schema = array(
'description' => "The revision data table for {$entity_type_id} entities.",
'primary key' => array(
$revision_key,
$entity_type
->getKey('langcode'),
),
'indexes' => array(
$entity_type_id . '__id__default_langcode__langcode' => array(
$id_key,
$entity_type
->getKey('default_langcode'),
$entity_type
->getKey('langcode'),
),
),
'foreign keys' => array(
$entity_type_id => array(
'table' => $this->storage
->getBaseTable(),
'columns' => array(
$id_key => $id_key,
),
),
$entity_type_id . '__revision' => array(
'table' => $this->storage
->getRevisionTable(),
'columns' => array(
$revision_key => $revision_key,
),
),
),
);
$this
->addTableDefaults($schema);
return $schema;
}
protected function addTableDefaults(&$schema) {
$schema += array(
'fields' => array(),
'unique keys' => array(),
'indexes' => array(),
'foreign keys' => array(),
);
}
protected function processBaseTable(ContentEntityTypeInterface $entity_type, array &$schema) {
$this
->processIdentifierSchema($schema, $entity_type
->getKey('id'));
}
protected function processRevisionTable(ContentEntityTypeInterface $entity_type, array &$schema) {
$this
->processIdentifierSchema($schema, $entity_type
->getKey('revision'));
}
protected function processDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
$schema['fields'][$entity_type
->getKey('default_langcode')]['not null'] = TRUE;
}
protected function processRevisionDataTable(ContentEntityTypeInterface $entity_type, array &$schema) {
$schema['fields'][$entity_type
->getKey('default_langcode')]['not null'] = TRUE;
}
protected function processIdentifierSchema(&$schema, $key) {
if ($schema['fields'][$key]['type'] == 'int') {
$schema['fields'][$key]['type'] = 'serial';
}
$schema['fields'][$key]['not null'] = TRUE;
unset($schema['fields'][$key]['default']);
}
protected function performFieldSchemaOperation($operation, FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original = NULL) {
$table_mapping = $this->storage
->getTableMapping();
if ($table_mapping
->requiresDedicatedTableStorage($storage_definition)) {
$this
->{$operation . 'DedicatedTableSchema'}($storage_definition, $original);
}
elseif ($table_mapping
->allowsSharedTableStorage($storage_definition)) {
$this
->{$operation . 'SharedTableSchema'}($storage_definition, $original);
}
}
protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
$schema = $this
->getDedicatedTableSchema($storage_definition);
foreach ($schema as $name => $table) {
if (!$this->database
->schema()
->tableExists($name)) {
$this->database
->schema()
->createTable($name, $table);
}
}
$this
->saveFieldSchemaData($storage_definition, $schema);
}
protected function createSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) {
$created_field_name = $storage_definition
->getName();
$table_mapping = $this->storage
->getTableMapping();
$column_names = $table_mapping
->getColumnNames($created_field_name);
$schema_handler = $this->database
->schema();
$shared_table_names = array_diff($table_mapping
->getTableNames(), $table_mapping
->getDedicatedTableNames());
$schema = array();
foreach ($shared_table_names as $table_name) {
foreach ($table_mapping
->getFieldNames($table_name) as $field_name) {
if ($field_name == $created_field_name) {
$schema[$table_name] = $this
->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
if (!$only_save) {
foreach ($schema[$table_name]['fields'] as $name => $specifier) {
if (!$schema_handler
->fieldExists($table_name, $name)) {
$schema_handler
->addField($table_name, $name, $specifier);
}
}
if (!empty($schema[$table_name]['indexes'])) {
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
$this
->addIndex($table_name, $name, $specifier, $schema[$table_name]);
}
}
if (!empty($schema[$table_name]['unique keys'])) {
foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
$schema_handler
->addUniqueKey($table_name, $name, $specifier);
}
}
}
break;
}
}
}
$this
->saveFieldSchemaData($storage_definition, $schema);
if (!$only_save) {
$this
->createEntitySchemaIndexes($this
->getEntitySchema($this->entityType), $storage_definition);
}
}
protected function deleteDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
$deleted = !$this->originalDefinitions;
$table_mapping = $this->storage
->getTableMapping();
$table_name = $table_mapping
->getDedicatedDataTableName($storage_definition, $deleted);
$this->database
->schema()
->dropTable($table_name);
if ($this->entityType
->isRevisionable()) {
$revision_name = $table_mapping
->getDedicatedRevisionTableName($storage_definition, $deleted);
$this->database
->schema()
->dropTable($revision_name);
}
$this
->deleteFieldSchemaData($storage_definition);
}
protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $storage_definition) {
$this
->deleteEntitySchemaIndexes($this
->loadEntitySchemaData($this->entityType), $storage_definition);
$deleted_field_name = $storage_definition
->getName();
$table_mapping = $this->storage
->getTableMapping($this->entityManager
->getLastInstalledFieldStorageDefinitions($this->entityType
->id()));
$column_names = $table_mapping
->getColumnNames($deleted_field_name);
$schema_handler = $this->database
->schema();
$shared_table_names = array_diff($table_mapping
->getTableNames(), $table_mapping
->getDedicatedTableNames());
foreach ($shared_table_names as $table_name) {
foreach ($table_mapping
->getFieldNames($table_name) as $field_name) {
if ($field_name == $deleted_field_name) {
$schema = $this
->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
if (!empty($schema['indexes'])) {
foreach ($schema['indexes'] as $name => $specifier) {
$schema_handler
->dropIndex($table_name, $name);
}
}
if (!empty($schema['unique keys'])) {
foreach ($schema['unique keys'] as $name => $specifier) {
$schema_handler
->dropUniqueKey($table_name, $name);
}
}
foreach ($column_names as $column_name) {
$schema_handler
->dropField($table_name, $column_name);
}
break;
}
}
}
$this
->deleteFieldSchemaData($storage_definition);
}
protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
if (!$this->storage
->countFieldData($original, TRUE)) {
if ($this->database
->supportsTransactionalDDL()) {
$transaction = $this->database
->startTransaction();
}
try {
$this
->performFieldSchemaOperation('delete', $original);
$this
->performFieldSchemaOperation('create', $storage_definition);
} catch (\Exception $e) {
if ($this->database
->supportsTransactionalDDL()) {
$transaction
->rollback();
}
else {
$this
->performFieldSchemaOperation('create', $original);
}
throw $e;
}
}
else {
if ($this
->hasColumnChanges($storage_definition, $original)) {
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition
->getName() . ' in ' . $storage_definition
->getTargetEntityTypeId() . ' entity) with data.');
}
$table_mapping = $this->storage
->getTableMapping();
$table = $table_mapping
->getDedicatedDataTableName($original);
$revision_table = $table_mapping
->getDedicatedRevisionTableName($original);
$schema = $storage_definition
->getSchema();
$original_schema = $original
->getSchema();
$actual_schema = $this
->getDedicatedTableSchema($storage_definition);
foreach ($original_schema['indexes'] as $name => $columns) {
if (!isset($schema['indexes'][$name]) || $columns != $schema['indexes'][$name]) {
$real_name = $this
->getFieldIndexName($storage_definition, $name);
$this->database
->schema()
->dropIndex($table, $real_name);
$this->database
->schema()
->dropIndex($revision_table, $real_name);
}
}
$table = $table_mapping
->getDedicatedDataTableName($storage_definition);
$revision_table = $table_mapping
->getDedicatedRevisionTableName($storage_definition);
foreach ($schema['indexes'] as $name => $columns) {
if (!isset($original_schema['indexes'][$name]) || $columns != $original_schema['indexes'][$name]) {
$real_name = $this
->getFieldIndexName($storage_definition, $name);
$real_columns = array();
foreach ($columns as $column_name) {
if (is_array($column_name)) {
$real_columns[] = array(
$table_mapping
->getFieldColumnName($storage_definition, $column_name[0]),
$column_name[1],
);
}
else {
$real_columns[] = $table_mapping
->getFieldColumnName($storage_definition, $column_name);
}
}
$this
->addIndex($table, $real_name, $real_columns, $actual_schema[$table]);
$this
->addIndex($revision_table, $real_name, $real_columns, $actual_schema[$revision_table]);
}
}
$this
->saveFieldSchemaData($storage_definition, $this
->getDedicatedTableSchema($storage_definition));
}
}
protected function updateSharedTableSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
if (!$this->storage
->countFieldData($original, TRUE)) {
if ($this->database
->supportsTransactionalDDL()) {
$transaction = $this->database
->startTransaction();
}
try {
$this
->performFieldSchemaOperation('delete', $original);
$this
->performFieldSchemaOperation('create', $storage_definition);
} catch (\Exception $e) {
if ($this->database
->supportsTransactionalDDL()) {
$transaction
->rollback();
}
else {
$this
->createSharedTableSchema($original);
}
throw $e;
}
}
else {
if ($this
->hasColumnChanges($storage_definition, $original)) {
throw new FieldStorageDefinitionUpdateForbiddenException('The SQL storage cannot change the schema for an existing field (' . $storage_definition
->getName() . ' in ' . $storage_definition
->getTargetEntityTypeId() . ' entity) with data.');
}
$updated_field_name = $storage_definition
->getName();
$table_mapping = $this->storage
->getTableMapping();
$column_names = $table_mapping
->getColumnNames($updated_field_name);
$schema_handler = $this->database
->schema();
$original_schema = $this
->loadFieldSchemaData($original);
$schema = array();
foreach ($table_mapping
->getTableNames() as $table_name) {
foreach ($table_mapping
->getFieldNames($table_name) as $field_name) {
if ($field_name == $updated_field_name) {
$schema[$table_name] = $this
->getSharedTableFieldSchema($storage_definition, $table_name, $column_names);
foreach ($schema[$table_name]['fields'] as $column_name => $specifier) {
$not_null = !empty($specifier['not null']);
$original_not_null = !empty($original_schema[$table_name]['fields'][$column_name]['not null']);
if ($not_null !== $original_not_null) {
if ($not_null && $this
->hasNullFieldPropertyData($table_name, $column_name)) {
throw new EntityStorageException("The {$column_name} column cannot have NOT NULL constraints as it holds NULL values.");
}
$column_schema = $original_schema[$table_name]['fields'][$column_name];
$column_schema['not null'] = $not_null;
$schema_handler
->changeField($table_name, $field_name, $field_name, $column_schema);
}
}
if (!empty($original_schema[$table_name]['indexes'])) {
foreach ($original_schema[$table_name]['indexes'] as $name => $specifier) {
$schema_handler
->dropIndex($table_name, $name);
}
}
if (!empty($original_schema[$table_name]['unique keys'])) {
foreach ($original_schema[$table_name]['unique keys'] as $name => $specifier) {
$schema_handler
->dropUniqueKey($table_name, $name);
}
}
if (!empty($schema[$table_name]['indexes'])) {
foreach ($schema[$table_name]['indexes'] as $name => $specifier) {
$this
->addIndex($table_name, $name, $specifier, $schema[$table_name]);
}
}
if (!empty($schema[$table_name]['unique keys'])) {
foreach ($schema[$table_name]['unique keys'] as $name => $specifier) {
$schema_handler
->addUniqueKey($table_name, $name, $specifier);
}
}
break;
}
}
}
$this
->saveFieldSchemaData($storage_definition, $schema);
}
}
protected function createEntitySchemaIndexes(array $entity_schema, FieldStorageDefinitionInterface $storage_definition = NULL) {
$schema_handler = $this->database
->schema();
if ($storage_definition) {
$table_mapping = $this->storage
->getTableMapping();
$column_names = $table_mapping
->getColumnNames($storage_definition
->getName());
}
$index_keys = [
'indexes' => 'addIndex',
'unique keys' => 'addUniqueKey',
];
foreach ($this
->getEntitySchemaData($this->entityType, $entity_schema) as $table_name => $schema) {
$schema['fields'] = $entity_schema[$table_name]['fields'];
foreach ($index_keys as $key => $add_method) {
if (!empty($schema[$key])) {
foreach ($schema[$key] as $name => $specifier) {
$create = FALSE;
$specifier_columns = array_map(function ($item) {
return is_string($item) ? $item : reset($item);
}, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$create = TRUE;
foreach ($specifier_columns as $specifier_column_name) {
if (!$schema_handler
->fieldExists($table_name, $specifier_column_name)) {
$create = FALSE;
break;
}
}
}
if ($create) {
$this
->{$add_method}($table_name, $name, $specifier, $schema);
}
}
}
}
}
}
protected function deleteEntitySchemaIndexes(array $entity_schema_data, FieldStorageDefinitionInterface $storage_definition = NULL) {
$schema_handler = $this->database
->schema();
if ($storage_definition) {
$table_mapping = $this->storage
->getTableMapping();
$column_names = $table_mapping
->getColumnNames($storage_definition
->getName());
}
$index_keys = [
'indexes' => 'dropIndex',
'unique keys' => 'dropUniqueKey',
];
foreach ($entity_schema_data as $table_name => $schema) {
foreach ($index_keys as $key => $drop_method) {
if (!empty($schema[$key])) {
foreach ($schema[$key] as $name => $specifier) {
$specifier_columns = array_map(function ($item) {
return is_string($item) ? $item : reset($item);
}, $specifier);
if (!isset($column_names) || array_intersect($specifier_columns, $column_names)) {
$schema_handler
->{$drop_method}($table_name, $name);
}
}
}
}
}
}
protected function hasNullFieldPropertyData($table_name, $column_name) {
$query = $this->database
->select($table_name, 't')
->fields('t', [
$column_name,
])
->range(0, 1);
$query
->isNull('t.' . $column_name);
$result = $query
->execute()
->fetchAssoc();
return (bool) $result;
}
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping) {
$schema = array();
$field_schema = $storage_definition
->getSchema();
if (array_intersect(array_keys($field_schema['columns']), $this->storage
->getTableMapping()
->getReservedColumns())) {
throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
}
$field_name = $storage_definition
->getName();
$base_table = $this->storage
->getBaseTable();
$not_null_keys = $this->entityType
->getKeys();
unset($not_null_keys['label']);
if ($table_name == $base_table) {
unset($not_null_keys['revision']);
}
foreach ($column_mapping as $field_column_name => $schema_field_name) {
$column_schema = $field_schema['columns'][$field_column_name];
$schema['fields'][$schema_field_name] = $column_schema;
$schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $not_null_keys);
}
if (!empty($field_schema['indexes'])) {
$schema['indexes'] = $this
->getFieldIndexes($field_name, $field_schema, $column_mapping);
}
if (!empty($field_schema['unique keys'])) {
$schema['unique keys'] = $this
->getFieldUniqueKeys($field_name, $field_schema, $column_mapping);
}
if (!empty($field_schema['foreign keys'])) {
$schema['foreign keys'] = $this
->getFieldForeignKeys($field_name, $field_schema, $column_mapping);
}
return $schema;
}
protected function addSharedTableFieldIndex(FieldStorageDefinitionInterface $storage_definition, &$schema, $not_null = FALSE, $size = NULL) {
$name = $storage_definition
->getName();
$real_key = $this
->getFieldSchemaIdentifierName($storage_definition
->getTargetEntityTypeId(), $name);
$schema['indexes'][$real_key] = array(
$size ? array(
$name,
$size,
) : $name,
);
if ($not_null) {
$schema['fields'][$name]['not null'] = TRUE;
}
}
protected function addSharedTableFieldUniqueKey(FieldStorageDefinitionInterface $storage_definition, &$schema) {
$name = $storage_definition
->getName();
$real_key = $this
->getFieldSchemaIdentifierName($storage_definition
->getTargetEntityTypeId(), $name);
$schema['unique keys'][$real_key] = array(
$name,
);
$schema['fields'][$name]['not null'] = TRUE;
}
protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface $storage_definition, &$schema, $foreign_table, $foreign_column) {
$name = $storage_definition
->getName();
$real_key = $this
->getFieldSchemaIdentifierName($storage_definition
->getTargetEntityTypeId(), $name);
$schema['foreign keys'][$real_key] = array(
'table' => $foreign_table,
'columns' => array(
$name => $foreign_column,
),
);
}
protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) {
$description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
$description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}.";
$id_definition = $this->fieldStorageDefinitions[$this->entityType
->getKey('id')];
if ($id_definition
->getType() == 'integer') {
$id_schema = array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity id this data is attached to',
);
}
else {
$id_schema = array(
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'description' => 'The entity id this data is attached to',
);
}
if (!$this->entityType
->isRevisionable()) {
$revision_id_schema = $id_schema;
$revision_id_schema['description'] = 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id';
}
elseif ($this->fieldStorageDefinitions[$this->entityType
->getKey('revision')]
->getType() == 'integer') {
$revision_id_schema = array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The entity revision id this data is attached to',
);
}
else {
$revision_id_schema = array(
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'description' => 'The entity revision id this data is attached to',
);
}
$data_schema = array(
'description' => $description_current,
'fields' => array(
'bundle' => array(
'type' => 'varchar_ascii',
'length' => 128,
'not null' => TRUE,
'default' => '',
'description' => 'The field instance bundle to which this row belongs, used when deleting a field instance',
),
'deleted' => array(
'type' => 'int',
'size' => 'tiny',
'not null' => TRUE,
'default' => 0,
'description' => 'A boolean indicating whether this data item has been deleted',
),
'entity_id' => $id_schema,
'revision_id' => $revision_id_schema,
'langcode' => array(
'type' => 'varchar_ascii',
'length' => 32,
'not null' => TRUE,
'default' => '',
'description' => 'The language code for this data item.',
),
'delta' => array(
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'description' => 'The sequence number for this data item, used for multi-value fields',
),
),
'primary key' => array(
'entity_id',
'deleted',
'delta',
'langcode',
),
'indexes' => array(
'bundle' => array(
'bundle',
),
'revision_id' => array(
'revision_id',
),
),
);
$schema = $storage_definition
->getSchema();
$properties = $storage_definition
->getPropertyDefinitions();
$table_mapping = $this->storage
->getTableMapping();
if (array_intersect(array_keys($schema['columns']), $table_mapping
->getReservedColumns())) {
throw new FieldException("Illegal field column names on {$storage_definition->getName()}");
}
foreach ($schema['columns'] as $column_name => $attributes) {
$real_name = $table_mapping
->getFieldColumnName($storage_definition, $column_name);
$data_schema['fields'][$real_name] = $attributes;
$data_schema['fields'][$real_name]['not null'] = $properties[$column_name]
->isRequired();
}
foreach ($schema['indexes'] as $index_name => $columns) {
$real_name = $this
->getFieldIndexName($storage_definition, $index_name);
foreach ($columns as $column_name) {
if (is_array($column_name)) {
$data_schema['indexes'][$real_name][] = array(
$table_mapping
->getFieldColumnName($storage_definition, $column_name[0]),
$column_name[1],
);
}
else {
$data_schema['indexes'][$real_name][] = $table_mapping
->getFieldColumnName($storage_definition, $column_name);
}
}
}
foreach ($schema['foreign keys'] as $specifier => $specification) {
$real_name = $this
->getFieldIndexName($storage_definition, $specifier);
$data_schema['foreign keys'][$real_name]['table'] = $specification['table'];
foreach ($specification['columns'] as $column_name => $referenced) {
$sql_storage_column = $table_mapping
->getFieldColumnName($storage_definition, $column_name);
$data_schema['foreign keys'][$real_name]['columns'][$sql_storage_column] = $referenced;
}
}
$dedicated_table_schema = array(
$table_mapping
->getDedicatedDataTableName($storage_definition) => $data_schema,
);
$entity_type = $entity_type ?: $this->entityType;
if ($entity_type
->isRevisionable()) {
$revision_schema = $data_schema;
$revision_schema['description'] = $description_revision;
$revision_schema['primary key'] = array(
'entity_id',
'revision_id',
'deleted',
'delta',
'langcode',
);
$revision_schema['fields']['revision_id']['not null'] = TRUE;
$revision_schema['fields']['revision_id']['description'] = 'The entity revision id this data is attached to';
$dedicated_table_schema += array(
$table_mapping
->getDedicatedRevisionTableName($storage_definition) => $revision_schema,
);
}
return $dedicated_table_schema;
}
protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $index) {
return $entity_type
->id() . '__' . $index;
}
protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) {
return $storage_definition
->getName() . '_' . $index;
}
protected function isTableEmpty($table_name) {
return !$this->database
->schema()
->tableExists($table_name) || !$this->database
->select($table_name)
->countQuery()
->range(0, 1)
->execute()
->fetchField();
}
protected function hasColumnChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) {
if ($storage_definition
->getColumns() != $original
->getColumns()) {
return TRUE;
}
if (!$storage_definition
->hasCustomStorage()) {
$keys = array_flip($this
->getColumnSchemaRelevantKeys());
$definition_schema = $this
->getSchemaFromStorageDefinition($storage_definition);
foreach ($this
->loadFieldSchemaData($original) as $table => $table_schema) {
foreach ($table_schema['fields'] as $name => $spec) {
$definition_spec = array_intersect_key($definition_schema[$table]['fields'][$name], $keys);
$stored_spec = array_intersect_key($spec, $keys);
if ($definition_spec != $stored_spec) {
return TRUE;
}
}
}
}
return FALSE;
}
protected function getColumnSchemaRelevantKeys() {
return [
'type',
'size',
'length',
'unsigned',
];
}
protected function addIndex($table, $name, array $specifier, array $schema) {
$schema_handler = $this->database
->schema();
$schema_handler
->dropIndex($table, $name);
$schema_handler
->addIndex($table, $name, $specifier, $schema);
}
protected function addUniqueKey($table, $name, array $specifier) {
$schema_handler = $this->database
->schema();
$schema_handler
->dropUniqueKey($table, $name);
$schema_handler
->addUniqueKey($table, $name, $specifier);
}
}