Commit 32d5530e authored by alexpott's avatar alexpott

Issue #2333113 by effulgentsia, plach: Add an EntityDefinitionUpdateManager so...

Issue #2333113 by effulgentsia, plach: Add an EntityDefinitionUpdateManager so that entity handlers can respond (e.g., by updating db schema) to code updates in a controlled way (e.g., from update.php).
parent 22c1362c
......@@ -286,10 +286,18 @@ 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', '@entity.definitions.installed']
parent: container.trait
tags:
- { name: plugin_manager_cache_clear }
entity.definitions.installed:
class: Drupal\Core\KeyValueStore\KeyValueStoreInterface
factory_method: get
factory_service: keyvalue
arguments: ['entity.definitions.installed']
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']
......
......@@ -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.
......@@ -257,6 +251,33 @@ function update_do_one($module, $number, $dependency_map, &$context) {
$context['message'] = 'Updating ' . String::checkPlain($module) . ' module';
}
/**
* 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.
*
......
<?php
/**
* @file
* Contains \Drupal\Core\Entity\EntityDefinitionUpdateManager.
*/
namespace Drupal\Core\Entity;
use Drupal\Core\Entity\Schema\EntityStorageSchemaInterface;
use Drupal\Core\Entity\Schema\FieldableEntityStorageSchemaInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Manages entity definition updates.
*/
class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInterface {
use StringTranslationTrait;
/**
* Indicates that a definition has just been created.
*
* @var int
*/
const DEFINITION_CREATED = 1;
/**
* Indicates that a definition has changes.
*
* @var int
*/
const DEFINITION_UPDATED = 2;
/**
* Indicates that a definition has just been deleted.
*
* @var int
*/
const DEFINITION_DELETED = 3;
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a new EntityDefinitionUpdateManager.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(EntityManagerInterface $entity_manager) {
$this->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);
$summary[$entity_type_id][] = $this->t('Update the %entity_type entity type.', 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->getLastInstalledFieldStorageDefinitions($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('Create the %field_name field.', array('%field_name' => $storage_definitions[$field_name]->getLabel()));
break;
case static::DEFINITION_UPDATED:
$summary[$entity_type_id][] = $this->t('Update the %field_name field.', array('%field_name' => $storage_definitions[$field_name]->getLabel()));
break;
case static::DEFINITION_DELETED:
$summary[$entity_type_id][] = $this->t('Delete the %field_name field.', 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->getLastInstalledDefinition($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->getLastInstalledFieldStorageDefinitions($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($original_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->getLastInstalledDefinition($entity_type_id);
// Only manage changes to already installed entity types. Entity type
// installation is handled elsewhere (e.g.,
// \Drupal\Core\Extension\ModuleHandler::install()).
if (!$original) {
continue;
}
// @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->getLastInstalledFieldStorageDefinitions($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(EntityTypeInterface $entity_type, EntityTypeInterface $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);
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface.
*/
namespace Drupal\Core\Entity;
/**
* Defines an interface for managing entity definition updates.
*
* During the application lifetime, the definitions of various entity types and
* their data components (e.g., fields for fieldable entity types) can change.
* For example, updated code can be deployed. Some entity handlers may need to
* perform complex or long-running logic in response to the change. For
* example, a SQL-based storage handler may need to update the database schema.
*
* To support this, \Drupal\Core\Entity\EntityManagerInterface has methods to
* retrieve the last installed definitions as well as the definitions specified
* by the current codebase. It also has create/update/delete methods to bring
* the former up to date with the latter.
*
* However, it is not the responsibility of the entity manager to decide how to
* report the differences or when to apply each update. This interface is for
* managing that.
*
* @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition()
* @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions()
* @see \Drupal\Core\Entity\EntityTypeListenerInterface
* @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface
*/
interface EntityDefinitionUpdateManagerInterface {
/**
* Checks if there are any definition updates that need to be applied.
*
* @return bool
* TRUE if updates are needed.
*/
public function needsUpdates();
/**
* Returns a human readable summary of the detected changes.
*
* @return array
* An associative array keyed by entity type id. Each entry is an array of
* human-readable strings, each describing a change.
*/
public function getChangeSummary();
/**
* Applies all the detected valid changes.
*
* @throws \Drupal\Core\Entity\EntityStorageException
* This exception is thrown if a change cannot be applied without
* unacceptable data loss. In such a case, the site administrator needs to
* apply some other process, such as a custom update function or a
* migration via the Migrate module.
*/
public function applyUpdates();
}
......@@ -17,9 +17,12 @@
use Drupal\Core\Entity\Exception\AmbiguousEntityClassException;
use Drupal\Core\Entity\Exception\NoCorrespondingEntityClassException;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\FieldStorageDefinitionListenerInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\TypedData\TranslatableInterface;
......@@ -112,6 +115,13 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
*/
protected $languageManager;
/**
* The keyvalue collection for tracking installed definitions.
*
* @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
*/
protected $installedDefinitions;
/**
* Static cache of bundle information.
*
......@@ -169,8 +179,12 @@ class EntityManager extends DefaultPluginManager implements EntityManagerInterfa
* The string translationManager.
* @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
* The class resolver.
* @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
* The typed data manager.
* @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $installed_definitions
* The keyvalue collection for tracking installed definitions.
*/
public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManager $typed_data_manager) {
public function __construct(\Traversable $namespaces, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, TranslationInterface $translation_manager, ClassResolverInterface $class_resolver, TypedDataManager $typed_data_manager, KeyValueStoreInterface $installed_definitions) {
parent::__construct('Entity', $namespaces, $module_handler, 'Drupal\Core\Entity\EntityInterface', 'Drupal\Core\Entity\Annotation\EntityType');
$this->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->installedDefinitions = $installed_definitions;
}
/**
......@@ -190,6 +205,7 @@ public function clearCachedDefinitions() {
$this->clearCachedBundles();
$this->clearCachedFieldDefinitions();
$this->classNameEntityTypeMap = array();
$this->handlers = array();
}
/**
......@@ -961,36 +977,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->setLastInstalledDefinition($entity_type);
if ($entity_type->isFieldable()) {
$this->setLastInstalledFieldStorageDefinitions($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->setLastInstalledDefinition($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->deleteLastInstalledDefinition($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->setLastInstalledFieldStorageDefinition($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->setLastInstalledFieldStorageDefinition($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->deleteLastInstalledFieldStorageDefinition($storage_definition);
$this->clearCachedFieldDefinitions();
}
/**
......@@ -1047,4 +1129,81 @@ public function onBundleDelete($entity_type_id, $bundle) {
$this->clearCachedFieldDefinitions();
}
/**
* {@inheritdoc}
*/
public function getLastInstalledDefinition($entity_type_id) {
return $this->installedDefinitions->get($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 setLastInstalledDefinition(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$this->installedDefinitions->set($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 deleteLastInstalledDefinition($entity_type_id) {
$this->installedDefinitions->delete($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->installedDefinitions->delete($entity_type_id . '.field_storage_definitions');
}
/**
* {@inheritdoc}
*/
public function getLastInstalledFieldStorageDefinitions($entity_type_id) {
return $this->installedDefinitions->get($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 setLastInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) {
$this->installedDefinitions->set($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.