diff --git a/core/core.services.yml b/core/core.services.yml index 9922b20..0d7d10a 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -281,13 +281,13 @@ services: arguments: ['@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@router.builder'] entity.manager: class: Drupal\Core\Entity\EntityManager - arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager'] + arguments: ['@container.namespaces', '@module_handler', '@cache.discovery', '@language_manager', '@string_translation', '@class_resolver', '@typed_data_manager', '@state'] parent: container.trait tags: - { name: plugin_manager_cache_clear } - entity.schema.manager: - class: Drupal\Core\Entity\Schema\ContentEntitySchemaManager - arguments: ['@entity.manager', '@state'] + entity.definition_update_manager: + class: Drupal\Core\Entity\EntityDefinitionUpdateManager + arguments: ['@entity.manager'] entity.form_builder: class: Drupal\Core\Entity\EntityFormBuilder arguments: ['@entity.manager', '@form_builder'] diff --git a/core/includes/update.inc b/core/includes/update.inc index c00f591..125907d 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -10,15 +10,8 @@ use Drupal\Component\Graph\Graph; use Drupal\Component\Utility\String; -use Drupal\Core\Config\FileStorage; -use Drupal\Core\Config\ConfigException; -use Drupal\Core\DrupalKernel; use Drupal\Core\Entity\EntityStorageException; -use Drupal\Core\Page\DefaultHtmlPageRenderer; use Drupal\Core\Utility\Error; -use Drupal\Component\Uuid\Uuid; -use Drupal\Component\Utility\NestedArray; -use Symfony\Component\HttpFoundation\Request; /** * Disables any extensions that are incompatible with the current core version. @@ -259,7 +252,7 @@ function update_do_one($module, $number, $dependency_map, &$context) { } /** - * Performs entity schema updates. + * Performs entity definition updates, which can trigger schema updates. * * @param $module * The module whose update will be run. @@ -268,9 +261,9 @@ function update_do_one($module, $number, $dependency_map, &$context) { * @param $context * The batch context array. */ -function update_entity_schema($module, $number, &$context) { +function update_entity_definitions($module, $number, &$context) { try { - \Drupal::service('entity.schema.manager')->applyChanges(); + \Drupal::service('entity.definition_update_manager')->applyUpdates(); } catch (EntityStorageException $e) { watchdog_exception('update', $e); @@ -281,7 +274,7 @@ function update_entity_schema($module, $number, &$context) { // \Drupal\Core\Utility\Error::decodeException(). $ret['#abort'] = array('success' => FALSE, 'query' => t('%type: !message in %function (line %line of %file).', $variables)); $context['results'][$module][$number] = $ret; - $context['results']['#abort'][] = 'update_entity_schema'; + $context['results']['#abort'][] = 'update_entity_definitions'; } } @@ -456,17 +449,15 @@ function update_get_update_function_list($starting_updates) { // Go through each module and find all updates that we need (including the // first update that was requested and any updates that run after it). $update_functions = array(); - if ($starting_updates) { - foreach ($starting_updates as $module => $version) { - $update_functions[$module] = array(); - $updates = drupal_get_schema_versions($module); - if ($updates !== FALSE) { - $max_version = max($updates); - if ($version <= $max_version) { - foreach ($updates as $update) { - if ($update >= $version) { - $update_functions[$module][$update] = $module . '_update_' . $update; - } + foreach ($starting_updates as $module => $version) { + $update_functions[$module] = array(); + $updates = drupal_get_schema_versions($module); + if ($updates !== FALSE) { + $max_version = max($updates); + if ($version <= $max_version) { + foreach ($updates as $update) { + if ($update >= $version) { + $update_functions[$module][$update] = $module . '_update_' . $update; } } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php index 5460436..12aeca3 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityNullStorage.php @@ -20,20 +20,6 @@ class ContentEntityNullStorage extends ContentEntityStorageBase { /** * {@inheritdoc} */ - public function hasData() { - return FALSE; - } - - /** - * {@inheritdoc} - */ - public function countFieldData($storage_definition, $as_bool = FALSE) { - return $as_bool ? FALSE : 0; - } - - /** - * {@inheritdoc} - */ public function loadMultiple(array $ids = NULL) { return array(); } @@ -145,4 +131,11 @@ protected function doSave($id, EntityInterface $entity) { protected function has($id, EntityInterface $entity) { } + /** + * {@inheritdoc} + */ + public function countFieldData($storage_definition, $as_bool = FALSE) { + return $as_bool ? FALSE : 0; + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php new file mode 100644 index 0000000..705faf2 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php @@ -0,0 +1,235 @@ +entityManager = $entity_manager; + } + + /** + * {@inheritdoc} + */ + public function needsUpdates() { + return (bool) $this->getChangeList(); + } + + /** + * {@inheritdoc} + */ + public function getChangeSummary() { + $summary = array(); + + foreach ($this->getChangeList() as $entity_type_id => $change_list) { + // Process entity type definition changes. + if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { + $entity_type = $this->entityManager->getDefinition($entity_type_id); + // @todo Support non-storage-schema-changing definition updates too: + // https://www.drupal.org/node/2336895. + $summary[$entity_type_id][] = $this->t('The %entity_type entity type has definition updates that require schema changes.', array('%entity_type' => $entity_type->getLabel())); + } + + // Process field storage definition changes. + if (!empty($change_list['field_storage_definitions'])) { + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->entityManager->getInstalledFieldStorageDefinitions($entity_type_id); + + foreach ($change_list['field_storage_definitions'] as $field_name => $change) { + switch ($change) { + case static::DEFINITION_CREATED: + $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', array('%field_name' => $storage_definitions[$field_name]->getLabel())); + break; + + case static::DEFINITION_UPDATED: + // @todo Support non-storage-schema-changing definition updates too: + // https://www.drupal.org/node/2336895. + $summary[$entity_type_id][] = $this->t('The %field_name field has storage definition updates that require schema changes.', array('%field_name' => $storage_definitions[$field_name]->getLabel())); + break; + + case static::DEFINITION_DELETED: + $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', array('%field_name' => $original_storage_definitions[$field_name]->getLabel())); + break; + } + } + } + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function applyUpdates() { + foreach ($this->getChangeList() as $entity_type_id => $change_list) { + // Process entity type definition changes. + if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { + $entity_type = $this->entityManager->getDefinition($entity_type_id); + $original = $this->entityManager->getInstalledDefinition($entity_type_id); + $this->entityManager->onEntityTypeUpdate($entity_type, $original); + } + + // Process field storage definition changes. + if (!empty($change_list['field_storage_definitions'])) { + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->entityManager->getInstalledFieldStorageDefinitions($entity_type_id); + + foreach ($change_list['field_storage_definitions'] as $field_name => $change) { + switch ($change) { + case static::DEFINITION_CREATED: + $this->entityManager->onFieldStorageDefinitionCreate($storage_definitions[$field_name]); + break; + + case static::DEFINITION_UPDATED: + $this->entityManager->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]); + break; + + case static::DEFINITION_DELETED: + $this->entityManager->onFieldStorageDefinitionDelete($storage_definitions[$field_name]); + break; + } + } + } + } + } + + /** + * Returns a list of changes to entity type and field storage definitions. + * + * @return array + * An associative array keyed by entity type id of change descriptors. Every + * entry is an associative array with the following optional keys: + * - entity_type: a scalar having only the DEFINITION_UPDATED value. + * - field_storage_definitions: an associative array keyed by field name of + * scalars having one value among: + * - DEFINITION_CREATED + * - DEFINITION_UPDATED + * - DEFINITION_DELETED + */ + protected function getChangeList() { + $this->entityManager->clearCachedDefinitions(); + $change_list = array(); + + foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { + $original = $this->entityManager->getInstalledDefinition($entity_type_id); + // @todo Support non-storage-schema-changing definition updates too: + // https://www.drupal.org/node/2336895. + if ($this->requiresEntityStorageSchemaChanges($entity_type, $original)) { + $change_list[$entity_type_id]['entity_type'] = static::DEFINITION_UPDATED; + } + + if ($entity_type->isFieldable()) { + $field_changes = array(); + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->entityManager->getInstalledFieldStorageDefinitions($entity_type_id); + + // Detect created field storage definitions. + foreach (array_diff_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) { + $field_changes[$field_name] = static::DEFINITION_CREATED; + } + + // Detect deleted field storage definitions. + foreach (array_diff_key($original_storage_definitions, $storage_definitions) as $field_name => $original_storage_definition) { + $field_changes[$field_name] = static::DEFINITION_DELETED; + } + + // Detect updated field storage definitions. + foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) { + // @todo Support non-storage-schema-changing definition updates too: + // https://www.drupal.org/node/2336895. + if ($this->requiresFieldStorageSchemaChanges($storage_definition, $original_storage_definitions[$field_name])) { + $field_changes[$field_name] = static::DEFINITION_UPDATED; + } + } + + if ($field_changes) { + $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes; + } + } + } + + return array_filter($change_list); + } + + /** + * Checks if the changes to the entity type requires storage schema changes. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The updated entity type definition. + * @param \Drupal\Core\Entity\EntityTypeInterface $original + * The original entity type definition. + * + * @return bool + * TRUE if storage schema changes are required, FALSE otherwise. + */ + protected function requiresEntityStorageSchemaChanges($entity_type, $original) { + $storage = $this->entityManager->getStorage($entity_type->id()); + return ($storage instanceof EntityStorageSchemaInterface) && $storage->requiresEntityStorageSchemaChanges($entity_type, $original); + } + + /** + * Checks if the changes to the storage definition requires schema changes. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The updated field storage definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original + * The original field storage definition. + * + * @return bool + * TRUE if storage schema changes are required, FALSE otherwise. + */ + protected function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $storage = $this->entityManager->getStorage($storage_definition->getTargetEntityTypeId()); + return ($storage instanceof FieldableEntityStorageSchemaInterface) && $storage->requiresFieldStorageSchemaChanges($storage_definition, $original); + } + +} diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php new file mode 100644 index 0000000..5c25ec1 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php @@ -0,0 +1,63 @@ +setCacheBackend($cache, 'entity_type', array('entity_types' => TRUE)); @@ -180,6 +194,7 @@ public function __construct(\Traversable $namespaces, ModuleHandlerInterface $mo $this->translationManager = $translation_manager; $this->classResolver = $class_resolver; $this->typedDataManager = $typed_data_manager; + $this->state = $state; } /** @@ -961,16 +976,18 @@ public function getEntityTypeFromClass($class_name) { * {@inheritdoc} */ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + // @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()); + $storage = $this->getStorage($entity_type_id); if ($storage instanceof EntityTypeListenerInterface) { $storage->onEntityTypeCreate($entity_type); } - // Keep the schema manager synchronized with the current state. - if ($entity_type instanceof ContentEntityTypeInterface) { - $this->schemaManager()->onEntityTypeCreate($entity_type); + $this->setInstalledDefinition($entity_type); + if ($entity_type->isFieldable()) { + $this->setInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id)); } } @@ -978,34 +995,83 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { * {@inheritdoc} */ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + $entity_type_id = $entity_type->id(); + // @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()); + $storage = $this->getStorage($entity_type_id); if ($storage instanceof EntityTypeListenerInterface) { $storage->onEntityTypeUpdate($entity_type, $original); } - // Keep the schema manager synchronized with the current state. - if ($entity_type instanceof ContentEntityTypeInterface) { - $this->schemaManager()->onEntityTypeUpdate($entity_type, $original); - } + $this->setInstalledDefinition($entity_type); } /** * {@inheritdoc} */ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + // @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()); + $storage = $this->getStorage($entity_type_id); if ($storage instanceof EntityTypeListenerInterface) { $storage->onEntityTypeDelete($entity_type); } - // Keep the schema manager synchronized with the current state. - if ($entity_type instanceof ContentEntityTypeInterface) { - $this->schemaManager()->onEntityTypeDelete($entity_type); + $this->deleteInstalledDefinition($entity_type_id); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + + // @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 FieldStorageDefinitionListenerInterface) { + $storage->onFieldStorageDefinitionCreate($storage_definition); + } + + $this->setInstalledFieldStorageDefinition($storage_definition); + $this->clearCachedFieldDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + + // @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 FieldStorageDefinitionListenerInterface) { + $storage->onFieldStorageDefinitionUpdate($storage_definition, $original); + } + + $this->setInstalledFieldStorageDefinition($storage_definition); + $this->clearCachedFieldDefinitions(); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + + // @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 FieldStorageDefinitionListenerInterface) { + $storage->onFieldStorageDefinitionDelete($storage_definition); } + + $this->deleteInstalledFieldStorageDefinition($storage_definition); + $this->clearCachedFieldDefinitions(); } /** @@ -1063,12 +1129,80 @@ public function onBundleDelete($entity_type_id, $bundle) { } /** - * Returns the entity schema manager service. + * {@inheritdoc} + */ + public function getInstalledDefinition($entity_type_id) { + return $this->state->get('entity.manager.' . $entity_type_id . '.entity_type'); + } + + /** + * Stores the entity type definition in the application state. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + */ + protected function setInstalledDefinition(EntityTypeInterface $entity_type) { + $entity_type_id = $entity_type->id(); + $this->state->set('entity.manager.' . $entity_type_id . '.entity_type', $entity_type); + } + + /** + * Deletes the entity type definition from the application state. + * + * @param string $entity_type_id + * The entity type definition identifier. + */ + protected function deleteInstalledDefinition($entity_type_id) { + $this->state->delete('entity.manager.' . $entity_type_id . '.entity_type'); + // Clean up field storage definitions as well. Even if the entity type + // isn't currently fieldable, there might be legacy definitions or an + // empty array stored from when it was. + $this->state->delete('entity.manager.' . $entity_type_id . '.field_storage_definitions'); + } + + /** + * {@inheritdoc} + */ + public function getInstalledFieldStorageDefinitions($entity_type_id) { + return $this->state->get('entity.manager.' . $entity_type_id . '.field_storage_definitions'); + } + + /** + * Stores the entity type's field storage definitions in the application state. + * + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + * An array of field storage definitions. + */ + protected function setInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) { + $this->state->set('entity.manager.' . $entity_type_id . '.field_storage_definitions', $storage_definitions); + } + + /** + * Stores the field storage definition in the application state. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + */ + protected function setInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + $definitions = $this->getInstalledFieldStorageDefinitions($entity_type_id); + $definitions[$storage_definition->getName()] = $storage_definition; + $this->setInstalledFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * Deletes the field storage definition from the application state. * - * @return \Drupal\Core\Entity\Schema\ContentEntitySchemaManagerInterface + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. */ - protected function schemaManager() { - return $this->container->get('entity.schema.manager'); + protected function deleteInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + $entity_type_id = $storage_definition->getTargetEntityTypeId(); + $definitions = $this->getInstalledFieldStorageDefinitions($entity_type_id); + unset($definitions[$storage_definition->getName()]); + $this->setInstalledFieldStorageDefinitions($entity_type_id, $definitions); } } diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php index 8d3f9f4..ad1a249 100644 --- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php @@ -8,11 +8,12 @@ namespace Drupal\Core\Entity; use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Field\FieldStorageDefinitionListenerInterface; /** * Provides an interface for entity type managers. */ -interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface { +interface EntityManagerInterface extends PluginManagerInterface, EntityTypeListenerInterface, FieldStorageDefinitionListenerInterface { /** * Builds a list of entity type labels suitable for a Form API options list. @@ -81,6 +82,40 @@ public function getFieldDefinitions($entity_type_id, $bundle); public function getFieldStorageDefinitions($entity_type_id); /** + * Gets the entity type's most recently installed field storage definitions. + * + * During the application lifetime, field storage definitions can change. For + * example, updated code can be deployed. The getFieldStorageDefinitions() + * method will always return the definitions as determined by the current + * codebase. This method, however, returns what the definitions were when the + * last time that one of the + * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface events was last + * fired and completed successfully. In other words, the definitions that + * the entity type's handlers have incorporated into the application state. + * For example, if the entity type's storage handler is SQL-based, the + * definitions for which database tables were created. + * + * Application management code can check if getFieldStorageDefinitions() + * differs from getInstalledFieldStorageDefinitions() and decide whether to: + * - Invoke the appropriate + * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface + * events so that handlers react to the new definitions. + * - Raise a warning that the application state is incompatible with the + * codebase. + * - Perform some other action. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] + * The array of installed field storage definitions for the entity type, + * keyed by field name. + * + * @see \Drupal\Core\Entity\EntityTypeListenerInterface + */ + public function getInstalledFieldStorageDefinitions($entity_type_id); + + /** * Returns a lightweight map of fields across bundles. * * @return array @@ -286,6 +321,38 @@ public function getTranslationFromContext(EntityInterface $entity, $langcode = N public function getDefinition($entity_type_id, $exception_on_invalid = TRUE); /** + * Returns the entity type definition in its most recently installed state. + * + * During the application lifetime, entity type definitions can change. For + * example, updated code can be deployed. The getDefinition() method will + * always return the definition as determined by the current codebase. This + * method, however, returns what the definition was when the last time that + * one of the \Drupal\Core\Entity\EntityTypeListenerInterface events was last + * fired and completed successfully. In other words, the definition that + * the entity type's handlers have incorporated into the application state. + * For example, if the entity type's storage handler is SQL-based, the + * definition for which database tables were created. + * + * Application management code can check if getDefinition() differs from + * getInstalledDefinition() and decide whether to: + * - Invoke the appropriate \Drupal\Core\Entity\EntityTypeListenerInterface + * event so that handlers react to the new definition. + * - Raise a warning that the application state is incompatible with the + * codebase. + * - Perform some other action. + * + * @param string $entity_type_id + * The entity type ID. + * + * @return \Drupal\Core\Entity\EntityTypeInterface|null + * The installed entity type definition, or NULL if the entity type has + * not yet been installed via onEntityTypeCreate(). + * + * @see \Drupal\Core\Entity\EntityTypeListenerInterface + */ + public function getInstalledDefinition($entity_type_id); + + /** * {@inheritdoc} * * @return \Drupal\Core\Entity\EntityTypeInterface[] diff --git a/core/lib/Drupal/Core/Entity/EntityStorageBase.php b/core/lib/Drupal/Core/Entity/EntityStorageBase.php index cca590d..b181bb4 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php @@ -97,16 +97,6 @@ public function getEntityType() { /** * {@inheritdoc} */ - public function hasData() { - return (bool) $this->getQuery() - ->range(0, 1) - ->count() - ->execute(); - } - - /** - * {@inheritdoc} - */ public function loadUnchanged($id) { $this->resetCache(array($id)); return $this->load($id); diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php index 5339fc1..7a329cc 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php @@ -31,14 +31,6 @@ const FIELD_LOAD_REVISION = 'FIELD_LOAD_REVISION'; /** - * Checks whether the storage contains at least one entity. - * - * @return bool - * TRUE if the storage has data, FALSE otherwise. - */ - public function hasData(); - - /** * Resets the internal, static entity cache. * * @param $ids diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php index 2d695c3..aaab687 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php @@ -9,42 +9,9 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\Core\Field\FieldStorageDefinitionListenerInterface; -interface FieldableEntityStorageInterface extends EntityStorageInterface { - - /** - * Reacts to the creation of a field storage definition. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The definition being created. - */ - public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition); - - /** - * Reacts to the update of a field storage definition. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field being updated. - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $original - * The original storage definition; i.e., the definition before the update. - * - * @throws \Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException - * Thrown when the update to the field is forbidden. - */ - public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original); - - /** - * Reacts to the deletion of a field storage definition. - * - * Stored values should not be wiped at once, but marked as 'deleted' so that - * they can go through a proper purge process later on. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field being deleted. - * - * @see purgeFieldData() - */ - public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition); +interface FieldableEntityStorageInterface extends EntityStorageInterface, FieldStorageDefinitionListenerInterface { /** * Reacts to the creation of a field. @@ -59,7 +26,7 @@ public function onFieldDefinitionCreate(FieldDefinitionInterface $field_definiti * * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition * The field definition being updated. - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * @param \Drupal\Core\Field\FieldDefinitionInterface $original * The original field definition; i.e., the definition before the update. */ public function onFieldDefinitionUpdate(FieldDefinitionInterface $field_definition, FieldDefinitionInterface $original); diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php deleted file mode 100644 index d20fb02..0000000 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php +++ /dev/null @@ -1,54 +0,0 @@ -entityManager = $entity_manager; - $this->state = $state; - } - - /** - * {@inheritdoc} - */ - public function onEntityTypeCreate(EntityTypeInterface $definition) { - $entity_type_id = $definition->id(); - // Store the current definitions to be able to track changes. - $this->saveEntityTypeDefinition($definition); - if ($definition->isFieldable()) { - $this->saveFieldStorageDefinitions($entity_type_id, $this->entityManager->getFieldStorageDefinitions($entity_type_id)); - } - } - - /** - * {@inheritdoc} - */ - public function onEntityTypeUpdate(EntityTypeInterface $definition, EntityTypeInterface $original) { - // Store the current definitions to be able to track changes. - $this->saveEntityTypeDefinition($definition); - } - - /** - * {@inheritdoc} - */ - public function onEntityTypeDelete(EntityTypeInterface $definition) { - $entity_type_id = $definition->id(); - // Store the current definitions to be able to track changes. - $this->deleteEntityTypeDefinition($entity_type_id); - // Ensure we delete any data concerning this entity type. It might have - // switched from fieldable to non-fieldable during its life cycle. - $this->deleteFieldStorageDefinitions($entity_type_id); - } - - /** - * {@inheritdoc} - */ - public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $definition) { - $entity_type_id = $definition->getTargetEntityTypeId(); - /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ - $storage = $this->entityManager->getStorage($entity_type_id); - // Notify the storage layer of the new field storage definition. - $storage->onFieldStorageDefinitionCreate($definition); - // Update our field storage definitions. - $definitions = $this->loadFieldStorageDefinitions($entity_type_id); - $definitions[$definition->getName()] = $definition; - $this->saveFieldStorageDefinitions($entity_type_id, $definitions); - } - - /** - * {@inheritdoc} - */ - public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { - $entity_type_id = $definition->getTargetEntityTypeId(); - /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ - $storage = $this->entityManager->getStorage($entity_type_id); - // Notify the storage layer of the new field storage definition. - $storage->onFieldStorageDefinitionUpdate($definition, $original); - // Update our field storage definitions. - $definitions = $this->loadFieldStorageDefinitions($entity_type_id); - $definitions[$definition->getName()] = $definition; - $this->saveFieldStorageDefinitions($entity_type_id, $definitions); - } - - /** - * {@inheritdoc} - */ - public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $definition) { - $entity_type_id = $definition->getTargetEntityTypeId(); - /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ - $storage = $this->entityManager->getStorage($entity_type_id); - // Notify the storage layer of the removed field storage definition. - $storage->onFieldStorageDefinitionDelete($definition); - // Update our field storage definitions. - $definitions = $this->loadFieldStorageDefinitions($entity_type_id); - unset($definitions[$definition->getName()]); - $this->saveFieldStorageDefinitions($entity_type_id, $definitions); - } - - /** - * {@inheritdoc} - */ - public function getChangeList($entity_type_id = NULL) { - $change_list = array(); - $definitions = array_filter($this->entityManager->getDefinitions(), function($definition) { return $definition instanceof ContentEntityTypeInterface; }); - $entity_type_ids = isset($entity_type_id) ? array($entity_type_id) : array_keys($definitions); - - foreach ($entity_type_ids as $entity_type_id) { - $definition = $definitions[$entity_type_id]; - $storage = $this->entityManager->getStorage($entity_type_id); - - if ($definition instanceof ContentEntityTypeInterface && $storage instanceof ContentEntitySchemaProviderInterface) { - // Check whether there are changes in the entity type definition that - // would affect entity schema. - $original = $this->loadEntityTypeDefinition($entity_type_id); - if ($storage->requiresEntitySchemaChanges($definition, $original)) { - $change_list[$entity_type_id]['entity_type'] = static::DEFINITION_UPDATED; - if ($storage->requiresEntityDataMigration($definition, $original)) { - $change_list[$entity_type_id]['data_migration'] = TRUE; - } - } - - // Check whether there are changes in the field storage definitions that - // would affect entity schema. We skip definitions with custom storage - // as they do not affect entity schema. - if ($definition->isFieldable()) { - $field_changes = array(); - $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); - $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); - - // Detect created field storage definitions. - $created = array_filter(array_diff_key($storage_definitions, $original_storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); }); - $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_CREATED; }, $created)); - - // Detect deleted field storage definitions. - $deleted = array_filter(array_diff_key($original_storage_definitions, $storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); }); - $field_changes = array_merge($field_changes, array_map(function() { return static::DEFINITION_DELETED; }, $deleted)); - - // Now compare field storage definitions. - foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $definition) { - /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $definition */ - if (!$definition->hasCustomStorage()) { - $original = $this->loadFieldStorageDefinitions($definition->getTargetEntityTypeId())[$definition->getName()]; - if ($storage->requiresFieldSchemaChanges($original, $definition)) { - $field_changes[$field_name] = static::DEFINITION_UPDATED; - if ($storage->requiresFieldDataMigration($original, $definition)) { - $change_list[$entity_type_id]['data_migration'] = TRUE; - } - } - } - } - - if ($field_changes) { - $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes; - } - } - } - } - - return array_filter($change_list); - } - - /** - * {@inheritdoc} - */ - public function applyChanges($entity_type_id = NULL) { - foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) { - $storage = $this->entityManager->getStorage($entity_type_id); - - try { - $has_data = $storage->hasData(); - } - catch (DatabaseExceptionWrapper $e) { - // The entity schema might be corrupted. In this case it is safer to - // assume there is data available, to avoid performing unrecoverable - // operations. - $has_data = TRUE; - } - - // We do not allow any kind of schema change that would imply a data - // migration. - if (empty($change_list['data_migration']) || !$has_data) { - // Process entity type definition changes. - if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { - /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $definition */ - $definition = $this->entityManager->getDefinition($entity_type_id); - $this->onEntityTypeUpdate($definition, $this->loadEntityTypeDefinition($entity_type_id)); - } - - // Process field storage definition changes. - if (!empty($change_list['field_storage_definitions'])) { - $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); - $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); - - foreach ($change_list['field_storage_definitions'] as $field_name => $change) { - switch ($change) { - case static::DEFINITION_CREATED: - $this->onFieldStorageDefinitionCreate($storage_definitions[$field_name]); - break; - - case static::DEFINITION_UPDATED: - $this->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]); - break; - - case static::DEFINITION_DELETED: - $this->onFieldStorageDefinitionDelete($original_storage_definitions[$field_name]); - break; - } - } - } - } - else { - $args = array('@entity_type_id' => $entity_type_id); - $message = String::format('Changes for the @entity_type_id entity type involve a data migration and cannot be applied.', $args); - throw new EntityStorageException($message); - } - } - } - - /** - * {@inheritdoc} - */ - public function getChangeSummary($entity_type_id = NULL) { - $summary = array(); - - foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) { - // Process entity type definition changes. - if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { - $definition = $this->entityManager->getDefinition($entity_type_id); - $summary[$entity_type_id][] = $this->t('The %entity_type entity type has schema changes.', array('%entity_type' => $definition->getLabel())); - } - - // Process field storage definition changes. - if (!empty($change_list['field_storage_definitions'])) { - $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); - $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); - - foreach ($change_list['field_storage_definitions'] as $field_name => $change) { - $definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : $original_storage_definitions[$field_name]; - $args = array('%field_name' => $definition->getLabel()); - - switch ($change) { - case static::DEFINITION_CREATED: - $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', $args); - break; - - case static::DEFINITION_UPDATED: - $summary[$entity_type_id][] = $this->t('The %field_name field has schema changes.', $args); - break; - - case static::DEFINITION_DELETED: - $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', $args); - break; - } - } - } - } - - return $summary; - } - - /** - * {@inheritdoc} - */ - public function getSystemRequirements($phase) { - $requirements = array( - 'title' => t('Entity schema'), - ); - - if ($this->getChangeList()) { - $requirements['value'] = $this->t('Out of date'); - $requirements['severity'] = REQUIREMENT_ERROR; - $requirements['description'] = $requirements['update']['description'] = $this->t('Some entity types have schema updates to install. You should run the database update script immediately.', array('@update' => base_path() . 'update.php')); - } - else { - $requirements['value'] = $this->t('Up to date'); - } - - return $requirements; - } - - /** - * Returns the specified stored entity type definition. - * - * @param string $entity_type_id - * The entity type identifier. - * - * @return \Drupal\Core\Entity\ContentEntityTypeInterface - * A stored entity type definition. - */ - protected function loadEntityTypeDefinition($entity_type_id) { - return $this->state->get('entity.schema.manager.' . $entity_type_id . '.entity_type'); - } - - /** - * Stores the specified stored entity type definition. - * - * @param \Drupal\Core\Entity\ContentEntityTypeInterface $definition - * The entity type definition. - */ - protected function saveEntityTypeDefinition(ContentEntityTypeInterface $definition) { - $entity_type_id = $definition->id(); - $this->state->set('entity.schema.manager.' . $entity_type_id . '.entity_type', $definition); - } - - /** - * Deletes the specified stored entity type. - * - * @param string $entity_type_id - * The entity type definition identifier. - */ - protected function deleteEntityTypeDefinition($entity_type_id) { - $this->state->delete('entity.schema.manager.' . $entity_type_id . '.entity_type'); - } - - /** - * Returns the stored field storage definitions for the specified entity type. - * - * @param string $entity_type_id - * The entity type identifier. - * - * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] - * An array of field storage definitions. - */ - protected function loadFieldStorageDefinitions($entity_type_id) { - return $this->state->get('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions'); - } - - /** - * Stores the field storage definitions for the specified entity type. - * - * @param string $entity_type_id - * The entity type identifier. - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions - * An array of field storage definitions. - */ - protected function saveFieldStorageDefinitions($entity_type_id, array $storage_definitions) { - $this->state->set('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions', $storage_definitions); - } - - /** - * Deletes the stored field storage definitions for the specified entity type. - * - * @param string $entity_type_id - * The entity type definition identifier. - */ - protected function deleteFieldStorageDefinitions($entity_type_id) { - $this->state->delete('entity.schema.manager.' . $entity_type_id . '.field_storage_definitions'); - } - -} diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php deleted file mode 100644 index 39dd069..0000000 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php +++ /dev/null @@ -1,123 +0,0 @@ -database->schema()->tableExists($this->baseTable) && - $this->database->select($this->baseTable) - ->countQuery() - ->range(0, 1) - ->execute() - ->fetchField(); - } - - /** * Initializes table name variables. */ protected function initTableLayout() { @@ -266,13 +249,13 @@ protected function schemaHandler() { /** * Updates the wrapped entity type definition. * - * @param ContentEntityTypeInterface $entity_type + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The update entity type. * * @deprecated in Drupal 8.x-dev, will be removed before Drupal 8.0. * See https://www.drupal.org/node/2274017. */ - public function setEntityType(ContentEntityTypeInterface $entity_type) { + public function setEntityType(EntityTypeInterface $entity_type) { if ($this->entityType->id() == $entity_type->id()) { $this->entityType = $entity_type; $this->initTableLayout(); @@ -1431,29 +1414,29 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ - public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { - return $this->schemaHandler()->requiresEntitySchemaChanges($definition, $original); + public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + return $this->schemaHandler()->requiresEntityStorageSchemaChanges($entity_type, $original); } /** * {@inheritdoc} */ - public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { - return $this->schemaHandler()->requiresFieldSchemaChanges($definition, $original); + public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldStorageSchemaChanges($storage_definition, $original); } /** * {@inheritdoc} */ - public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { - return $this->schemaHandler()->requiresEntityDataMigration($definition, $original); + public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + return $this->schemaHandler()->requiresEntityDataMigration($entity_type, $original); } /** * {@inheritdoc} */ - public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { - return $this->schemaHandler()->requiresFieldDataMigration($definition, $original); + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldDataMigration($storage_definition, $original); } /** @@ -1466,13 +1449,6 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { /** * {@inheritdoc} */ - public function onEntityTypeDelete(EntityTypeInterface $entity_type) { - $this->schemaHandler()->onEntityTypeDelete($entity_type); - } - - /** - * {@inheritdoc} - */ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { // Ensure we have an updated entity type definition. $this->entityType = $entity_type; @@ -1486,6 +1462,13 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI /** * {@inheritdoc} */ + public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + $this->schemaHandler()->onEntityTypeDelete($entity_type); + } + + /** + * {@inheritdoc} + */ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { // If we are adding a field stored in a shared table we need to recompute // the table mapping. @@ -1495,14 +1478,14 @@ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $ if ($this->getTableMapping()->allowsSharedTableStorage($storage_definition)) { $this->tableMapping = NULL; } - $this->schemaHandler()->createFieldSchema($storage_definition); + $this->schemaHandler()->onFieldStorageDefinitionCreate($storage_definition); } /** * {@inheritdoc} */ public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { - $this->schemaHandler()->updateFieldSchema($storage_definition, $original); + $this->schemaHandler()->onFieldStorageDefinitionUpdate($storage_definition, $original); } /** @@ -1670,7 +1653,7 @@ protected function purgeFieldItems(ContentEntityInterface $entity, FieldDefiniti * {@inheritdoc} */ public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { - $this->schemaHandler()->deleteFieldSchema($storage_definition); + $this->schemaHandler()->finalizePurge($storage_definition); } /** @@ -1691,40 +1674,32 @@ public function countFieldData($storage_definition, $as_bool = FALSE) { ->condition($or) ->fields('t', array('entity_id')) ->distinct(TRUE); - // If we are performing the query just to check if the field has data - // limit the number of rows. - if ($as_bool) { - $query->range(0, 1); - } - $count = $query->countQuery()->execute()->fetchField(); } else { - if ($as_bool) { - $count = $this->hasData(); + $data_table = $this->dataTable ?: $this->baseTable; + $query = $this->database->select($data_table, 't'); + $columns = $storage_definition->getColumns(); + if (count($columns) > 1) { + $or = $query->orConditionGroup(); + foreach ($columns as $column_name => $data) { + $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); + } + $query->condition($or); } else { - $data_table = $this->dataTable ?: $this->baseTable; - $query = $this->database->select($data_table, 't'); - $columns = $storage_definition->getColumns(); - if (count($columns) > 1) { - $or = $query->orConditionGroup(); - foreach ($columns as $column_name => $data) { - $or->isNotNull($table_mapping->getFieldColumnName($storage_definition, $column_name)); - } - $query->condition($or); - } - else { - $query->isNotNull($storage_definition->getName()); - } - $count = $query - ->fields('t', array($this->idKey)) - ->distinct(TRUE) - ->countQuery() - ->execute() - ->fetchField(); + $query->isNotNull($storage_definition->getName()); } + $query + ->fields('t', array($this->idKey)) + ->distinct(TRUE); } + // If we are performing the query just to check if the field has data + // limit the number of rows. + if ($as_bool) { + $query->range(0, 1); + } + $count = $query->countQuery()->execute()->fetchField(); return $as_bool ? (bool) $count : (int) $count; } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index b6ff8ee..6131650 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -11,18 +11,17 @@ use Drupal\Core\Database\Connection; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; -use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; -use Drupal\Core\Entity\Schema\ContentEntitySchemaHandlerInterface; -use Drupal\Core\Entity\Schema\ContentEntitySchemaProviderInterface; +use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Defines a schema handler that supports revisionable, translatable entities. */ -class SqlContentEntityStorageSchema implements ContentEntitySchemaHandlerInterface, ContentEntitySchemaProviderInterface { +class SqlContentEntityStorageSchema implements FieldableEntityStorageSchemaInterface { /** * The entity manager. @@ -114,6 +113,82 @@ protected function state() { /** * {@inheritdoc} */ + public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + return + $entity_type->getStorageClass() != $original->getStorageClass() || + $entity_type->isRevisionable() != $original->isRevisionable() || + $entity_type->isTranslatable() != $original->isTranslatable() || + // Detect changes in key or index definitions. + $this->getEntitySchemaData($entity_type, $this->getEntitySchema($entity_type, TRUE)) != $this->loadEntitySchemaData($original); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + return + $storage_definition->hasCustomStorage() != $original->hasCustomStorage() || + $storage_definition->getSchema() != $original->getSchema() || + $storage_definition->isRevisionable() != $original->isRevisionable() || + $this->requiresFieldDataMigration($storage_definition, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + // If we're updating from NULL storage, then there's no stored data that + // requires migration. + // @todo Remove in https://www.drupal.org/node/2335879. + $original_storage_class = $original->getStorageClass(); + $null_storage_class = 'Drupal\Core\Entity\ContentEntityNullStorage'; + if ($original_storage_class == $null_storage_class || is_subclass_of($original_storage_class, $null_storage_class)) { + return FALSE; + } + + return + // If the original storage class is different, then there might be + // existing entities in that storage even if the new storage's base + // table is empty. + // @todo Ask the old storage handler rather than assuming: + // https://www.drupal.org/node/2335879. + $entity_type->getStorageClass() != $original_storage_class || + !$this->tableIsEmpty($this->storage->getBaseTable()); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + // If the base table is empty, there are no entities, and therefore, no + // field data that we care about preserving. + if ($this->tableIsEmpty($this->storage->getBaseTable())) { + return FALSE; + } + + $table_mapping = $this->storage->getTableMapping(); + + // If the field changes its custom storage status, we will need to create or + // drop its schema. In any case we cannot migrate its data as custom storage + // is involved. Otherwise if a field is moved from a shared table to a + // dedicated table or viceversa we need a data migration. + $custom_storage = $storage_definition->hasCustomStorage() || $original->hasCustomStorage(); + $shared_table_changed = $table_mapping->allowsSharedTableStorage($storage_definition) != $table_mapping->allowsSharedTableStorage($original); + $dedicated_table_changed = $table_mapping->requiresDedicatedTableStorage($storage_definition) != $table_mapping->requiresDedicatedTableStorage($original); + if (!$custom_storage && ($shared_table_changed || $dedicated_table_changed)) { + return TRUE; + } + // If columns change we may need data manipulation, which we cannot handle. + if ($storage_definition->getColumns() != $original->getColumns()) { + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { $this->checkEntityType($entity_type); $schema_handler = $this->database->schema(); @@ -133,8 +208,13 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI $this->checkEntityType($entity_type); $this->checkEntityType($original); + // If no schema changes are needed, we don't need to do anything. + if (!$this->requiresEntityStorageSchemaChanges($entity_type, $original)) { + return; + } + // If we have no data just recreate the entity schema from scratch. - if (!$this->database->schema()->tableExists($this->storage->getBaseTable()) || !$this->storage->hasData()) { + if (!$this->requiresEntityDataMigration($entity_type, $original)) { if ($this->database->supportsTransactionalDDL()) { // If the database supports transactional DDL, we can go ahead and rely // on it. If not, we will have to rollback manually if something fails. @@ -224,62 +304,48 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { /** * {@inheritdoc} */ - public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { - return !$original || - $original->getStorageClass() != $definition->getStorageClass() || - $original->isRevisionable() != $definition->isRevisionable() || - $original->isTranslatable() != $definition->isTranslatable() || - // Detect changes in key or index definitions. - $this->loadEntitySchemaData($original) != $this->getEntitySchemaData($definition, $this->getEntitySchema($definition, TRUE)); + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { + $this->performFieldSchemaOperation('create', $storage_definition); } /** * {@inheritdoc} */ - public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { - return !$original || - $original->getSchema() != $definition->getSchema() || - $original->isRevisionable() != $definition->isRevisionable() || - $original->hasCustomStorage() != $definition->hasCustomStorage() || - $this->requiresFieldDataMigration($definition, $original); + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + // Store original definitions so that switching between shared and dedicated + // field table layout works. + $this->originalDefinitions = $this->fieldStorageDefinitions; + $this->originalDefinitions[$original->getName()] = $original; + $this->performFieldSchemaOperation('update', $storage_definition, $original); + $this->originalDefinitions = NULL; } /** * {@inheritdoc} */ - public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { - // A change in the storage class may or may not imply a data migration. We - // assume it does. This method should be overridden otherwise. Basically the - // only schema change that does not imply a data migration is from - // revisionable to non revisionable, as in that case we just need to drop - // revision tables. - return $original->getStorageClass() != $definition->getStorageClass() || - $original->isRevisionable() != $definition->isRevisionable() || - $original->isTranslatable() != $definition->isTranslatable(); + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { + $table_mapping = $this->storage->getTableMapping(); + // @todo Implement this also for shared table storage. See + // https://www.drupal.org/node/2282119. + if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { + // Move the table to a unique name while the table contents are being + // deleted. + $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); + } + } } /** * {@inheritdoc} */ - public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { - $table_mapping = $this->storage->getTableMapping(); - - // If the field changes its custom storage status, we will need to create or - // drop its schema. In any case we cannot migrate its data as custom storage - // is involved. Otherwise if a field is moved from a shared table to a - // dedicated table or viceversa we need a data migration. - $custom_storage = $original->hasCustomStorage() || $definition->hasCustomStorage(); - $shared_table_changed = $table_mapping->allowsSharedTableStorage($original) != $table_mapping->allowsSharedTableStorage($definition); - $dedicated_table_changed = $table_mapping->requiresDedicatedTableStorage($original) != $table_mapping->requiresDedicatedTableStorage($definition); - if (!$custom_storage && ($shared_table_changed || $dedicated_table_changed)) { - return TRUE; - } - // If columns change we may need data manipulation, which we cannot handle. - if ($original->getColumns() != $definition->getColumns()) { - return TRUE; - } - - return FALSE; + public function finalizePurge(FieldStorageDefinitionInterface $storage_definition) { + $this->performFieldSchemaOperation('delete', $storage_definition); } /** @@ -314,6 +380,24 @@ protected function dropDedicatedTableRevisionSchema() { } /** + * Checks that we are dealing with the correct entity type. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type to be checked. + * + * @return bool + * TRUE if the entity type matches the current one. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + protected function checkEntityType(EntityTypeInterface $entity_type) { + if ($entity_type->id() != $this->entityType->id()) { + throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id()))); + } + return TRUE; + } + + /** * Returns the entity schema for the specified entity type. * * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type @@ -405,24 +489,6 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res } /** - * Checks that we are dealing with the correct entity type. - * - * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type - * The entity type to be checked. - * - * @return bool - * TRUE if the entity type matches the current one. - * - * @throws \Drupal\Core\Entity\EntityStorageException - */ - protected function checkEntityType(ContentEntityTypeInterface $entity_type) { - if ($entity_type->id() != $this->entityType->id()) { - throw new EntityStorageException(String::format('Unsupported entity type @id', array('@id' => $entity_type->id()))); - } - return TRUE; - } - - /** * Gets a list of entity type tables. * * @return array @@ -476,6 +542,162 @@ protected function getEntitySchemaData(ContentEntityTypeInterface $entity_type, } /** + * Returns an index schema array for a given field. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * + * @return array + * The schema definition for the indexes. + */ + protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) { + return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes'); + } + + /** + * Returns a unique key schema array for a given field. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * + * @return array + * The schema definition for the unique keys. + */ + protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) { + return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys'); + } + + /** + * Returns field schema data for the given key. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * @param string $schema_key + * The type of schema data. Either 'indexes' or 'unique keys'. + * + * @return array + * The schema definition for the specified key. + */ + protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) { + $data = array(); + + foreach ($field_schema[$schema_key] as $key => $columns) { + // To avoid clashes with entity-level indexes or unique keys we use + // "{$entity_type_id}_field__" as a prefix instead of just + // "{$entity_type_id}__". We additionally namespace the specifier by the + // field name to avoid clashes when multiple fields of the same type are + // added to an entity type. + $entity_type_id = $this->entityType->id(); + $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key); + foreach ($columns as $column) { + // Allow for indexes and unique keys to specified as an array of column + // name and length. + 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; + } + + /** + * Generates a safe schema identifier (name of an index, column name etc.). + * + * @param string $entity_type_id + * The ID of the entity type. + * @param string $field_name + * The name of the field. + * @param string $key + * The key of the field. + * + * @return string + * The field identifier name. + */ + protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key) { + $real_key = "{$entity_type_id}_field__{$field_name}__{$key}"; + // Limit the string to 48 characters, keeping a 16 characters margin for db + // prefixes. + if (strlen($real_key) > 48) { + // Use a shorter separator, a truncated entity_type, and a hash of the + // field name. + // Truncate to the same length for the current and revision tables. + $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; + } + + /** + * Returns field foreign keys. + * + * @param string $field_name + * The name of the field. + * @param array $field_schema + * The schema of the field. + * @param string[] $column_mapping + * A mapping of field column names to database column names. + * + * @return array + * The schema definition for the foreign keys. + */ + protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) { + $foreign_keys = array(); + + foreach ($field_schema['foreign keys'] as $specifier => $specification) { + // To avoid clashes with entity-level foreign keys we use + // "{$entity_type_id}_field__" as a prefix instead of just + // "{$entity_type_id}__". We additionally namespace the specifier by the + // field name to avoid clashes when multiple fields of the same type are + // added to an entity type. + $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; + } + + /** + * Returns the schema for the 'default_langcode' metadata field. + * + * @param array $schema + * The table schema to add the field schema to, passed by reference. + * + * @return array + * A schema field array for the 'default_langcode' metadata field. + */ + protected function addDefaultLangcodeSchema(&$schema) { + $schema['fields']['default_langcode'] = array( + 'description' => 'Boolean indicating whether field values are in the default entity language.', + 'type' => 'int', + 'size' => 'tiny', + 'not null' => TRUE, + 'default' => 1, + ); + } + + /** * Loads stored schema data for the given entity type definition. * * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type @@ -636,10 +858,25 @@ protected function initializeRevisionDataTable(ContentEntityTypeInterface $entit ) ), ); - - $this->addTableDefaults($schema); - - return $schema; + + $this->addTableDefaults($schema); + + return $schema; + } + + /** + * Adds defaults to a table schema definition. + * + * @param $schema + * The schema definition array for a single table, passed by reference. + */ + protected function addTableDefaults(&$schema) { + $schema += array( + 'fields' => array(), + 'unique keys' => array(), + 'indexes' => array(), + 'foreign keys' => array(), + ); } /** @@ -701,6 +938,21 @@ protected function processRevisionDataTable(ContentEntityTypeInterface $entity_t } /** + * Processes the specified entity key. + * + * @param array $schema + * The table schema, passed by reference. + * @param string $key + * The entity key name. + */ + protected function processIdentifierSchema(&$schema, $key) { + if ($schema['fields'][$key]['type'] == 'int') { + $schema['fields'][$key]['type'] = 'serial'; + } + unset($schema['fields'][$key]['default']); + } + + /** * Performs the specified operation on a field. * * This figures out whether the field is stored in a dedicated or shared table @@ -725,13 +977,6 @@ protected function performFieldSchemaOperation($operation, FieldStorageDefinitio } /** - * {@inheritdoc} - */ - public function createFieldSchema(FieldStorageDefinitionInterface $storage_definition) { - $this->performFieldSchemaOperation('create', $storage_definition); - } - - /** * Creates the schema for a field stored in a dedicated table. * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition @@ -774,34 +1019,6 @@ protected function createSharedTableSchema(FieldStorageDefinitionInterface $stor } /** - * {@inheritdoc} - */ - public function prepareFieldSchemaDeletion(FieldStorageDefinitionInterface $storage_definition) { - $table_mapping = $this->storage->getTableMapping(); - // @todo Implement this also for shared table storage. See - // https://www.drupal.org/node/2282119. - if ($table_mapping->requiresDedicatedTableStorage($storage_definition)) { - // Move the table to a unique name while the table contents are being - // deleted. - $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); - } - } - } - - /** - * {@inheritdoc} - */ - public function deleteFieldSchema(FieldStorageDefinitionInterface $storage_definition) { - $this->performFieldSchemaOperation('delete', $storage_definition); - } - - /** * Deletes the schema for a field stored in a dedicated table. * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition @@ -863,18 +1080,6 @@ protected function deleteSharedTableSchema(FieldStorageDefinitionInterface $stor } /** - * {@inheritdoc} - */ - public function updateFieldSchema(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { - // Store original definitions so that switching between shared and dedicated - // field table layout works. - $this->originalDefinitions = $this->fieldStorageDefinitions; - $this->originalDefinitions[$original->getName()] = $original; - $this->performFieldSchemaOperation('update', $storage_definition, $original); - $this->originalDefinitions = NULL; - } - - /** * Updates the schema for a field stored in a shared table. * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition @@ -898,8 +1103,8 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s try { // Since there is no data we may be switching from a shared table schema // to a dedicated table schema, hence we should use the proper API. - $this->deleteFieldSchema($original); - $this->createFieldSchema($storage_definition); + $this->performFieldSchemaOperation('delete', $original); + $this->performFieldSchemaOperation('create', $storage_definition); } catch (\Exception $e) { if ($this->database->supportsTransactionalDDL()) { @@ -907,7 +1112,7 @@ protected function updateDedicatedTableSchema(FieldStorageDefinitionInterface $s } else { // Recreate tables. - $this->createFieldSchema($original); + $this->performFieldSchemaOperation('create', $original); } throw $e; } @@ -982,8 +1187,8 @@ protected function updateSharedTableSchema(FieldStorageDefinitionInterface $stor try { // Since there is no data we may be switching from a dedicated table // to a schema table schema, hence we should use the proper API. - $this->deleteFieldSchema($original); - $this->createFieldSchema($storage_definition); + $this->performFieldSchemaOperation('delete', $original); + $this->performFieldSchemaOperation('create', $storage_definition); } catch (\Exception $e) { if ($this->database->supportsTransactionalDDL()) { @@ -1110,162 +1315,6 @@ protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $st return $schema; } - /** - * Returns an index schema array for a given field. - * - * @param string $field_name - * The name of the field. - * @param array $field_schema - * The schema of the field. - * @param string[] $column_mapping - * A mapping of field column names to database column names. - * - * @return array - * The schema definition for the indexes. - */ - protected function getFieldIndexes($field_name, array $field_schema, array $column_mapping) { - return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'indexes'); - } - - /** - * Returns a unique key schema array for a given field. - * - * @param string $field_name - * The name of the field. - * @param array $field_schema - * The schema of the field. - * @param string[] $column_mapping - * A mapping of field column names to database column names. - * - * @return array - * The schema definition for the unique keys. - */ - protected function getFieldUniqueKeys($field_name, array $field_schema, array $column_mapping) { - return $this->getFieldSchemaData($field_name, $field_schema, $column_mapping, 'unique keys'); - } - - /** - * Returns field schema data for the given key. - * - * @param string $field_name - * The name of the field. - * @param array $field_schema - * The schema of the field. - * @param string[] $column_mapping - * A mapping of field column names to database column names. - * @param string $schema_key - * The type of schema data. Either 'indexes' or 'unique keys'. - * - * @return array - * The schema definition for the specified key. - */ - protected function getFieldSchemaData($field_name, array $field_schema, array $column_mapping, $schema_key) { - $data = array(); - - foreach ($field_schema[$schema_key] as $key => $columns) { - // To avoid clashes with entity-level indexes or unique keys we use - // "{$entity_type_id}_field__" as a prefix instead of just - // "{$entity_type_id}__". We additionally namespace the specifier by the - // field name to avoid clashes when multiple fields of the same type are - // added to an entity type. - $entity_type_id = $this->entityType->id(); - $real_key = $this->getFieldSchemaIdentifierName($entity_type_id, $field_name, $key); - foreach ($columns as $column) { - // Allow for indexes and unique keys to specified as an array of column - // name and length. - 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; - } - - /** - * Generates a safe schema identifier (name of an index, column name etc.). - * - * @param string $entity_type_id - * The ID of the entity type. - * @param string $field_name - * The name of the field. - * @param string $key - * The key of the field. - * - * @return string - * The field identifier name. - */ - protected function getFieldSchemaIdentifierName($entity_type_id, $field_name, $key) { - $real_key = "{$entity_type_id}_field__{$field_name}__{$key}"; - // Limit the string to 48 characters, keeping a 16 characters margin for db - // prefixes. - if (strlen($real_key) > 48) { - // Use a shorter separator, a truncated entity_type, and a hash of the - // field name. - // Truncate to the same length for the current and revision tables. - $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; - } - - /** - * Returns field foreign keys. - * - * @param string $field_name - * The name of the field. - * @param array $field_schema - * The schema of the field. - * @param string[] $column_mapping - * A mapping of field column names to database column names. - * - * @return array - * The schema definition for the foreign keys. - */ - protected function getFieldForeignKeys($field_name, array $field_schema, array $column_mapping) { - $foreign_keys = array(); - - foreach ($field_schema['foreign keys'] as $specifier => $specification) { - // To avoid clashes with entity-level foreign keys we use - // "{$entity_type_id}_field__" as a prefix instead of just - // "{$entity_type_id}__". We additionally namespace the specifier by the - // field name to avoid clashes when multiple fields of the same type are - // added to an entity type. - $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; - } - - /** - * Returns the schema for the 'default_langcode' metadata field. - * - * @param array $schema - * The table schema to add the field schema to, passed by reference. - * - * @return array - * A schema field array for the 'default_langcode' metadata field. - */ - protected function addDefaultLangcodeSchema(&$schema) { - $schema['fields']['default_langcode'] = array( - 'description' => 'Boolean indicating whether field values are in the default entity language.', - 'type' => 'int', - 'size' => 'tiny', - 'not null' => TRUE, - 'default' => 1, - ); - } - /** * Returns the SQL schema for a dedicated table. @@ -1433,21 +1482,6 @@ protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $stor } /** - * Adds defaults to a table schema definition. - * - * @param $schema - * The schema definition array for a single table, passed by reference. - */ - protected function addTableDefaults(&$schema) { - $schema += array( - 'fields' => array(), - 'unique keys' => array(), - 'indexes' => array(), - 'foreign keys' => array(), - ); - } - - /** * Returns the name to be used for the given entity index. * * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type @@ -1477,20 +1511,24 @@ protected function getEntityIndexName(ContentEntityTypeInterface $entity_type, $ protected function getFieldIndexName(FieldStorageDefinitionInterface $storage_definition, $index) { return $storage_definition->getName() . '_' . $index; } - /** - * Processes the specified entity key. + * Checks whether a database table is non-existent or empty. * - * @param array $schema - * The table schema, passed by reference. - * @param string $key - * The entity key name. + * Empty tables can be dropped and recreated without data loss. + * + * @param string $table_name + * The database table to check. + * + * @return bool + * TRUE if the table is empty, FALSE otherwise. */ - protected function processIdentifierSchema(&$schema, $key) { - if ($schema['fields'][$key]['type'] == 'int') { - $schema['fields'][$key]['type'] = 'serial'; - } - unset($schema['fields'][$key]['default']); + protected function tableIsEmpty($table_name) { + return !$this->database->schema()->tableExists($table_name) || + !$this->database->select($table_name) + ->countQuery() + ->range(0, 1) + ->execute() + ->fetchField(); } } diff --git a/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php new file mode 100644 index 0000000..fcea2e4 --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldStorageDefinitionListenerInterface.php @@ -0,0 +1,46 @@ +settings += $field_type_manager->getDefaultSettings($this->type); - // Notify the entity schema manager. - \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionCreate($this); + // Notify the entity manager. + $entity_manager->onFieldStorageDefinitionCreate($this); } /** @@ -334,19 +334,16 @@ protected function preSaveUpdated(EntityStorageInterface $storage) { // invokes hook_field_storage_config_update_forbid(). $module_handler->invokeAll('field_storage_config_update_forbid', array($this, $this->original)); - // Notify the schema manager. The controller can reject the definition + // Notify the entity manager. A listener can reject the definition // update as invalid by raising an exception, which stops execution before // the definition is written to config. - \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionUpdate($this, $this->original); + $entity_manager->onFieldStorageDefinitionUpdate($this, $this->original); } /** * {@inheritdoc} */ public function postSave(EntityStorageInterface $storage, $update = TRUE) { - // Clear the cache. - \Drupal::entityManager()->clearCachedFieldDefinitions(); - if ($update) { // Invalidate the render cache for all affected entities. $entity_manager = \Drupal::entityManager(); @@ -406,13 +403,10 @@ public static function postDelete(EntityStorageInterface $storage, array $fields // Notify the storage. foreach ($fields as $field) { if (!$field->deleted) { - \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionDelete($field); + \Drupal::entityManager()->onFieldStorageDefinitionDelete($field); $field->deleted = TRUE; } } - - // Clear the cache. - \Drupal::entityManager()->clearCachedFieldDefinitions(); } /** diff --git a/core/modules/system/src/Controller/DbUpdateController.php b/core/modules/system/src/Controller/DbUpdateController.php index 7b84a54..fc0c6b7 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -130,14 +130,9 @@ public function handle($op, Request $request) { } else { switch ($op) { - case 'entity_schema': - $regions['sidebar_first'] = $this->updateTasksList('entity_schema'); - $output = $this->entitySchema(); - break; - case 'selection': $regions['sidebar_first'] = $this->updateTasksList('selection'); - $output = $this->selection($request); + $output = $this->selection(); break; case 'run': @@ -167,7 +162,7 @@ public function handle($op, Request $request) { if ($output instanceof Response) { return $output; } - $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal module updates'); + $title = isset($output['#title']) ? $output['#title'] : $this->t('Drupal database update'); return new Response(DefaultHtmlPageRenderer::renderPage($output, $title, 'maintenance', $regions)); } @@ -204,8 +199,7 @@ protected function info() { '#markup' => '
' . $this->t('When you have performed the steps above, you may proceed.') . '
', ); - $entity_schema_updates = count(\Drupal::service('entity.schema.manager')->getChangeList()); - $url = new Url('system.db_update', array('op' => ($entity_schema_updates ? 'entity_schema' : 'selection'))); + $url = new Url('system.db_update', array('op' => 'selection')); $build['link'] = array( '#type' => 'link', '#title' => $this->t('Continue'), @@ -215,55 +209,12 @@ protected function info() { } /** - * Renders a list of available entity schema updates. + * Renders a list of available database updates. * * @return array * A render array. */ - function entitySchema() { - $build = array('#title' => $this->t('Drupal entity schema updates')); - - // Build a summary of the entity schema changes. - $summary = \Drupal::service('entity.schema.manager')->getChangeSummary(); - if ($summary) { - $entity_manager = $this->entityManager(); - foreach ($summary as $entity_type_id => $items) { - $definition = $entity_manager->getDefinition($entity_type_id); - $build['summary'][$entity_type_id] = array( - '#type' => 'details', - '#title' => $definition->getLabel(), - ); - $build['summary'][$entity_type_id]['changes'] = array( - '#theme' => 'item_list', - '#items' => $items, - ); - } - } - else { - $build['summary'] = array('#markup' => $this->t('No entity schema changes available.')); - } - - $url = new Url('system.db_update', array('op' => 'selection'), array('query' => array('entity_schema_updates' => (int) !empty($summary)))); - $build['link'] = array( - '#type' => 'link', - '#title' => $this->t('Continue'), - '#attributes' => array('class' => array('button', 'button--primary')), - '#weight' => 5, - ) + $url->toRenderArray(); - - return $build; - } - - /** - * Renders a list of available module updates. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * The current request object. - * - * @return array - * A render array. - */ - protected function selection(Request $request) { + protected function selection() { // Make sure there is no stale theme registry. $this->cache->deleteAll(); @@ -334,8 +285,24 @@ protected function selection(Request $request) { drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning'); } - $force_updates = (bool) $request->get('entity_schema_updates'); - if (empty($count) && !$force_updates) { + // If there are entity definition updates, display their summary. + if (\Drupal::service('entity.definition_update_manager')->needsUpdates()) { + $entity_build = array(); + $summary = \Drupal::service('entity.definition_update_manager')->getChangeSummary(); + foreach ($summary as $entity_type_id => $items) { + $entity_update_key = 'entity_type_updates_' . $entity_type_id; + $entity_build[$entity_update_key] = array( + '#theme' => 'item_list', + '#items' => $items, + '#title' => $entity_type_id . ' entity type', + ); + $count++; + } + // Display these above the module updates, since they will be run first. + $build['start'] = $entity_build + $build['start']; + } + + if (empty($count)) { drupal_set_message($this->t('No pending updates.')); unset($build); $build['links'] = array( @@ -347,31 +314,21 @@ protected function selection(Request $request) { drupal_flush_all_caches(); } else { - if ($count > 0) { - $build['help'] = array( - '#markup' => '' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '
', - '#weight' => -5, + $build['help'] = array( + '#markup' => '' . $this->t('The version of Drupal you are updating from has been automatically detected.') . '
', + '#weight' => -5, + ); + if ($incompatible_count) { + $build['start']['#title'] = $this->formatPlural( + $count, + '1 pending update (@number_applied to be applied, @number_incompatible skipped)', + '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', + array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) ); - if ($incompatible_count) { - $build['start']['#title'] = $this->formatPlural( - $count, - '1 pending update (@number_applied to be applied, @number_incompatible skipped)', - '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', - array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) - ); - } - else { - $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates'); - } } else { - unset($build); - $build['help'] = array( - '#markup' => 'No module update available.
', - '#weight' => -5, - ); + $build['start']['#title'] = $this->formatPlural($count, '1 pending update', '@count pending updates'); } - $url = new Url('system.db_update', array('op' => 'run')); $build['link'] = array( '#type' => 'link', @@ -531,8 +488,7 @@ protected function updateTasksList($active = NULL) { $tasks = array( 'requirements' => $this->t('Verify requirements'), 'info' => $this->t('Overview'), - 'entity_schema' => $this->t('Review entity schema updates'), - 'selection' => $this->t('Review module updates'), + 'selection' => $this->t('Review updates'), 'run' => $this->t('Run updates'), 'results' => $this->t('Review log'), ); @@ -562,11 +518,13 @@ protected function triggerBatch(Request $request) { $this->state->set('system.maintenance_mode', TRUE); } - // First of all perform entity schema updates, if needed, so that subsequent - // updates work with a correct entity schema. $operations = array(); - if (\Drupal::service('entity.schema.manager')->getChangeList()) { - $operations[] = array('update_entity_schema', array('system', '0 - Update entity schema')); + + // First of all perform entity definition updates, which will update + // storage schema if needed, so that module update functions work with + // the correct entity schema. + if (\Drupal::service('entity.definition_update_manager')->needsUpdates()) { + $operations[] = array('update_entity_definitions', array('system', '0 - Update entity definitions')); } // Resolve any update dependencies to determine the actual updates that will diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 9b7e901..b9c1b83 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -452,9 +452,11 @@ function system_requirements($phase) { } } } - - // Check entity schema status. - $requirements['entity_schema'] = \Drupal::service('entity.schema.manager')->getSystemRequirements($phase); + if (!isset($requirements['update']['severity']) && \Drupal::service('entity.definition_update_manager')->needsUpdates()) { + $requirements['update']['severity'] = REQUIREMENT_ERROR; + $requirements['update']['value'] = t('Out of date'); + $requirements['update']['description'] = t('Some modules have database schema updates to install. You should run the database update script immediately.', array('@update' => base_path() . 'update.php')); + } } // Verify the update.php access setting diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php index 9163a6d..b8c24a1 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityManagerTest.php @@ -82,7 +82,7 @@ class EntityManagerTest extends UnitTestCase { /** * The controller resolver. * - * @var \Drupal\Core\Handler\HandlerResolverInterface|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject */ protected $controllerResolver; @@ -94,6 +94,13 @@ class EntityManagerTest extends UnitTestCase { protected $typedDataManager; /** + * The state service. + * + * @var \Drupal\Core\State\StateInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $state; + + /** * {@inheritdoc} */ protected function setUp() { @@ -128,6 +135,8 @@ protected function setUp() { $this->typedDataManager = $this->getMockBuilder('\Drupal\Core\TypedData\TypedDataManager') ->disableOriginalConstructor() ->getMock(); + + $this->state = $this->getMock('Drupal\Core\State\StateInterface'); } /** @@ -158,7 +167,7 @@ protected function setUpEntityManager($definitions = array()) { ->method('getDefinitions') ->will($this->returnValue($definitions)); - $this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler, $this->cache, $this->languageManager, $this->translationManager, $this->getClassResolverStub(), $this->typedDataManager); + $this->entityManager = new TestEntityManager(new \ArrayObject(), $this->moduleHandler, $this->cache, $this->languageManager, $this->translationManager, $this->getClassResolverStub(), $this->typedDataManager, $this->state); $this->entityManager->setContainer($this->container); $this->entityManager->setDiscovery($this->discovery); } diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php index b89f999..0181232 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php @@ -972,7 +972,7 @@ public function testDedicatedTableSchema() { ->method('getTableMapping') ->will($this->returnValue($table_mapping)); - $this->schemaHandler->createFieldSchema($field_storage); + $this->schemaHandler->onFieldStorageDefinitionCreate($field_storage); } /** diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php index 132229d..38a3df8 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php @@ -932,7 +932,7 @@ public function testGetTableMappingRevisionableTranslatableWithFields(array $ent /** * Tests field SQL schema generation for an entity with a string identifier. * - * @covers SqlContentEntityStorageSchema::createFieldSchema() + * @covers SqlContentEntityStorageSchema::onFieldStorageDefinitionCreate() */ public function testFieldSqlSchemaForEntityWithStringIdentifier() { $field_type_manager = $this->getMock('Drupal\Core\Field\FieldTypePluginManagerInterface'); @@ -1015,7 +1015,7 @@ public function testFieldSqlSchemaForEntityWithStringIdentifier() { ->will($this->returnValue($schema)); $schema_handler = new SqlContentEntityStorageSchema($this->entityManager, $this->entityType, $this->entityStorage, $this->connection); - $schema_handler->createFieldSchema($field_storage); + $schema_handler->onFieldStorageDefinitionCreate($field_storage); // Make sure that the entity_id schema field if of type varchar. // $schema['test_entity__test_field']['fields']['entity_id']['type']