diff --git a/core/core.services.yml b/core/core.services.yml index f8c428f..0d7d10a 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -281,10 +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.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 f9a88db..125907d 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -10,14 +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\Page\DefaultHtmlPageRenderer; +use Drupal\Core\Entity\EntityStorageException; 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. @@ -258,6 +252,33 @@ function update_do_one($module, $number, $dependency_map, &$context) { } /** + * Performs entity definition updates, which can trigger schema updates. + * + * @param $module + * The module whose update will be run. + * @param $number + * The update number to run. + * @param $context + * The batch context array. + */ +function update_entity_definitions($module, $number, &$context) { + try { + \Drupal::service('entity.definition_update_manager')->applyUpdates(); + } + catch (EntityStorageException $e) { + watchdog_exception('update', $e); + $variables = Error::decodeException($e); + unset($variables['backtrace']); + // The exception message is run through + // \Drupal\Component\Utility\String::checkPlain() by + // \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_definitions'; + } +} + +/** * Returns a list of all the pending database updates. * * @return diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php new file mode 100644 index 0000000..3644b34 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php @@ -0,0 +1,237 @@ +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); + $original = $this->entityManager->getInstalledDefinition($entity_type_id); + if ($this->requiresEntityStorageSchemaChanges($entity_type, $original)) { + $summary[$entity_type_id][] = $this->t('The %entity_type entity type has definition updates that require schema changes.', array('%entity_type' => $entity_type->getLabel())); + } + else { + $summary[$entity_type_id][] = $this->t('The %entity_type entity type has definition updates that do not 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: + if ($this->requiresFieldStorageSchemaChanges($storage_definitions[$field_name], $original_storage_definitions[$field_name])) { + $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())); + } + else { + $summary[$entity_type_id][] = $this->t('The %field_name field has storage definition updates that do not 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) { + if ($entity_type != $this->entityManager->getInstalledDefinition($entity_type_id)) { + $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) { + if ($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,36 +976,102 @@ 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); } + + $this->setInstalledDefinition($entity_type); + if ($entity_type->isFieldable()) { + $this->setInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id)); + } } /** * {@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); } + + $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); } + + $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(); } /** @@ -1047,4 +1128,81 @@ public function onBundleDelete($entity_type_id, $bundle) { $this->clearCachedFieldDefinitions(); } + /** + * {@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. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition + * The field storage definition. + */ + 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/Exception/FieldStorageDefinitionUpdateForbiddenException.php b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php index 54e2bff..0149ec3 100644 --- a/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php +++ b/core/lib/Drupal/Core/Entity/Exception/FieldStorageDefinitionUpdateForbiddenException.php @@ -7,7 +7,10 @@ namespace Drupal\Core\Entity\Exception; +use Drupal\Core\Entity\EntityStorageException; + /** * Exception thrown when a storage definition update is forbidden. */ -class FieldStorageDefinitionUpdateForbiddenException extends \Exception { } +class FieldStorageDefinitionUpdateForbiddenException extends EntityStorageException { +} 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/EntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php deleted file mode 100644 index 51001bd..0000000 --- a/core/lib/Drupal/Core/Entity/Schema/EntitySchemaHandlerInterface.php +++ /dev/null @@ -1,15 +0,0 @@ -schemaHandler()->requiresEntityStorageSchemaChanges($entity_type, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldStorageSchemaChanges($storage_definition, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + return $this->schemaHandler()->requiresEntityDataMigration($entity_type, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldDataMigration($storage_definition, $original); + } + + /** + * {@inheritdoc} + */ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { $this->schemaHandler()->onEntityTypeCreate($entity_type); } diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index 32c83c7..322b838 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -7,16 +7,19 @@ namespace Drupal\Core\Entity\Sql; +use Drupal\Component\Utility\String; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Entity\Schema\EntitySchemaHandlerInterface; +use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface; +use Drupal\Core\Field\FieldStorageDefinitionInterface; /** * Defines a schema handler that supports revisionable, translatable entities. */ -class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface { +class SqlContentEntityStorageSchema implements FieldableEntityStorageSchemaInterface { /** * The entity type this schema builder is responsible for. @@ -75,7 +78,69 @@ public function __construct(EntityManagerInterface $entity_manager, ContentEntit /** * {@inheritdoc} */ + public function requiresEntityStorageSchemaChanges(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + return + $entity_type->getStorageClass() != $original->getStorageClass() || + $entity_type->getKeys() != $original->getKeys() || + $entity_type->isRevisionable() != $original->isRevisionable() || + $entity_type->isTranslatable() != $original->isTranslatable(); + } + + /** + * {@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() || + $storage_definition->isTranslatable() != $original->isTranslatable(); + } + + /** + * {@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. + // @todo We might be returning TRUE here in cases where it would be safe + // to return FALSE (for example, if the field is in a dedicated table + // and that table is empty), and thereby preventing automatic updates + // that should be possible, but determining that requires refactoring + // SqlContentEntityStorage::_fieldSqlSchema(), and in the meantime, + // it's safer to return false positives than false negatives: + // https://www.drupal.org/node/1498720. + return !$this->tableIsEmpty($this->storage->getBaseTable()); + } + + /** + * {@inheritdoc} + */ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { + $this->checkEntityType($entity_type); $schema_handler = $this->database->schema(); $schema = $this->getEntitySchema($entity_type, TRUE); foreach ($schema as $table_name => $table_schema) { @@ -89,11 +154,39 @@ public function onEntityTypeCreate(EntityTypeInterface $entity_type) { * {@inheritdoc} */ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) { - // @todo Implement proper updates: https://www.drupal.org/node/1498720. - // Meanwhile, treat a change from non-SQL storage to SQL storage as - // identical to creation with respect to SQL schema handling. - if (!is_subclass_of($original->getStorageClass(), '\Drupal\Core\Entity\Sql\SqlEntityStorageInterface')) { - $this->onEntityTypeCreate($entity_type); + $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->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. + $transaction = $this->database->startTransaction(); + } + try { + $this->onEntityTypeDelete($original); + $this->onEntityTypeCreate($entity_type); + } + catch (\Exception $e) { + if ($this->database->supportsTransactionalDDL()) { + $transaction->rollback(); + } + else { + // Recreate original schema. + $this->onEntityTypeCreate($original); + } + throw $e; + } + } + // Otherwise, throw an exception. + else { + throw new EntityStorageException(String::format('The SQL storage cannot change the schema for an existing entity type with data.')); } } @@ -101,9 +194,9 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI * {@inheritdoc} */ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { + $this->checkEntityType($entity_type); $schema_handler = $this->database->schema(); - $schema = $this->getEntitySchema($entity_type, TRUE); - foreach ($schema as $table_name => $table_schema) { + foreach ($this->getEntitySchemaTables() as $table_name) { if ($schema_handler->tableExists($table_name)) { $schema_handler->dropTable($table_name); } @@ -111,6 +204,51 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { } /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { + // @todo Move implementation from + // SqlContentEntityStorage::onFieldStorageDefinitionCreate() + // into here: https://www.drupal.org/node/1498720 + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $storage_definition, FieldStorageDefinitionInterface $original) { + // @todo Move implementation from + // SqlContentEntityStorage::onFieldStorageDefinitionUpdate() + // into here: https://www.drupal.org/node/1498720 + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $storage_definition) { + // @todo Move implementation from + // SqlContentEntityStorage::onFieldStorageDefinitionDelete() + // into here: https://www.drupal.org/node/1498720 + } + + /** + * 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 @@ -128,7 +266,7 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res if (!isset($this->schema[$entity_type_id]) || $reset) { // Initialize the table schema. - $tables = $this->getTables(); + $tables = $this->getEntitySchemaTables(); $schema[$tables['base_table']] = $this->initializeBaseTable(); if (isset($tables['revision_table'])) { $schema[$tables['revision_table']] = $this->initializeRevisionTable(); @@ -180,7 +318,7 @@ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $res * @return array * A list of entity type tables, keyed by table key. */ - protected function getTables() { + protected function getEntitySchemaTables() { return array_filter(array( 'base_table' => $this->storage->getBaseTable(), 'revision_table' => $this->storage->getRevisionTable(), @@ -619,4 +757,24 @@ protected function getEntityIndexName($index) { return $this->entityType->id() . '__' . $index; } + /** + * Checks whether a database table is non-existent or empty. + * + * 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 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 storage. - $entity_manager->getStorage($this->entity_type)->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 storage. 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. - $entity_manager->getStorage($this->entity_type)->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::entityManager()->getStorage($field->entity_type)->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 62752ae..fc0c6b7 100644 --- a/core/modules/system/src/Controller/DbUpdateController.php +++ b/core/modules/system/src/Controller/DbUpdateController.php @@ -285,6 +285,23 @@ protected function selection() { drupal_set_message($this->t('Some of the pending updates cannot be applied because their dependencies were not met.'), 'warning'); } + // 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); @@ -501,9 +518,18 @@ protected function triggerBatch(Request $request) { $this->state->set('system.maintenance_mode', TRUE); } - $start = $this->getModuleUpdates(); + $operations = array(); + + // 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 // be run and the order they will be run in. + $start = $this->getModuleUpdates(); $updates = update_resolve_dependencies($start); // Store the dependencies for each update function in an array which the @@ -515,7 +541,7 @@ protected function triggerBatch(Request $request) { $dependency_map[$function] = !empty($update['reverse_paths']) ? array_keys($update['reverse_paths']) : array(); } - $operations = array(); + // Determine updates to be performed. foreach ($updates as $update) { if ($update['allowed']) { // Set the installed version of each module so updates will start at the diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 8e7b534..b9c1b83 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -452,6 +452,11 @@ function system_requirements($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); }