Commit 5c3b66c8 authored by alexpott's avatar alexpott
Browse files

Issue #2330121 by plach, effulgentsia, fago: Replace...

Issue #2330121 by plach, effulgentsia, fago: Replace ContentEntityDatabaseStorage::getSchema() with a new EntityTypeListenerInterface implemented by SqlContentEntityStorageSchema.
parent c08a915d
......@@ -34,7 +34,7 @@
*
* @ingroup entity_api
*/
class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface {
class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, EntityTypeListenerInterface {
/**
* The mapping of field columns to SQL tables.
......@@ -217,13 +217,6 @@ public function getRevisionDataTable() {
return $this->revisionDataTable;
}
/**
* {@inheritdoc}
*/
public function getSchema() {
return $this->schemaHandler()->getSchema();
}
/**
* Gets the schema handler for this entity storage.
*
......@@ -233,7 +226,7 @@ public function getSchema() {
protected function schemaHandler() {
if (!isset($this->schemaHandler)) {
$schema_handler_class = $this->entityType->getHandlerClass('storage_schema') ?: 'Drupal\Core\Entity\Schema\SqlContentEntityStorageSchema';
$this->schemaHandler = new $schema_handler_class($this->entityManager, $this->entityType, $this);
$this->schemaHandler = new $schema_handler_class($this->entityManager, $this->entityType, $this, $this->database);
}
return $this->schemaHandler;
}
......@@ -1365,6 +1358,27 @@ protected function usesDedicatedTable(FieldStorageDefinitionInterface $definitio
return $definition->getProvider() != $this->entityType->getProvider() && !$definition->hasCustomStorage();
}
/**
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
$this->schemaHandler()->onEntityTypeCreate($entity_type);
}
/**
* {@inheritdoc}
*/
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
$this->schemaHandler()->onEntityTypeUpdate($entity_type, $original);
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
$this->schemaHandler()->onEntityTypeDelete($entity_type);
}
/**
* {@inheritdoc}
*/
......
......@@ -957,6 +957,42 @@ public function getEntityTypeFromClass($class_name) {
throw new NoCorrespondingEntityClassException($class_name);
}
/**
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
// @todo Forward this to all interested handlers, not only storage, once
// iterating handlers is possible: https://www.drupal.org/node/2332857.
$storage = $this->getStorage($entity_type->id());
if ($storage instanceof EntityTypeListenerInterface) {
$storage->onEntityTypeCreate($entity_type);
}
}
/**
* {@inheritdoc}
*/
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
// @todo Forward this to all interested handlers, not only storage, once
// iterating handlers is possible: https://www.drupal.org/node/2332857.
$storage = $this->getStorage($entity_type->id());
if ($storage instanceof EntityTypeListenerInterface) {
$storage->onEntityTypeUpdate($entity_type, $original);
}
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
// @todo Forward this to all interested handlers, not only storage, once
// iterating handlers is possible: https://www.drupal.org/node/2332857.
$storage = $this->getStorage($entity_type->id());
if ($storage instanceof EntityTypeListenerInterface) {
$storage->onEntityTypeDelete($entity_type);
}
}
/**
* Acts on entity bundle rename.
*
......
......@@ -12,7 +12,7 @@
/**
* Provides an interface for entity type managers.
*/
interface EntityManagerInterface extends PluginManagerInterface {
interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface {
/**
* Builds a list of entity type labels suitable for a Form API options list.
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\EntityTypeListenerInterface.
*/
namespace Drupal\Core\Entity;
/**
* Defines an interface for reacting to entity type creation, deletion, and updates.
*
* @todo Convert to Symfony events: https://www.drupal.org/node/2332935
*/
interface EntityTypeListenerInterface {
/**
* Reacts to the creation of the entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type being created.
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type);
/**
* Reacts to the update of the entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The updated entity type definition.
* @param \Drupal\Core\Entity\EntityTypeInterface $original
* The original entity type definition.
*/
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original);
/**
* Reacts to the deletion of the entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type being deleted.
*/
public function onEntityTypeDelete(EntityTypeInterface $entity_type);
}
......@@ -6,9 +6,10 @@
*/
namespace Drupal\Core\Entity\Schema;
use Drupal\Core\Entity\EntityTypeListenerInterface;
/**
* Defines an interface for handling the storage schema of entities.
*/
interface EntitySchemaHandlerInterface extends EntitySchemaProviderInterface {
interface EntitySchemaHandlerInterface extends EntityTypeListenerInterface {
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\Schema\EntitySchemaProviderInterface.
*/
namespace Drupal\Core\Entity\Schema;
/**
* Defines a common interface to return the storage schema for entities.
*/
interface EntitySchemaProviderInterface {
/**
* Gets the full schema array for a given entity type.
*
* @return array
* A schema array for the entity type's tables.
*/
public function getSchema();
}
......@@ -7,9 +7,11 @@
namespace Drupal\Core\Entity\Schema;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityDatabaseStorage;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
/**
* Defines a schema handler that supports revisionable, translatable entities.
......@@ -44,6 +46,13 @@ class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
*/
protected $schema;
/**
* The database connection to be used.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Constructs a SqlContentEntityStorageSchema.
*
......@@ -53,18 +62,52 @@ class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface {
* The entity type.
* @param \Drupal\Core\Entity\ContentEntityDatabaseStorage $storage
* The storage of the entity type. This must be an SQL-based storage.
* @param \Drupal\Core\Database\Connection $database
* The database connection to be used.
*/
public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage) {
public function __construct(EntityManagerInterface $entity_manager, ContentEntityTypeInterface $entity_type, ContentEntityDatabaseStorage $storage, Connection $database) {
$this->entityType = $entity_type;
$this->fieldStorageDefinitions = $entity_manager->getFieldStorageDefinitions($entity_type->id());
$this->storage = $storage;
$this->database = $database;
}
/**
* {@inheritdoc}
*/
public function getSchema() {
return $this->getEntitySchema($this->entityType);
public function onEntityTypeCreate(EntityTypeInterface $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);
}
}
}
/**
* {@inheritdoc}
*/
public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
// @todo Implement proper updates: https://www.drupal.org/node/1498720.
// Meanwhile, treat a change from non-SQL storage to SQL storage as
// identical to creation with respect to SQL schema handling.
if (!is_subclass_of($original->getStorageClass(), '\Drupal\Core\Entity\Sql\SqlEntityStorageInterface')) {
$this->onEntityTypeCreate($entity_type);
}
}
/**
* {@inheritdoc}
*/
public function onEntityTypeDelete(EntityTypeInterface $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->dropTable($table_name);
}
}
}
/**
......
......@@ -8,12 +8,11 @@
namespace Drupal\Core\Entity\Sql;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
/**
* A common interface for SQL-based entity storage implementations.
*/
interface SqlEntityStorageInterface extends EntityStorageInterface, EntitySchemaProviderInterface {
interface SqlEntityStorageInterface extends EntityStorageInterface {
/**
* Gets a table mapping for the entity's SQL tables.
......
......@@ -12,7 +12,6 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\String;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -845,19 +844,14 @@ public function install(array $module_list, $enable_dependencies = TRUE) {
}
drupal_set_installed_schema_version($module, $version);
// Install any entity schemas belonging to the module.
// Notify the entity manager that this module's entity types are new,
// so that it can notify all interested handlers. For example, a
// SQL-based storage handler can use this as an opportunity to create
// the necessary database tables.
$entity_manager = \Drupal::entityManager();
$schema = \Drupal::database()->schema();
foreach ($entity_manager->getDefinitions() as $entity_type) {
if ($entity_type->getProvider() == $module) {
$storage = $entity_manager->getStorage($entity_type->id());
if ($storage instanceof EntitySchemaProviderInterface) {
foreach ($storage->getSchema() as $table_name => $table_schema) {
if (!$schema->tableExists($table_name)) {
$schema->createTable($table_name, $table_schema);
}
}
}
$entity_manager->onEntityTypeCreate($entity_type);
}
}
......@@ -965,19 +959,13 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) {
// Remove all configuration belonging to the module.
\Drupal::service('config.manager')->uninstall('module', $module);
// Remove any entity schemas belonging to the module.
$schema = \Drupal::database()->schema();
// Notify the entity manager that this module's entity types are being
// deleted, so that it can notify all interested handlers. For example,
// a SQL-based storage handler can use this as an opportunity to drop
// the corresponding database tables.
foreach ($entity_manager->getDefinitions() as $entity_type) {
if ($entity_type->getProvider() == $module) {
$storage = $entity_manager->getStorage($entity_type->id());
if ($storage instanceof EntitySchemaProviderInterface) {
foreach ($storage->getSchema() as $table_name => $table_schema) {
if ($schema->tableExists($table_name)) {
$schema->dropTable($table_name);
}
}
}
$entity_manager->onEntityTypeDelete($entity_type);
}
}
......
......@@ -9,16 +9,15 @@
* Implements hook_install().
*/
function contact_storage_test_install() {
// ModuleHandler won't create the schema automatically because Message entity
// belongs to contact.module.
// @todo Remove this when https://www.drupal.org/node/1498720 is in.
$entity_manager = \Drupal::entityManager();
$schema = \Drupal::database()->schema();
$entity_type = $entity_manager->getDefinition('contact_message');
$storage = $entity_manager->getStorage($entity_type->id());
foreach ($storage->getSchema() as $table_name => $table_schema) {
if (!$schema->tableExists($table_name)) {
$schema->createTable($table_name, $table_schema);
}
}
// Recreate the original entity type definition, in order to notify the
// manager of what changed. The change of storage backend will trigger
// schema installation.
// @see contact_storage_test_entity_type_alter()
$original = clone $entity_type;
$original->setStorageClass('Drupal\Core\Entity\ContentEntityNullStorage');
$entity_manager->onEntityTypeUpdate($entity_type, $original);
}
......@@ -11,10 +11,10 @@
use Drupal\Core\Database\Database;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\Core\Language\Language;
use Drupal\Core\Site\Settings;
use Drupal\Core\Entity\Schema\EntitySchemaProviderInterface;
use Symfony\Component\DependencyInjection\Parameter;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
......@@ -377,36 +377,39 @@ protected function installSchema($module, $tables) {
/**
* Installs the tables for a specific entity type.
* Installs the storage schema for a specific entity type.
*
* @param string $entity_type_id
* The ID of the entity type.
*
* @throws \RuntimeException
* Thrown when the entity type does not support automatic schema installation.
*/
protected function installEntitySchema($entity_type_id) {
/** @var \Drupal\Core\Entity\EntityManagerInterface $entity_manager */
$entity_manager = $this->container->get('entity.manager');
/** @var \Drupal\Core\Database\Schema $schema_handler */
$schema_handler = $this->container->get('database')->schema();
$entity_type = $entity_manager->getDefinition($entity_type_id);
$entity_manager->onEntityTypeCreate($entity_type);
// For test runs, the most common storage backend is a SQL database. For
// this case, ensure the tables got created.
$storage = $entity_manager->getStorage($entity_type_id);
if ($storage instanceof EntitySchemaProviderInterface) {
$schema = $storage->getSchema();
foreach ($schema as $table_name => $table_schema) {
$schema_handler->createTable($table_name, $table_schema);
if ($storage instanceof SqlEntityStorageInterface) {
$tables = $storage->getTableMapping()->getTableNames();
$db_schema = $this->container->get('database')->schema();
$all_tables_exist = TRUE;
foreach ($tables as $table) {
if (!$db_schema->tableExists($table)) {
$this->fail(String::format('Installed entity type table for the %entity_type entity type: %table', array(
'%entity_type' => $entity_type_id,
'%table' => $table,
)));
$all_tables_exist = FALSE;
}
}
if ($all_tables_exist) {
$this->pass(String::format('Installed entity type tables for the %entity_type entity type: %tables', array(
'%entity_type' => $entity_type_id,
'%tables' => '{' . implode('}, {', $tables) . '}',
)));
}
$this->pass(String::format('Installed entity type tables for the %entity_type entity type: %tables', array(
'%entity_type' => $entity_type_id,
'%tables' => '{' . implode('}, {', array_keys($schema)) . '}',
)));
}
else {
throw new \RuntimeException(String::format('Entity type %entity_type does not support automatic schema installation.', array(
'%entity-type' => $entity_type_id,
)));
}
}
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Entity\ContentEntityDatabaseStorage;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\Schema\SqlContentEntityStorageSchema;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Language\Language;
use Drupal\Tests\UnitTestCase;
......@@ -266,21 +267,23 @@ public function providerTestGetRevisionDataTable() {
}
/**
* Tests ContentEntityDatabaseStorage::getSchema().
* Tests ContentEntityDatabaseStorage::onEntityTypeCreate().
*
* @covers ::__construct()
* @covers ::getSchema()
* @covers ::schemaHandler()
* @covers ::onEntityTypeCreate()
* @covers ::getTableMapping()
*/
public function testGetSchema() {
public function testOnEntityTypeCreate() {
$columns = array(
'value' => array(
'type' => 'int',
),
);
$this->fieldDefinitions['id'] = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$this->fieldDefinitions['id'] = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');
$this->fieldDefinitions['id']->expects($this->any())
->method('getName')
->will($this->returnValue('id'));
$this->fieldDefinitions['id']->expects($this->once())
->method('getColumns')
->will($this->returnValue($columns));
......@@ -305,35 +308,47 @@ public function testGetSchema() {
array('id' => 'id'),
)));
$this->entityManager->expects($this->once())
->method('getFieldStorageDefinitions')
->with($this->entityType->id())
->will($this->returnValue($this->fieldDefinitions));
$this->setUpEntityStorage();
$expected = array(
'entity_test' => array(
'description' => 'The base table for entity_test entities.',
'fields' => array(
'id' => array(
'type' => 'serial',
'description' => NULL,
'not null' => TRUE,
),
'description' => 'The base table for entity_test entities.',
'fields' => array(
'id' => array(
'type' => 'serial',
'description' => NULL,
'not null' => TRUE,
),
'primary key' => array('id'),
'unique keys' => array(),
'indexes' => array(),
'foreign keys' => array(),
),
'primary key' => array('id'),
'unique keys' => array(),
'indexes' => array(),
'foreign keys' => array(),
);
$this->assertEquals($expected, $this->entityStorage->getSchema());
// Test that repeated calls do not result in repeatedly instantiating
// SqlContentEntityStorageSchema as getFieldStorageDefinitions() is only
// expected to be called once.
$this->assertEquals($expected, $this->entityStorage->getSchema());
$schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema')
->disableOriginalConstructor()
->getMock();
$schema_handler->expects($this->any())
->method('createTable')
->with($this->equalTo('entity_test'), $this->equalTo($expected));
$this->connection->expects($this->once())
->method('schema')
->will($this->returnValue($schema_handler));
$storage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage')
->setConstructorArgs(array($this->entityType, $this->connection, $this->entityManager, $this->cache))
->setMethods(array('schemaHandler'))
->getMock();
$schema_handler = new SqlContentEntityStorageSchema($this->entityManager, $this->entityType, $storage, $this->connection);
$storage
->expects($this->any())
->method('schemaHandler')
->will($this->returnValue($schema_handler));
$storage->onEntityTypeCreate($this->entityType);
}
/**
......@@ -397,18 +412,7 @@ public function testGetTableMappingSimple(array $entity_keys) {
public function testGetTableMappingSimpleWithFields(array $entity_keys) {
$base_field_names = array('title', 'description', 'owner');
$field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
$definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$this->fieldDefinitions = array_fill_keys($field_names, $definition);
$this->entityType->expects($this->any())
->method('getKey')
->will($this->returnValueMap(array(
array('id', $entity_keys['id']),
array('uuid', $entity_keys['uuid']),
array('bundle', $entity_keys['bundle']),
)));
$this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
$this->setUpEntityStorage();
$mapping = $this->entityStorage->getTableMapping();
......@@ -533,25 +537,11 @@ public function testGetTableMappingRevisionableWithFields(array $entity_keys) {
$base_field_names = array('title');
$field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
$definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
$this->fieldDefinitions = array_fill_keys($field_names, $definition);
$this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
$revisionable_field_names = array('description', 'owner');
$definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface');
// isRevisionable() is only called once, but we re-use the same definition
// for all revisionable fields.
$definition->expects($this->any())
->method('isRevisionable')
->will($this->returnValue(TRUE));
$field_names = array_merge(
$field_names,
$revisionable_field_names
);