Commit 295e297e authored by plach's avatar plach

Issue #2916018 by amateescu, plach, Berdir, tstoeckler: Allow the table...

Issue #2916018 by amateescu, plach, Berdir, tstoeckler: Allow the table mapping to be initialized from outside the storage
parent 37b9e45a
...@@ -24,6 +24,35 @@ class DefaultTableMapping implements TableMappingInterface { ...@@ -24,6 +24,35 @@ class DefaultTableMapping implements TableMappingInterface {
*/ */
protected $fieldStorageDefinitions = []; protected $fieldStorageDefinitions = [];
/**
* The base table of the entity.
*
* @var string
*/
protected $baseTable;
/**
* The table that stores revisions, if the entity supports revisions.
*
* @var string
*/
protected $revisionTable;
/**
* The table that stores field data, if the entity has multilingual support.
*
* @var string
*/
protected $dataTable;
/**
* The table that stores revision field data if the entity supports revisions
* and has multilingual support.
*
* @var string
*/
protected $revisionDataTable;
/** /**
* A list of field names per table. * A list of field names per table.
* *
...@@ -87,6 +116,132 @@ class DefaultTableMapping implements TableMappingInterface { ...@@ -87,6 +116,132 @@ class DefaultTableMapping implements TableMappingInterface {
public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions) { public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
$this->entityType = $entity_type; $this->entityType = $entity_type;
$this->fieldStorageDefinitions = $storage_definitions; $this->fieldStorageDefinitions = $storage_definitions;
// @todo Remove table names from the entity type definition in
// https://www.drupal.org/node/2232465.
$this->baseTable = $entity_type->getBaseTable() ?: $entity_type->id();
if ($entity_type->isRevisionable()) {
$this->revisionTable = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
}
if ($entity_type->isTranslatable()) {
$this->dataTable = $entity_type->getDataTable() ?: $entity_type->id() . '_field_data';
}
if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
$this->revisionDataTable = $entity_type->getRevisionDataTable() ?: $entity_type->id() . '_field_revision';
}
}
/**
* Initializes the table mapping.
*
* @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions
* A list of field storage definitions that should be available for the
* field columns of this table mapping.
*
* @return static
*
* @internal
*/
public static function create(ContentEntityTypeInterface $entity_type, array $storage_definitions) {
$table_mapping = new static($entity_type, $storage_definitions);
$revisionable = $entity_type->isRevisionable();
$translatable = $entity_type->isTranslatable();
$id_key = $entity_type->getKey('id');
$revision_key = $entity_type->getKey('revision');
$bundle_key = $entity_type->getKey('bundle');
$uuid_key = $entity_type->getKey('uuid');
$langcode_key = $entity_type->getKey('langcode');
$shared_table_definitions = array_filter($storage_definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
return $table_mapping->allowsSharedTableStorage($definition);
});
$key_fields = array_values(array_filter([$id_key, $revision_key, $bundle_key, $uuid_key, $langcode_key]));
$all_fields = array_keys($shared_table_definitions);
$revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
return $definition->isRevisionable();
}));
// Make sure the key fields come first in the list of fields.
$all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
$revision_metadata_fields = $revisionable ? array_values($entity_type->getRevisionMetadataKeys()) : [];
if (!$revisionable && !$translatable) {
// The base layout stores all the base field values in the base table.
$table_mapping->setFieldNames($table_mapping->baseTable, $all_fields);
}
elseif ($revisionable && !$translatable) {
// The revisionable layout stores all the base field values in the base
// table, except for revision metadata fields. Revisionable fields
// denormalized in the base table but also stored in the revision table
// together with the entity ID and the revision ID as identifiers.
$table_mapping->setFieldNames($table_mapping->baseTable, array_diff($all_fields, $revision_metadata_fields));
$revision_key_fields = [$id_key, $revision_key];
$table_mapping->setFieldNames($table_mapping->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
}
elseif (!$revisionable && $translatable) {
// Multilingual layouts store key field values in the base table. The
// other base field values are stored in the data table, no matter
// whether they are translatable or not. The data table holds also a
// denormalized copy of the bundle field value to allow for more
// performant queries. This means that only the UUID is not stored on
// the data table.
$table_mapping
->setFieldNames($table_mapping->baseTable, $key_fields)
->setFieldNames($table_mapping->dataTable, array_values(array_diff($all_fields, [$uuid_key])));
}
elseif ($revisionable && $translatable) {
// The revisionable multilingual layout stores key field values in the
// base table and the revision table holds the entity ID, revision ID and
// langcode ID along with revision metadata. The revision data table holds
// data field values for all the revisionable fields and the data table
// holds the data field values for all non-revisionable fields. The data
// field values of revisionable fields are denormalized in the data
// table, as well.
$table_mapping->setFieldNames($table_mapping->baseTable, $key_fields);
// Like in the multilingual, non-revisionable case the UUID is not
// in the data table. Additionally, do not store revision metadata
// fields in the data table.
$data_fields = array_values(array_diff($all_fields, [$uuid_key], $revision_metadata_fields));
$table_mapping->setFieldNames($table_mapping->dataTable, $data_fields);
$revision_base_fields = array_merge([$id_key, $revision_key, $langcode_key], $revision_metadata_fields);
$table_mapping->setFieldNames($table_mapping->revisionTable, $revision_base_fields);
$revision_data_key_fields = [$id_key, $revision_key, $langcode_key];
$revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$langcode_key]);
$table_mapping->setFieldNames($table_mapping->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
}
// Add dedicated tables.
$dedicated_table_definitions = array_filter($table_mapping->fieldStorageDefinitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
return $table_mapping->requiresDedicatedTableStorage($definition);
});
$extra_columns = [
'bundle',
'deleted',
'entity_id',
'revision_id',
'langcode',
'delta',
];
foreach ($dedicated_table_definitions as $field_name => $definition) {
$tables = [$table_mapping->getDedicatedDataTableName($definition)];
if ($revisionable && $definition->isRevisionable()) {
$tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
}
foreach ($tables as $table_name) {
$table_mapping->setFieldNames($table_name, [$field_name]);
$table_mapping->setExtraColumns($table_name, $extra_columns);
}
}
return $table_mapping;
} }
/** /**
...@@ -143,17 +298,13 @@ public function getFieldTableName($field_name) { ...@@ -143,17 +298,13 @@ public function getFieldTableName($field_name) {
// where field data is stored, otherwise the base table is responsible for // where field data is stored, otherwise the base table is responsible for
// storing field data. Revision metadata is an exception as it's stored // storing field data. Revision metadata is an exception as it's stored
// only in the revision table. // only in the revision table.
// @todo The table mapping itself should know about entity tables. See
// https://www.drupal.org/node/2274017.
/** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
$storage = \Drupal::entityManager()->getStorage($this->entityType->id());
$storage_definition = $this->fieldStorageDefinitions[$field_name]; $storage_definition = $this->fieldStorageDefinitions[$field_name];
$table_names = [ $table_names = array_filter([
$storage->getDataTable(), $this->dataTable,
$storage->getBaseTable(), $this->baseTable,
$storage->getRevisionTable(), $this->revisionTable,
$this->getDedicatedDataTableName($storage_definition), $this->getDedicatedDataTableName($storage_definition),
]; ]);
// Collect field columns. // Collect field columns.
$field_columns = []; $field_columns = [];
...@@ -161,7 +312,7 @@ public function getFieldTableName($field_name) { ...@@ -161,7 +312,7 @@ public function getFieldTableName($field_name) {
$field_columns[] = $this->getFieldColumnName($storage_definition, $property_name); $field_columns[] = $this->getFieldColumnName($storage_definition, $property_name);
} }
foreach (array_filter($table_names) as $table_name) { foreach ($table_names as $table_name) {
$columns = $this->getAllColumns($table_name); $columns = $this->getAllColumns($table_name);
// We assume finding one field column belonging to the mapping is enough // We assume finding one field column belonging to the mapping is enough
// to identify the field table. // to identify the field table.
...@@ -227,6 +378,10 @@ public function getFieldColumnName(FieldStorageDefinitionInterface $storage_defi ...@@ -227,6 +378,10 @@ public function getFieldColumnName(FieldStorageDefinitionInterface $storage_defi
* A list of field names to add the columns for. * A list of field names to add the columns for.
* *
* @return $this * @return $this
*
* @deprecated in Drupal 8.6.0 and will be changed to a protected method
* before Drupal 9.0.0. There will be no replacement for it because the
* default table mapping is now able to be initialized on its own.
*/ */
public function setFieldNames($table_name, array $field_names) { public function setFieldNames($table_name, array $field_names) {
$this->fieldNames[$table_name] = $field_names; $this->fieldNames[$table_name] = $field_names;
...@@ -254,6 +409,10 @@ public function getExtraColumns($table_name) { ...@@ -254,6 +409,10 @@ public function getExtraColumns($table_name) {
* The list of column names. * The list of column names.
* *
* @return $this * @return $this
*
* @deprecated in Drupal 8.6.0 and will be changed to a protected method
* before Drupal 9.0.0. There will be no replacement for it because the
* default table mapping is now able to be initialized on its own.
*/ */
public function setExtraColumns($table_name, array $column_names) { public function setExtraColumns($table_name, array $column_names) {
$this->extraColumns[$table_name] = $column_names; $this->extraColumns[$table_name] = $column_names;
......
...@@ -307,102 +307,12 @@ public function getTableMapping(array $storage_definitions = NULL) { ...@@ -307,102 +307,12 @@ public function getTableMapping(array $storage_definitions = NULL) {
// case, we can statically cache the computed table mapping. If a new set // case, we can statically cache the computed table mapping. If a new set
// of field storage definitions is passed, for instance when comparing old // of field storage definitions is passed, for instance when comparing old
// and new storage schema, we compute the table mapping without caching. // and new storage schema, we compute the table mapping without caching.
// @todo Clean-up this in https://www.drupal.org/node/2274017 so we can
// easily instantiate a new table mapping whenever needed.
if (!isset($this->tableMapping) || $storage_definitions) { if (!isset($this->tableMapping) || $storage_definitions) {
$table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class; $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class;
$definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId); $definitions = $storage_definitions ?: $this->entityManager->getFieldStorageDefinitions($this->entityTypeId);
/** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
$table_mapping = new $table_mapping_class($this->entityType, $definitions);
$shared_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) {
return $table_mapping->allowsSharedTableStorage($definition);
});
$key_fields = array_values(array_filter([$this->idKey, $this->revisionKey, $this->bundleKey, $this->uuidKey, $this->langcodeKey]));
$all_fields = array_keys($shared_table_definitions);
$revisionable_fields = array_keys(array_filter($shared_table_definitions, function (FieldStorageDefinitionInterface $definition) {
return $definition->isRevisionable();
}));
// Make sure the key fields come first in the list of fields.
$all_fields = array_merge($key_fields, array_diff($all_fields, $key_fields));
// If the entity is revisionable, gather the fields that need to be put
// in the revision table.
$revisionable = $this->entityType->isRevisionable();
$revision_metadata_fields = $revisionable ? array_values($this->entityType->getRevisionMetadataKeys()) : [];
$translatable = $this->entityType->isTranslatable();
if (!$revisionable && !$translatable) {
// The base layout stores all the base field values in the base table.
$table_mapping->setFieldNames($this->baseTable, $all_fields);
}
elseif ($revisionable && !$translatable) {
// The revisionable layout stores all the base field values in the base
// table, except for revision metadata fields. Revisionable fields
// denormalized in the base table but also stored in the revision table
// together with the entity ID and the revision ID as identifiers.
$table_mapping->setFieldNames($this->baseTable, array_diff($all_fields, $revision_metadata_fields));
$revision_key_fields = [$this->idKey, $this->revisionKey];
$table_mapping->setFieldNames($this->revisionTable, array_merge($revision_key_fields, $revisionable_fields));
}
elseif (!$revisionable && $translatable) {
// Multilingual layouts store key field values in the base table. The
// other base field values are stored in the data table, no matter
// whether they are translatable or not. The data table holds also a
// denormalized copy of the bundle field value to allow for more
// performant queries. This means that only the UUID is not stored on
// the data table.
$table_mapping
->setFieldNames($this->baseTable, $key_fields)
->setFieldNames($this->dataTable, array_values(array_diff($all_fields, [$this->uuidKey])));
}
elseif ($revisionable && $translatable) {
// The revisionable multilingual layout stores key field values in the
// base table, except for language, which is stored in the revision
// table along with revision metadata. The revision data table holds
// data field values for all the revisionable fields and the data table
// holds the data field values for all non-revisionable fields. The data
// field values of revisionable fields are denormalized in the data
// table, as well.
$table_mapping->setFieldNames($this->baseTable, array_values($key_fields));
// Like in the multilingual, non-revisionable case the UUID is not
// in the data table. Additionally, do not store revision metadata
// fields in the data table.
$data_fields = array_values(array_diff($all_fields, [$this->uuidKey], $revision_metadata_fields));
$table_mapping->setFieldNames($this->dataTable, $data_fields);
$revision_base_fields = array_merge([$this->idKey, $this->revisionKey, $this->langcodeKey], $revision_metadata_fields);
$table_mapping->setFieldNames($this->revisionTable, $revision_base_fields);
$revision_data_key_fields = [$this->idKey, $this->revisionKey, $this->langcodeKey];
$revision_data_fields = array_diff($revisionable_fields, $revision_metadata_fields, [$this->langcodeKey]);
$table_mapping->setFieldNames($this->revisionDataTable, array_merge($revision_data_key_fields, $revision_data_fields));
}
// Add dedicated tables. /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */
$dedicated_table_definitions = array_filter($definitions, function (FieldStorageDefinitionInterface $definition) use ($table_mapping) { $table_mapping = $table_mapping_class::create($this->entityType, $definitions);
return $table_mapping->requiresDedicatedTableStorage($definition);
});
$extra_columns = [
'bundle',
'deleted',
'entity_id',
'revision_id',
'langcode',
'delta',
];
foreach ($dedicated_table_definitions as $field_name => $definition) {
$tables = [$table_mapping->getDedicatedDataTableName($definition)];
if ($revisionable && $definition->isRevisionable()) {
$tables[] = $table_mapping->getDedicatedRevisionTableName($definition);
}
foreach ($tables as $table_name) {
$table_mapping->setFieldNames($table_name, [$field_name]);
$table_mapping->setExtraColumns($table_name, $extra_columns);
}
}
// Cache the computed table mapping only if we are using our internal // Cache the computed table mapping only if we are using our internal
// storage definitions. // storage definitions.
......
...@@ -362,38 +362,35 @@ public function testGetFieldTableName($table_names, $expected) { ...@@ -362,38 +362,35 @@ public function testGetFieldTableName($table_names, $expected) {
->method('getColumns') ->method('getColumns')
->willReturn($columns); ->willReturn($columns);
$storage = $this->getMockBuilder('\Drupal\Core\Entity\Sql\SqlContentEntityStorage') $this->entityType
->disableOriginalConstructor()
->getMock();
$storage
->expects($this->any()) ->expects($this->any())
->method('getBaseTable') ->method('getBaseTable')
->willReturn(isset($table_names['base']) ? $table_names['base'] : 'base_table'); ->willReturn(isset($table_names['base']) ? $table_names['base'] : 'entity_test');
$storage $this->entityType
->expects($this->any()) ->expects($this->any())
->method('getDataTable') ->method('getDataTable')
->willReturn(isset($table_names['data']) ? $table_names['data'] : NULL); ->willReturn(isset($table_names['data']) ? $table_names['data'] : FALSE);
$storage $this->entityType
->expects($this->any()) ->expects($this->any())
->method('getRevisionTable') ->method('getRevisionTable')
->willReturn(isset($table_names['revision']) ? $table_names['revision'] : NULL); ->willReturn(isset($table_names['revision']) ? $table_names['revision'] : FALSE);
$entity_manager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface'); $this->entityType
$entity_manager
->expects($this->any()) ->expects($this->any())
->method('getStorage') ->method('isTranslatable')
->willReturn($storage); ->willReturn(isset($table_names['data']));
$container = $this->getMock('\Symfony\Component\DependencyInjection\ContainerInterface'); $this->entityType
$container
->expects($this->any()) ->expects($this->any())
->method('get') ->method('isRevisionable')
->willReturn($entity_manager); ->willReturn(isset($table_names['revision']));
\Drupal::setContainer($container); $this->entityType
->expects($this->any())
->method('getRevisionMetadataKeys')
->willReturn([]);
$table_mapping = new DefaultTableMapping($this->entityType, [$field_name => $definition]); $table_mapping = new DefaultTableMapping($this->entityType, [$field_name => $definition]);
......
...@@ -389,13 +389,22 @@ public function testGetSchemaBase() { ...@@ -389,13 +389,22 @@ public function testGetSchemaBase() {
* @covers ::processIdentifierSchema * @covers ::processIdentifierSchema
*/ */
public function testGetSchemaRevisionable() { public function testGetSchemaRevisionable() {
$this->entityType = new ContentEntityType([ $this->entityType = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityType')
'id' => 'entity_test', ->setConstructorArgs([
'entity_keys' => [ [
'id' => 'id', 'id' => 'entity_test',
'revision' => 'revision_id', 'entity_keys' => [
], 'id' => 'id',
]); 'revision' => 'revision_id',
],
]
])
->setMethods(['getRevisionMetadataKeys'])
->getMock();
$this->entityType->expects($this->any())
->method('getRevisionMetadataKeys')
->will($this->returnValue([]));
$this->storage->expects($this->exactly(2)) $this->storage->expects($this->exactly(2))
->method('getRevisionTable') ->method('getRevisionTable')
...@@ -595,14 +604,23 @@ public function testGetSchemaTranslatable() { ...@@ -595,14 +604,23 @@ public function testGetSchemaTranslatable() {
* @covers ::processRevisionDataTable * @covers ::processRevisionDataTable
*/ */
public function testGetSchemaRevisionableTranslatable() { public function testGetSchemaRevisionableTranslatable() {
$this->entityType = new ContentEntityType([ $this->entityType = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityType')
'id' => 'entity_test', ->setConstructorArgs([
'entity_keys' => [ [
'id' => 'id', 'id' => 'entity_test',
'revision' => 'revision_id', 'entity_keys' => [
'langcode' => 'langcode', 'id' => 'id',
], 'revision' => 'revision_id',
]); 'langcode' => 'langcode',
],
]
])
->setMethods(['getRevisionMetadataKeys'])
->getMock();
$this->entityType->expects($this->any())
->method('getRevisionMetadataKeys')
->will($this->returnValue([]));
$this->storage->expects($this->exactly(3)) $this->storage->expects($this->exactly(3))
->method('getRevisionTable') ->method('getRevisionTable')
......
...@@ -205,6 +205,9 @@ public function testGetRevisionTable($revision_table, $expected) { ...@@ -205,6 +205,9 @@ public function testGetRevisionTable($revision_table, $expected) {
$this->entityType->expects($this->once()) $this->entityType->expects($this->once())
->method('getRevisionTable') ->method('getRevisionTable')
->will($this->returnValue($revision_table)); ->will($this->returnValue($revision_table));
$this->entityType->expects($this->any())
->method('getRevisionMetadataKeys')
->willReturn([]);
$this->setUpEntityStorage(); $this->setUpEntityStorage();
...@@ -243,6 +246,9 @@ public function testGetDataTable() { ...@@ -243,6 +246,9 @@ public function testGetDataTable() {
$this->entityType->expects($this->exactly(1)) $this->entityType->expects($this->exactly(1))
->method('getDataTable') ->method('getDataTable')
->will($this->returnValue('entity_test_field_data')); ->will($this->returnValue('entity_test_field_data'));
$this->entityType->expects($this->any())
->method('getRevisionMetadataKeys')
->willReturn([]);
$this->setUpEntityStorage(); $this->setUpEntityStorage();
...@@ -276,6 +282,9 @@ public function testGetRevisionDataTable($revision_data_table, $expected) { ...@@ -276,6 +282,9 @@ public function testGetRevisionDataTable($revision_data_table, $expected) {
$this->entityType->expects($this->once()) $this->entityType->expects($this->once())
->method('getRevisionDataTable') ->method('getRevisionDataTable')
->will($this->returnValue($revision_data_table)); ->will($this->returnValue($revision_data_table));
$this->entityType->expects($this->any())
->method('getRevisionMetadataKeys')
->willReturn([]);
$this->setUpEntityStorage(); $this->setUpEntityStorage();
...@@ -533,7 +542,7 @@ public function testGetTableMappingRevisionable(array $entity_keys) { ...@@ -533,7 +542,7 @@ public function testGetTableMappingRevisionable(array $entity_keys) {
'uuid' => $entity_keys['uuid'], 'uuid' => $entity_keys['uuid'],
]; ];
$this->entityType->expects($this->exactly(2)) $this->entityType->expects($this->exactly(4))
->method('isRevisionable') ->method('isRevisionable')
->will($this->returnValue(TRUE)); ->will($this->returnValue(TRUE));
$this->entityType->expects($this->any()) $this->entityType->expects($this->any())
...@@ -606,7 +615,7 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) { ...@@ -606,7 +615,7 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) {
$field_names = array_merge($field_names, $revisionable_field_names); $field_names = array_merge($field_names, $revisionable_field_names);
$this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, array_values($revision_metadata_field_names)), ['isRevisionable' => TRUE]); $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, array_values($revision_metadata_field_names)), ['isRevisionable' => TRUE]);
$this->entityType->expects($this->exactly(2)) $this->entityType->expects($this->exactly(4))
->method('isRevisionable') ->method('isRevisionable')
->will($this->returnValue(TRUE)); ->will($this->returnValue(TRUE));
$this->entityType->expects($this->any()) $this->entityType->expects($this->any())
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment