diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php index 14374a5e3f1cf00eba98c8d93edec65d36ca9c0e..879762de81b472ff7bd98743b1f148777e6274c7 100644 --- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php @@ -24,6 +24,35 @@ class DefaultTableMapping implements TableMappingInterface { */ 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. * @@ -87,6 +116,132 @@ class DefaultTableMapping implements TableMappingInterface { public function __construct(ContentEntityTypeInterface $entity_type, array $storage_definitions) { $this->entityType = $entity_type; $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) { // where field data is stored, otherwise the base table is responsible for // storing field data. Revision metadata is an exception as it's stored // 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]; - $table_names = [ - $storage->getDataTable(), - $storage->getBaseTable(), - $storage->getRevisionTable(), + $table_names = array_filter([ + $this->dataTable, + $this->baseTable, + $this->revisionTable, $this->getDedicatedDataTableName($storage_definition), - ]; + ]); // Collect field columns. $field_columns = []; @@ -161,7 +312,7 @@ public function getFieldTableName($field_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); // We assume finding one field column belonging to the mapping is enough // to identify the field table. @@ -227,6 +378,10 @@ public function getFieldColumnName(FieldStorageDefinitionInterface $storage_defi * A list of field names to add the columns for. * * @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) { $this->fieldNames[$table_name] = $field_names; @@ -254,6 +409,10 @@ public function getExtraColumns($table_name) { * The list of column names. * * @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) { $this->extraColumns[$table_name] = $column_names; diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index f235ecdad0865afe185ec187470b300d173688a5..3fefd5e763fd713235ea50b73748c70297ff91cf 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -307,102 +307,12 @@ public function getTableMapping(array $storage_definitions = NULL) { // case, we can statically cache the computed table mapping. If a new set // of field storage definitions is passed, for instance when comparing old // 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) { $table_mapping_class = $this->temporary ? TemporaryTableMapping::class : DefaultTableMapping::class; $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. - $dedicated_table_definitions = array_filter($definitions, 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); - } - } + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping|\Drupal\Core\Entity\Sql\TemporaryTableMapping $table_mapping */ + $table_mapping = $table_mapping_class::create($this->entityType, $definitions); // Cache the computed table mapping only if we are using our internal // storage definitions. diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php index 39c9a053c845e884dd8bbd728fb2deed3f28a9d2..e9a306dd4cdc639a869f7e9ccd257409296b2453 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/DefaultTableMappingTest.php @@ -362,38 +362,35 @@ public function testGetFieldTableName($table_names, $expected) { ->method('getColumns') ->willReturn($columns); - $storage = $this->getMockBuilder('\Drupal\Core\Entity\Sql\SqlContentEntityStorage') - ->disableOriginalConstructor() - ->getMock(); - - $storage + $this->entityType ->expects($this->any()) ->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()) ->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()) ->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'); - $entity_manager + $this->entityType ->expects($this->any()) - ->method('getStorage') - ->willReturn($storage); + ->method('isTranslatable') + ->willReturn(isset($table_names['data'])); - $container = $this->getMock('\Symfony\Component\DependencyInjection\ContainerInterface'); - $container + $this->entityType ->expects($this->any()) - ->method('get') - ->willReturn($entity_manager); + ->method('isRevisionable') + ->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]); diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php index d16ece2097dd72e784fcf054f645e7eb53f97c10..5643b15ca3e6465308b1fa4f53cbc2c56c483176 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php @@ -389,13 +389,22 @@ public function testGetSchemaBase() { * @covers ::processIdentifierSchema */ public function testGetSchemaRevisionable() { - $this->entityType = new ContentEntityType([ - 'id' => 'entity_test', - 'entity_keys' => [ - 'id' => 'id', - 'revision' => 'revision_id', - ], - ]); + $this->entityType = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityType') + ->setConstructorArgs([ + [ + 'id' => 'entity_test', + '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)) ->method('getRevisionTable') @@ -595,14 +604,23 @@ public function testGetSchemaTranslatable() { * @covers ::processRevisionDataTable */ public function testGetSchemaRevisionableTranslatable() { - $this->entityType = new ContentEntityType([ - 'id' => 'entity_test', - 'entity_keys' => [ - 'id' => 'id', - 'revision' => 'revision_id', - 'langcode' => 'langcode', - ], - ]); + $this->entityType = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityType') + ->setConstructorArgs([ + [ + 'id' => 'entity_test', + 'entity_keys' => [ + '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)) ->method('getRevisionTable') diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php index bf3fd46eb6a331eba92983524c95fc809eee4103..964020cf5ca8a53c50d652c134eaf3e6b7ead4b4 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php @@ -205,6 +205,9 @@ public function testGetRevisionTable($revision_table, $expected) { $this->entityType->expects($this->once()) ->method('getRevisionTable') ->will($this->returnValue($revision_table)); + $this->entityType->expects($this->any()) + ->method('getRevisionMetadataKeys') + ->willReturn([]); $this->setUpEntityStorage(); @@ -243,6 +246,9 @@ public function testGetDataTable() { $this->entityType->expects($this->exactly(1)) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); + $this->entityType->expects($this->any()) + ->method('getRevisionMetadataKeys') + ->willReturn([]); $this->setUpEntityStorage(); @@ -276,6 +282,9 @@ public function testGetRevisionDataTable($revision_data_table, $expected) { $this->entityType->expects($this->once()) ->method('getRevisionDataTable') ->will($this->returnValue($revision_data_table)); + $this->entityType->expects($this->any()) + ->method('getRevisionMetadataKeys') + ->willReturn([]); $this->setUpEntityStorage(); @@ -533,7 +542,7 @@ public function testGetTableMappingRevisionable(array $entity_keys) { 'uuid' => $entity_keys['uuid'], ]; - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->exactly(4)) ->method('isRevisionable') ->will($this->returnValue(TRUE)); $this->entityType->expects($this->any()) @@ -606,7 +615,7 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) { $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->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->exactly(4)) ->method('isRevisionable') ->will($this->returnValue(TRUE)); $this->entityType->expects($this->any())