diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php index 0be84dd..1f9af77 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php @@ -148,7 +148,8 @@ protected function addFieldSchema(array &$schema, $field_name, array $column_map $schema['fields'][$schema_field_name] = $column_schema; $schema['fields'][$schema_field_name]['description'] = $field_description; // Only entity keys are required. - $schema['fields'][$schema_field_name]['not null'] = (bool) $this->entityType->getKey($field_name); + $keys = $this->entityType->getKeys() + array('langcode' => 'langcode'); + $schema['fields'][$schema_field_name]['not null'] = in_array($field_name, $keys); } if (!empty($field_schema['indexes'])) { @@ -316,12 +317,12 @@ protected function initializeBaseTable() { ); } - if ($this->storage->getTableLayout() & ContentEntityDatabaseStorage::LAYOUT_REVISION) { + if ($this->entityType->hasKey('revision')) { $revision_key = $this->entityType->getKey('revision'); $key_name = $this->getEntityIndexName($revision_key); $schema['unique keys'][$key_name] = array($revision_key); $schema['foreign keys'][$entity_type_id . '__revision'] = array( - 'table' => $this->entityType->getRevisionTable(), + 'table' => $this->storage->getRevisionTable(), 'columns' => array($revision_key => $revision_key), ); } @@ -346,7 +347,7 @@ protected function initializeRevisionTable() { 'indexes' => array(), 'foreign keys' => array( $entity_type_id . '__revisioned' => array( - 'table' => $this->entityType->getBaseTable(), + 'table' => $this->storage->getBaseTable(), 'columns' => array($id_key => $id_key), ), ), @@ -375,13 +376,13 @@ protected function initializeDataTable() { 'indexes' => array(), 'foreign keys' => array( $entity_type_id => array( - 'table' => $this->entityType->getBaseTable(), + 'table' => $this->storage->getBaseTable(), 'columns' => array($id_key => $id_key), ), ), ); - if ($this->storage->getTableLayout() & ContentEntityDatabaseStorage::LAYOUT_REVISION) { + if ($this->entityType->hasKey('revision')) { $key = $this->entityType->getKey('revision'); $schema['indexes'][$this->getEntityIndexName($key)] = array($key); } @@ -408,11 +409,11 @@ protected function initializeRevisionDataTable() { 'indexes' => array(), 'foreign keys' => array( $entity_type_id => array( - 'table' => $this->entityType->getBaseTable(), + 'table' => $this->storage->getBaseTable(), 'columns' => array($id_key => $id_key), ), $entity_type_id . '__revision' => array( - 'table' => $this->entityType->getRevisionTable(), + 'table' => $this->storage->getRevisionTable(), 'columns' => array($revision_key => $revision_key), ) ), diff --git a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php index 258c25a..49ae477 100644 --- a/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php +++ b/core/lib/Drupal/Core/Entity/Sql/DefaultTableMapping.php @@ -41,8 +41,26 @@ class DefaultTableMapping implements TableMappingInterface { protected $extraColumns = array(); /** + * A mapping of column names per field name. + * + * This corresponds to the return value of + * TableMappingInterface::getColumnMapping() except that this variable is + * additionally keyed by field name. + * + * This data is derived from static::$storageDefinitions, but is stored + * separately to avoid repeated processing. + * + * @var array[] + */ + protected $columnMapping = array(); + + /** * A list of all database columns per table. * + * This corresponds to the return value of + * TableMappingInterface::getAllColumns() except that this variable is + * additionally keyed by table name. + * * This data is derived from static::$storageDefinitions, static::$fieldNames, * and static::$extraColumns, but is stored separately to avoid repeated * processing. @@ -99,17 +117,19 @@ public function getFieldNames($table_name) { * {@inheritdoc} */ public function getColumnMapping($field_name) { - $column_names = array_keys($this->storageDefinitions[$field_name]->getColumns()); - if (count($column_names) == 1) { - $mapping = array(reset($column_names) => $field_name); - } - else { - $mapping = array(); - foreach ($column_names as $column_name) { - $mapping[$column_name] = $field_name . '__' . $column_name; + if (!isset($this->columnMapping[$field_name])) { + $column_names = array_keys($this->storageDefinitions[$field_name]->getColumns()); + if (count($column_names) == 1) { + $this->columnMapping[$field_name] = array(reset($column_names) => $field_name); + } + else { + $this->columnMapping[$field_name] = array(); + foreach ($column_names as $column_name) { + $this->columnMapping[$field_name][$column_name] = $field_name . '__' . $column_name; + } } } - return $mapping; + return $this->columnMapping[$field_name]; } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php index 235ecf8..f9b007c 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Schema/ContentEntitySchemaHandlerTest.php @@ -6,7 +6,7 @@ namespace Drupal\Tests\Core\Entity\Schema; -use Drupal\Core\Entity\ContentEntityDatabaseStorage; +use Drupal\Core\Entity\ContentEntityType; use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; use Drupal\Core\Entity\Sql\DefaultTableMapping; use Drupal\Tests\UnitTestCase; @@ -31,7 +31,7 @@ class ContentEntitySchemaHandlerTest extends UnitTestCase { /** * The mocked entity type used in this test. * - * @var \Drupal\Core\Entity\ContentEntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Entity\ContentEntityTypeInterface */ protected $entityType; @@ -72,26 +72,27 @@ public static function getInfo() { */ public function setUp() { $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); - $this->entityType = $this->getMock('Drupal\Core\Entity\ContentEntityTypeInterface'); $this->storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage') ->disableOriginalConstructor() ->getMock(); - // Set up basic expectations that apply to all tests. - $this->entityType->expects($this->any()) - ->method('id') - ->will($this->returnValue('entity_test')); - $this->storage->expects($this->any()) ->method('getBaseTable') ->will($this->returnValue('entity_test')); + + // Add an ID field. This also acts as a test for a simple, single-column + // field. + $this->setUpStorageDefinition('id', array( + 'columns' => array( + 'value' => array( + 'type' => 'int', + ), + ), + )); } /** - * Tests ContentEntitySchemaHandler::getSchema() with a basic table layout. - * - * This tests that the schema is generated correctly for non-revisionable, - * non-translatable entities. + * Tests the schema for non-revisionable, non-translatable entities. * * @param bool $uuid_key * Whether or not the tested entity type should have a UUID key. @@ -112,19 +113,24 @@ public function setUp() { * * @dataProvider providerTestGetSchemaLayoutBase */ - public function testGetSchemaLayoutBase($uuid_key) { - $this->entityType->expects($this->any()) - ->method('getKey') - ->will($this->returnValueMap(array( - array('id', 'id'), - array('uuid', $uuid_key ? 'uuid' : NULL), - ))); - - $this->storage->expects($this->once()) - ->method('getTableLayout') - ->will($this->returnValue(ContentEntityDatabaseStorage::LAYOUT_BASE)); + public function testGetSchemaBase($uuid_key) { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + 'uuid' => $uuid_key ? 'uuid' : NULL, + ), + )); - $this->setUpFieldDefinitions(); + // Add a field with a 'length' constraint. + $this->setUpStorageDefinition('name', array( + 'columns' => array( + 'value' => array( + 'type' => 'varchar', + 'length' => 255, + ), + ), + )); if ($uuid_key) { $this->setUpStorageDefinition('uuid', array( 'columns' => array( @@ -135,6 +141,91 @@ public function testGetSchemaLayoutBase($uuid_key) { ), )); } + // Add a multi-column field. + $this->setUpStorageDefinition('description', array( + 'columns' => array( + 'value' => array( + 'type' => 'text', + 'description' => 'The text value', + ), + 'format' => array( + 'type' => 'varchar', + 'description' => 'The text description', + ), + ), + )); + // Add a field with an index. + $this->setUpStorageDefinition('owner', array( + 'columns' => array( + 'target_id' => array( + 'description' => 'The ID of the target entity.', + 'type' => 'int', + ), + ), + 'indexes' => array( + 'target_id' => array('target_id'), + ), + )); + // Add a field with an index, specified as column name and length. + $this->setUpStorageDefinition('translator', array( + 'columns' => array( + 'target_id' => array( + 'description' => 'The ID of the target entity.', + 'type' => 'int', + ), + ), + 'indexes' => array( + 'target_id' => array(array('target_id', 10)), + ), + )); + // Add a field with a multi-column index. + $this->setUpStorageDefinition('location', array( + 'columns' => array( + 'country' => array( + 'type' => 'varchar', + ), + 'state' => array( + 'type' => 'varchar', + ), + 'city' => array( + 'type' => 'varchar', + ) + ), + 'indexes' => array( + 'country_state_city' => array('country', 'state', array('city', 10)), + ), + )); + // Add a field with a foreign key. + $this->setUpStorageDefinition('editor', array( + 'columns' => array( + 'target_id' => array( + 'type' => 'int', + ), + ), + 'foreign keys' => array( + 'user_id' => array( + 'table' => 'users', + 'columns' => array('target_id' => 'uid'), + ), + ), + )); + // Add a multi-column field with a foreign key. + $this->setUpStorageDefinition('editor_revision', array( + 'columns' => array( + 'target_id' => array( + 'type' => 'int', + ), + 'target_revision_id' => array( + 'type' => 'int', + ), + ), + 'foreign keys' => array( + 'user_id' => array( + 'table' => 'users', + 'columns' => array('target_id' => 'uid'), + ), + ), + )); $this->setUpSchemaHandler(); @@ -233,16 +324,12 @@ public function testGetSchemaLayoutBase($uuid_key) { 'foreign keys' => array( 'entity_test_field__editor__user_id' => array( 'table' => 'users', - 'columns' => array( - 'editor' => 'uid', - ), + 'columns' => array('editor' => 'uid'), ), 'entity_test_field__editor_revision__user_id' => array( 'table' => 'users', - 'columns' => array( - 'editor_revision__target_id' => 'uid', - ) - ) + 'columns' => array('editor_revision__target_id' => 'uid'), + ), ), ), ); @@ -276,115 +363,382 @@ public function providerTestGetSchemaLayoutBase() { } /** - * Sets up the field definitions that are used for the base storage layout. + * Tests the schema for revisionable, non-translatable entities. + * + * @covers ::__construct + * @covers ::getSchema + * @covers ::getTables + * @covers ::initializeBaseTable + * @covers ::initializeRevisionTable + * @covers ::getEntityIndexName + * @covers ::processRevisionTable + * @covers ::processIdentifierSchema */ - protected function setUpFieldDefinitions() { - // Add a single-column field. - $this->setUpStorageDefinition('id', array( + public function testGetSchemaRevisionable() { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + 'revision' => 'revision_id', + ), + )); + + $this->storage->expects($this->exactly(2)) + ->method('getRevisionTable') + ->will($this->returnValue('entity_test_revision')); + + $this->setUpStorageDefinition('revision_id', array( 'columns' => array( 'value' => array( 'type' => 'int', ), ), )); - // Add a field with a 'length' constraint. - $this->setUpStorageDefinition('name', array( - 'columns' => array( - 'value' => array( - 'type' => 'varchar', - 'length' => 255, + + $this->setUpSchemaHandler(); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping->addFieldColumns('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->addFieldColumns('entity_test_revision', array_keys($this->storageDefinitions)); + + $this->storage->expects($this->once()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + 'not null' => TRUE, + ) + ), + 'primary key' => array('id'), + 'indexes' => array(), + 'foreign keys' => array( + 'entity_test__revision' => array( + 'table' => 'entity_test_revision', + 'columns' => array('revision_id' => 'revision_id'), + ) + ), + 'unique keys' => array( + 'entity_test__revision_id' => array('revision_id'), + ), + ), + 'entity_test_revision' => array( + 'description' => 'The revision table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + ), + 'primary key' => array('revision_id'), + 'indexes' => array( + 'entity_test__id' => array('id'), + ), + 'foreign keys' => array( + 'entity_test__revisioned' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), ), ), + ); + + $actual = $this->schemaHandler->getSchema(); + + $this->assertEquals($expected, $actual); + } + + /** + * Tests the schema for non-revisionable, translatable entities. + * + * @covers ::__construct + * @covers ::getSchema + * @covers ::getTables + * @covers ::initializeDataTable + * @covers ::getEntityIndexName + * @covers ::processDataTable + */ + public function testGetSchemaTranslatable() { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + ), )); - // Add a multi-column field. - $this->setUpStorageDefinition('description', array( + + $this->storage->expects($this->once()) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + + $this->setUpStorageDefinition('langcode', array( 'columns' => array( 'value' => array( - 'type' => 'text', - 'description' => 'The text value', - ), - 'format' => array( 'type' => 'varchar', - 'description' => 'The text description', ), ), )); - // Add a field with an index. - $this->setUpStorageDefinition('owner', array( - 'columns' => array( - 'target_id' => array( - 'description' => 'The ID of the target entity.', - 'type' => 'int', + + $this->setUpSchemaHandler(); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping->addFieldColumns('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->addFieldColumns('entity_test_field_data', array_keys($this->storageDefinitions)); + + $this->storage->expects($this->once()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ) ), + 'primary key' => array('id'), + 'indexes' => array(), + 'foreign keys' => array(), ), - 'indexes' => array( - 'target_id' => array('target_id'), + 'entity_test_field_data' => array( + 'description' => 'The data table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), + ), + 'primary key' => array('id', 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + 'entity_test' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), + ), + ), + ); + + $actual = $this->schemaHandler->getSchema(); + + $this->assertEquals($expected, $actual); + } + + /** + * Tests the schema for revisionable, translatable entities. + * + * @covers ::__construct + * @covers ::getSchema + * @covers ::getTables + * @covers ::initializeDataTable + * @covers ::getEntityIndexName + * @covers ::initializeRevisionDataTable + * @covers ::processRevisionDataTable + */ + public function testGetSchemaRevisionableTranslatable() { + $this->entityType = new ContentEntityType(array( + 'id' => 'entity_test', + 'entity_keys' => array( + 'id' => 'id', + 'revision' => 'revision_id', ), )); - // Add a field with an index, specified as column name and length. - $this->setUpStorageDefinition('translator', array( + + $this->storage->expects($this->exactly(3)) + ->method('getRevisionTable') + ->will($this->returnValue('entity_test_revision')); + $this->storage->expects($this->once()) + ->method('getDataTable') + ->will($this->returnValue('entity_test_field_data')); + $this->storage->expects($this->once()) + ->method('getRevisionDataTable') + ->will($this->returnValue('entity_test_revision_field_data')); + + $this->setUpStorageDefinition('revision_id', array( 'columns' => array( - 'target_id' => array( - 'description' => 'The ID of the target entity.', + 'value' => array( 'type' => 'int', ), ), - 'indexes' => array( - 'target_id' => array(array('target_id', 10)), - ), )); - // Add a field with a multi-column index. - $this->setUpStorageDefinition('location', array( + $this->setUpStorageDefinition('langcode', array( 'columns' => array( - 'country' => array( - 'type' => 'varchar', - ), - 'state' => array( + 'value' => array( 'type' => 'varchar', ), - 'city' => array( - 'type' => 'varchar', - ) - ), - 'indexes' => array( - 'country_state_city' => array('country', 'state', array('city', 10)), ), )); - // Add a field with a foreign key. - $this->setUpStorageDefinition('editor', array( - 'columns' => array( - 'target_id' => array( - 'type' => 'int', + + $this->setUpSchemaHandler(); + + $table_mapping = new DefaultTableMapping($this->storageDefinitions); + $table_mapping->addFieldColumns('entity_test', array_keys($this->storageDefinitions)); + $table_mapping->addFieldColumns('entity_test_revision', array_keys($this->storageDefinitions)); + $table_mapping->addFieldColumns('entity_test_field_data', array_keys($this->storageDefinitions)); + $table_mapping->addFieldColumns('entity_test_revision_field_data', array_keys($this->storageDefinitions)); + + $this->storage->expects($this->once()) + ->method('getTableMapping') + ->will($this->returnValue($table_mapping)); + + $expected = array( + 'entity_test' => array( + 'description' => 'The base table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ) + ), + 'primary key' => array('id'), + 'indexes' => array(), + 'unique keys' => array( + 'entity_test__revision_id' => array('revision_id'), + ), + 'foreign keys' => array( + 'entity_test__revision' => array( + 'table' => 'entity_test_revision', + 'columns' => array('revision_id' => 'revision_id'), + ), ), ), - 'foreign keys' => array( - 'user_id' => array( - 'table' => 'users', - 'columns' => array( - 'target_id' => 'uid', + 'entity_test_revision' => array( + 'description' => 'The revision table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'serial', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), + ), + 'primary key' => array('revision_id'), + 'indexes' => array( + 'entity_test__id' => array('id'), + ), + 'foreign keys' => array( + 'entity_test__revisioned' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), ), ), ), - )); - // Add a multi-column field with a foreign key. - $this->setUpStorageDefinition('editor_revision', array( - 'columns' => array( - 'target_id' => array( - 'type' => 'int', + 'entity_test_field_data' => array( + 'description' => 'The data table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), ), - 'target_revision_id' => array( - 'type' => 'int', + 'primary key' => array('id', 'langcode'), + 'indexes' => array( + 'entity_test__revision_id' => array('revision_id'), + ), + 'foreign keys' => array( + 'entity_test' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), ), ), - 'foreign keys' => array( - 'user_id' => array( - 'table' => 'users', - 'columns' => array( - 'target_id' => 'uid', + 'entity_test_revision_field_data' => array( + 'description' => 'The revision data table for entity_test entities.', + 'fields' => array( + 'id' => array( + 'description' => 'The id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'revision_id' => array( + 'description' => 'The revision_id field.', + 'type' => 'int', + 'not null' => TRUE, + ), + 'langcode' => array( + 'description' => 'The langcode field.', + 'type' => 'varchar', + 'not null' => TRUE, + ), + ), + 'primary key' => array('revision_id', 'langcode'), + 'indexes' => array(), + 'foreign keys' => array( + 'entity_test' => array( + 'table' => 'entity_test', + 'columns' => array('id' => 'id'), + ), + 'entity_test__revision' => array( + 'table' => 'entity_test_revision', + 'columns' => array('revision_id' => 'revision_id'), ), ), ), - )); + ); + + $actual = $this->schemaHandler->getSchema(); + + $this->assertEquals($expected, $actual); } /** @@ -392,10 +746,10 @@ protected function setUpFieldDefinitions() { * * This uses the field definitions set in $this->fieldDefinitions. */ - public function setUpSchemaHandler() { + protected function setUpSchemaHandler() { $this->entityManager->expects($this->once()) ->method('getFieldStorageDefinitions') - ->with('entity_test') + ->with($this->entityType->id()) ->will($this->returnValue($this->storageDefinitions)); $this->schemaHandler = new ContentEntitySchemaHandler( $this->entityManager, @@ -415,10 +769,12 @@ public function setUpSchemaHandler() { */ public function setUpStorageDefinition($field_name, array $schema) { $this->storageDefinitions[$field_name] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); - $this->storageDefinitions[$field_name]->expects($this->once()) + // getDescription() is called once for each table. + $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getDescription') ->will($this->returnValue("The $field_name field.")); - $this->storageDefinitions[$field_name]->expects($this->once()) + // getSchema() is called once for each table. + $this->storageDefinitions[$field_name]->expects($this->any()) ->method('getSchema') ->will($this->returnValue($schema)); $this->storageDefinitions[$field_name]->expects($this->once())