diff --git a/core/core.services.yml b/core/core.services.yml index df50344..7ae1cd4 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -575,7 +575,7 @@ services: - { name: event_subscriber } entity.definition_update_manager: class: Drupal\Core\Entity\EntityDefinitionUpdateManager - arguments: ['@entity.manager', '@entity.last_installed_schema.repository'] + arguments: ['@entity.manager', '@entity.last_installed_schema.repository', '@entity_type.listener'] entity.last_installed_schema.repository: class: Drupal\Core\Entity\EntityLastInstalledSchemaRepository arguments: ['@keyvalue'] diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php index c5d0abd..f551fdd 100644 --- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php @@ -29,14 +29,23 @@ class EntityDefinitionUpdateManager implements EntityDefinitionUpdateManagerInte protected $entityLastInstalledSchemaRepository; /** + * The entity type listener service. + * + * @var \Drupal\Core\Entity\EntityTypeListener + */ + protected $entityTypeListener; + + /** * Constructs a new EntityDefinitionUpdateManager. * * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager. * @param \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository * The last installed schema repository service. + * @param \Drupal\Core\Entity\EntityTypeListener $entity_type_listener + * The entity type listener service. */ - public function __construct(EntityManagerInterface $entity_manager, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository = NULL) { + public function __construct(EntityManagerInterface $entity_manager, EntityLastInstalledSchemaRepositoryInterface $entity_last_installed_schema_repository = NULL, $entity_type_listener = NULL) { $this->entityManager = $entity_manager; if (!isset($entity_last_installed_schema_repository)) { @@ -44,6 +53,12 @@ public function __construct(EntityManagerInterface $entity_manager, EntityLastIn $entity_last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); } $this->entityLastInstalledSchemaRepository = $entity_last_installed_schema_repository; + + if (!isset($entity_type_listener)) { + @trigger_error('The $entity_type_listener parameter was added in Drupal 8.6.x and will be required in 9.0.0. See https://www.drupal.org/node/2973262.', E_USER_DEPRECATED); + $entity_type_listener = \Drupal::service('entity_type.listener'); + } + $this->entityTypeListener = $entity_type_listener; } /** @@ -154,7 +169,7 @@ public function getEntityTypes() { */ public function installEntityType(EntityTypeInterface $entity_type) { $this->entityManager->clearCachedDefinitions(); - $this->entityManager->onEntityTypeCreate($entity_type); + $this->entityTypeListener->onEntityTypeCreate($entity_type); } /** @@ -163,7 +178,7 @@ public function installEntityType(EntityTypeInterface $entity_type) { public function updateEntityType(EntityTypeInterface $entity_type) { $original = $this->getEntityType($entity_type->id()); $this->entityManager->clearCachedDefinitions(); - $this->entityManager->onEntityTypeUpdate($entity_type, $original); + $this->entityTypeListener->onEntityTypeUpdate($entity_type, $original); } /** @@ -171,7 +186,21 @@ public function updateEntityType(EntityTypeInterface $entity_type) { */ public function uninstallEntityType(EntityTypeInterface $entity_type) { $this->entityManager->clearCachedDefinitions(); - $this->entityManager->onEntityTypeDelete($entity_type); + $this->entityTypeListener->onEntityTypeDelete($entity_type); + } + + /** + * {@inheritdoc} + */ + public function updateEntityTypeSchema(EntityTypeInterface $entity_type, array $field_storage_definitions, array &$sandbox = NULL) { + $original = $this->getEntityType($entity_type->id()); + + if ($this->requiresEntityDataMigration($entity_type, $original) && $sandbox === NULL) { + throw new \InvalidArgumentException('The entity schema update for the ' . $entity_type->id() . ' entity type requires a data migration.'); + } + + $original_field_storage_definitions = $original_storage_definitions = $this->entityLastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($entity_type->id()); + $this->entityTypeListener->onEntityTypeSchemaUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox); } /** @@ -373,4 +402,20 @@ protected function requiresFieldStorageSchemaChanges(FieldStorageDefinitionInter return ($storage instanceof DynamicallyFieldableEntityStorageSchemaInterface) && $storage->requiresFieldStorageSchemaChanges($storage_definition, $original); } + /** + * Checks if existing data would be lost if the schema changes were applied. + * + * @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 data migration is required, FALSE otherwise. + */ + protected function requiresEntityDataMigration(EntityTypeInterface $entity_type, EntityTypeInterface $original) { + $storage = $this->entityManager->getStorage($entity_type->id()); + return ($storage instanceof EntityStorageSchemaInterface) && $storage->requiresEntityDataMigration($entity_type, $original); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php index a219f3c..3494b60 100644 --- a/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php @@ -145,6 +145,20 @@ public function updateEntityType(EntityTypeInterface $entity_type); public function uninstallEntityType(EntityTypeInterface $entity_type); /** + * Applies any schema change performed to the passed entity type definition. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The updated entity type definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions + * The updated field storage definitions, including possibly new ones. + * @param array &$sandbox + * (optional) A sandbox array provided by a hook_update_N() implementation + * or a Batch API callback. If the entity schema update requires a data + * migration, this parameter is mandatory. Defaults to NULL. + */ + public function updateEntityTypeSchema(EntityTypeInterface $entity_type, array $field_storage_definitions, array &$sandbox = NULL); + + /** * Returns a field storage definition ready to be manipulated. * * When needing to apply updates to existing field storage definitions, this diff --git a/core/lib/Drupal/Core/Entity/EntityTypeListener.php b/core/lib/Drupal/Core/Entity/EntityTypeListener.php index 4dc1e9c..c8b23f6 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeListener.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeListener.php @@ -9,7 +9,7 @@ * * @see \Drupal\Core\Entity\EntityTypeEvents */ -class EntityTypeListener implements EntityTypeListenerInterface { +class EntityTypeListener implements EntityTypeListenerInterface, EntityTypeSchemaListenerInterface { /** * The entity type manager. @@ -115,4 +115,27 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { $this->entityLastInstalledSchemaRepository->deleteLastInstalledDefinition($entity_type_id); } + /** + * {@inheritdoc} + */ + public function onEntityTypeSchemaUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) { + $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->entityTypeManager->createHandlerInstance($entity_type->getStorageClass(), $entity_type); + if ($storage instanceof EntityTypeSchemaListenerInterface) { + $storage->onEntityTypeSchemaUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox); + } + + if ($sandbox === NULL || (isset($sandbox['#finished']) && $sandbox['#finished'] === 1)) { + $this->eventDispatcher->dispatch(EntityTypeEvents::UPDATE, new EntityTypeEvent($entity_type, $original)); + + $this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type); + if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) { + $this->entityLastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($entity_type_id, $field_storage_definitions); + } + } + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityTypeSchemaListenerInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeSchemaListenerInterface.php new file mode 100644 index 0000000..d053104 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/EntityTypeSchemaListenerInterface.php @@ -0,0 +1,28 @@ +entityManager->createHandlerInstance($entity_type->getStorageClass(), $entity_type); + $sandbox['original_storage'] = $this->entityManager->createHandlerInstance($original->getStorageClass(), $original); + + $this->preUpdateEntityTypeSchema($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox); + } + + // Copy data from the original storage to the temporary one. + if ($sandbox['original_storage']->hasData()) { + $this->copyData($entity_type, $original, $sandbox); + } + else { + // If there is no existing data, we still need to run the + // post-schema-update tasks. + $sandbox['#finished'] = 1; + } + + // If the data copying has finished successfully, allow the storage schema + // to do any required cleanup tasks. For example, this process should take + // care of transforming the temporary storage into the current storage. + if ($sandbox['#finished'] === 1) { + $this->postUpdateEntityTypeSchema($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox); + } + } + + /** + * Allows subscribers to prepare their schema before data copying. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The updated entity type definition. + * @param \Drupal\Core\Entity\EntityTypeInterface $original + * The original entity type definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions + * The updated field storage definitions, including possibly new ones. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $original_field_storage_definitions + * The original field storage definitions. + * @param array &$sandbox + * (optional) A sandbox array provided by a hook_update_N() implementation + * or a Batch API callback. If the entity schema update requires a data + * migration, this parameter is mandatory. Defaults to NULL. + */ + abstract protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL); + + /** + * Allows subscribers to do any cleanup necessary after data copying. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The updated entity type definition. + * @param \Drupal\Core\Entity\EntityTypeInterface $original + * The original entity type definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions + * The updated field storage definitions, including possibly new ones. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $original_field_storage_definitions + * The original field storage definitions. + * @param array &$sandbox + * (optional) A sandbox array provided by a hook_update_N() implementation + * or a Batch API callback. If the entity schema update requires a data + * migration, this parameter is mandatory. Defaults to NULL. + */ + abstract protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL); + + /** + * Copies entity data from the original storage to the temporary one. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The updated entity type definition. + * @param \Drupal\Core\Entity\EntityTypeInterface $original + * The original entity type definition. + * @param array &$sandbox + * The sandbox array from a hook_update_N() implementation. + */ + protected function copyData(EntityTypeInterface $entity_type, EntityTypeInterface $original, array &$sandbox) { + /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */ + $id_key = $entity_type->getKey('id'); + $revision_id_key = $entity_type->getKey('revision'); + $revision_default_key = $entity_type->getRevisionMetadataKey('revision_default'); + $langcode_key = $entity_type->getKey('langcode'); + $default_langcode_key = $entity_type->getKey('default_langcode'); + $revision_translation_affected_key = $entity_type->getKey('revision_translation_affected'); + + $temporary_storage = $sandbox['temporary_storage']; + $original_storage = $sandbox['original_storage']; + + // If both the original entity type and the updated one are revisionable, we + // need to handle all their revisions. + $load_revisions = $original->isRevisionable() && $entity_type->isRevisionable(); + + // If 'progress' is not set, then this will be the first run of the batch. + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['current_id'] = 0; + if ($load_revisions) { + $sandbox['max'] = (int) $original_storage->getQuery()->allRevisions()->count()->execute(); + } + else { + $sandbox['max'] = (int) $original_storage->getQuery()->count()->execute(); + } + } + + // Get the next entity identifiers to migrate. + $step_size = Settings::get('entity_update_batch_size', 50); + $query = $original_storage->getQuery(); + if ($load_revisions) { + $query->allRevisions(); + $identifier_field = $revision_id_key; + } + else { + $identifier_field = $id_key; + } + $entity_identifiers = $query + ->condition($identifier_field, $sandbox['current_id'], '>') + ->sort($identifier_field, 'ASC') + ->range(0, $step_size) + ->execute(); + + $entities = $load_revisions ? $original_storage->loadMultipleRevisions(array_keys($entity_identifiers)) : $original_storage->loadMultiple($entity_identifiers); + foreach ($entities as $entity_id => $entity) { + try { + if (!$original->isRevisionable() && $entity_type->isRevisionable()) { + // Set the revision ID to be same as the entity ID. + $entity->set($revision_id_key, $entity_id); + + // We had no revisions so far, so the existing data belongs to the + // default revision now. + $entity->set($revision_default_key, TRUE); + } + + // Set the 'langcode' and 'default_langcode' values as needed. + if (!$original->isTranslatable() && $entity_type->isTranslatable()) { + $original_has_langcode = $entity->hasField($langcode_key) && !$entity->get($langcode_key)->isEmpty(); + $langcode = $original_has_langcode ? $entity->get($langcode_key) : \Drupal::languageManager()->getDefaultLanguage()->getId(); + + $entity->set($langcode_key, $langcode); + $entity->set($default_langcode_key, $langcode); + } + + // Set the 'revision_translation_affected' flag to TRUE to match the + // previous API return value: if the field was not defined the value + // returned was always TRUE. + if (!$original->isRevisionable() && !$original->isTranslatable() && $entity_type->isRevisionable() && $entity_type->isTranslatable()) { + $entity->set($revision_translation_affected_key, TRUE); + } + + // Treat the entity as new in order to make the storage do an INSERT + // rather than an UPDATE. + $entity->enforceIsNew(TRUE); + + // Finally, save the entity in the temporary storage. + $temporary_storage->save($entity); + } + catch (\Exception $e) { + $this->handleEntityTypeSchemaUpdateExceptionOnDataCopy($entity_type, $original, $sandbox); + + // Re-throw the original exception with a helpful message. + throw new EntityStorageException("The entity update process failed while processing the entity {$entity_type->id()}:$entity_id.", $e->getCode(), $e); + } + + $sandbox['progress']++; + $sandbox['current_id'] = $entity_id; + } + + // If we're not in maintenance mode, the number of entities could change at + // any time so make sure that we always use the latest record count. + if ($load_revisions) { + $sandbox['max'] = (int) $original_storage->getQuery()->allRevisions()->count()->execute(); + } + else { + $sandbox['max'] = (int) $original_storage->getQuery()->count()->execute(); + } + + $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); + } + + /** + * Handles the case when an error occurs during the data copying step. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The updated entity type definition. + * @param \Drupal\Core\Entity\EntityTypeInterface $original + * The original entity type definition. + * @param array &$sandbox + * The sandbox array from a hook_update_N() implementation. + */ + protected function handleEntityTypeSchemaUpdateExceptionOnDataCopy(EntityTypeInterface $entity_type, EntityTypeInterface $original, array &$sandbox) { + } + +} diff --git a/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php index 4feab7b..72f1682 100644 --- a/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php +++ b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php @@ -4,6 +4,7 @@ use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeListenerInterface; +use Drupal\Core\Entity\EntityTypeSchemaListenerInterface; /** * Defines the interface for entity storage schema handler classes. @@ -18,7 +19,7 @@ * * @see \Drupal\Core\Entity\EntityStorageInterface */ -interface EntityStorageSchemaInterface extends EntityTypeListenerInterface { +interface EntityStorageSchemaInterface extends EntityTypeListenerInterface, EntityTypeSchemaListenerInterface { /** * Checks if the changes to the entity type requires storage schema changes. diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php index ed01f22..64c5d2e 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -119,13 +119,6 @@ class SqlContentEntityStorage extends ContentEntityStorageBase implements SqlEnt protected $languageManager; /** - * Whether this storage should use the temporary table mapping. - * - * @var bool - */ - protected $temporary = FALSE; - - /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { @@ -295,18 +288,6 @@ public function setTableMapping(TableMappingInterface $table_mapping) { } /** - * Changes the temporary state of the storage. - * - * @param bool $temporary - * Whether to use a temporary table mapping or not. - * - * @internal Only to be used internally by Entity API. - */ - public function setTemporary($temporary) { - $this->temporary = $temporary; - } - - /** * {@inheritdoc} */ public function getTableMapping(array $storage_definitions = NULL) { @@ -346,7 +327,6 @@ public function getTableMapping(array $storage_definitions = NULL) { * @internal */ public function getCustomTableMapping(ContentEntityTypeInterface $entity_type, array $storage_definitions, $prefix = '') { - $prefix = $prefix ?: ($this->temporary ? 'tmp_' : ''); return DefaultTableMapping::create($entity_type, $storage_definitions, $prefix); } @@ -1418,6 +1398,15 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { /** * {@inheritdoc} */ + public function onEntityTypeSchemaUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) { + $this->wrapSchemaException(function () use ($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, &$sandbox) { + $this->getStorageSchema()->onEntityTypeSchemaUpdate($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox); + }); + } + + /** + * {@inheritdoc} + */ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { $this->wrapSchemaException(function () use ($storage_definition) { $this->getStorageSchema()->onFieldStorageDefinitionCreate($storage_definition); diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php index c73a28c..897efbc 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchema.php @@ -10,6 +10,7 @@ use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeSchemaListenerTrait; use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; use Drupal\Core\Entity\Schema\DynamicallyFieldableEntityStorageSchemaInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -28,6 +29,7 @@ class SqlContentEntityStorageSchema implements DynamicallyFieldableEntityStorageSchemaInterface { use DependencySerializationTrait; + use EntityTypeSchemaListenerTrait; /** * The entity manager. @@ -411,6 +413,132 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) { /** * {@inheritdoc} */ + protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) { + // Make sure that each storage object has a proper table mapping. + /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $temporary_storage */ + $temporary_storage = &$sandbox['temporary_storage']; + $temporary_table_mapping = $temporary_storage->getCustomTableMapping($entity_type, $field_storage_definitions, 'tmp_'); + $temporary_storage->setTableMapping($temporary_table_mapping); + + /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $original_storage */ + $original_storage = &$sandbox['original_storage']; + $original_table_mapping = $original_storage->getCustomTableMapping($original, $original_field_storage_definitions); + $original_storage->setTableMapping($original_table_mapping); + + $sandbox['current_table_mapping'] = $temporary_storage->getCustomTableMapping($entity_type, $field_storage_definitions); + $sandbox['backup_table_mapping'] = $original_storage->getCustomTableMapping($original, $original_field_storage_definitions, 'old_'); + + // Create temporary tables based on the new entity type and field storage + // definitions. + $schema_handler = $this->database->schema(); + + // Create entity tables. + $temporary_table_names = array_combine($sandbox['current_table_mapping']->getTableNames(), $temporary_table_mapping->getTableNames()); + $schema = $this->buildEntitySchema($entity_type, $field_storage_definitions); + foreach ($schema as $table_name => $table_schema) { + $table_name = $temporary_table_names[$table_name]; + if (!$schema_handler->tableExists($table_name)) { + $schema_handler->createTable($table_name, $table_schema); + } + } + + // Create dedicated field tables. + foreach ($field_storage_definitions as $field_storage_definition) { + if ($temporary_table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { + $schema = $this->getDedicatedTableSchema($field_storage_definition); + foreach ($schema as $table_name => $table_schema) { + $table_name = $temporary_table_names[$table_name]; + if (!$schema_handler->tableExists($table_name)) { + $schema_handler->createTable($table_name, $table_schema); + } + } + } + } + } + + /** + * {@inheritdoc} + */ + protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) { + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $temporary_table_mapping */ + $temporary_table_mapping = $sandbox['temporary_storage']->getTableMapping(); + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $original_table_mapping */ + $original_table_mapping = $sandbox['original_storage']->getTableMapping(); + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $current_table_mapping */ + $current_table_mapping = $sandbox['current_table_mapping']; + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $backup_table_mapping */ + $backup_table_mapping = $sandbox['backup_table_mapping']; + + // Rename the original tables so we can put them back in place in case + // anything goes wrong. + $backup_table_names = array_combine($original_table_mapping->getTableNames(), $backup_table_mapping->getTableNames()); + foreach ($backup_table_names as $original_table_name => $backup_table_name) { + $this->database->schema()->renameTable($original_table_name, $backup_table_name); + } + + // Put the new tables in place and update the entity type and field storage + // definitions. + try { + $table_name_mapping = array_combine($temporary_table_mapping->getTableNames(), $current_table_mapping->getTableNames()); + foreach ($table_name_mapping as $temp_table_name => $current_table_name) { + $this->database->schema()->renameTable($temp_table_name, $current_table_name); + } + + // Store the updated entity schema. + $entity_schema = $this->buildEntitySchema($entity_type, $field_storage_definitions); + $this->saveEntitySchemaData($entity_type, $entity_schema); + + // Store the updated field schema for each field storage. + foreach ($field_storage_definitions as $field_storage_definition) { + if ($current_table_mapping->requiresDedicatedTableStorage($field_storage_definition)) { + $this->createDedicatedTableSchema($field_storage_definition, TRUE); + } + elseif ($current_table_mapping->allowsSharedTableStorage($field_storage_definition)) { + // The shared tables are already fully created, but we need to save the + // per-field schema definitions for later use. + $this->createSharedTableSchema($field_storage_definition, TRUE); + } + } + } + catch (\Exception $e) { + // Something went wrong, bring back the original tables. + foreach ($backup_table_names as $original_table_name => $backup_table_name) { + // We are in the 'original data recovery' phase, so we need to be sure + // that the initial tables can be properly restored. + if ($this->database->schema()->tableExists($original_table_name)) { + $this->database->schema()->dropTable($original_table_name); + } + + $this->database->schema()->renameTable($backup_table_name, $original_table_name); + } + + // Re-throw the original exception. + throw $e; + } + + // At this point the update process either finished successfully or any + // error has been thrown already, so we can drop the backup entity tables. + foreach ($backup_table_names as $original_table_name => $backup_table_name) { + $this->database->schema()->dropTable($backup_table_name); + } + } + + /** + * {@inheritdoc} + */ + protected function handleEntityTypeSchemaUpdateExceptionOnDataCopy(EntityTypeInterface $entity_type, EntityTypeInterface $original, array &$sandbox) { + // In case of an error during the save process, we need to clean up the + // temporary tables. + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $temporary_table_mapping */ + $temporary_table_mapping = $sandbox['temporary_storage']->getTableMapping(); + foreach ($temporary_table_mapping->getTableNames() as $table_name) { + $this->database->schema()->dropTable($table_name); + } + } + + /** + * {@inheritdoc} + */ public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $storage_definition) { $this->performFieldSchemaOperation('create', $storage_definition); } @@ -657,97 +785,118 @@ protected function checkEntityType(EntityTypeInterface $entity_type) { * @return array * A Schema API array describing the entity schema, excluding dedicated * field tables. - * - * @throws \Drupal\Core\Field\FieldException */ protected function getEntitySchema(ContentEntityTypeInterface $entity_type, $reset = FALSE) { $this->checkEntityType($entity_type); $entity_type_id = $entity_type->id(); if (!isset($this->schema[$entity_type_id]) || $reset) { - // Prepare basic information about the entity type. - $tables = $this->getEntitySchemaTables(); + $this->schema[$entity_type_id] = $this->buildEntitySchema($entity_type, $this->fieldStorageDefinitions); + } - // Initialize the table schema. - $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type); - if (isset($tables['revision_table'])) { - $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type); - } - if (isset($tables['data_table'])) { - $schema[$tables['data_table']] = $this->initializeDataTable($entity_type); - } - if (isset($tables['revision_data_table'])) { - $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type); - } + return $this->schema[$entity_type_id]; + } - // We need to act only on shared entity schema tables. - $table_mapping = $this->storage->getCustomTableMapping($entity_type, $this->fieldStorageDefinitions); - $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()); - foreach ($table_names as $table_name) { - if (!isset($schema[$table_name])) { - $schema[$table_name] = []; + /** + * Builds the entity schema for the specified entity type. + * + * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions + * The field storage definitions. + * + * @return array + * A Schema API array describing the entity schema, excluding dedicated + * field tables. + * + * @throws \Drupal\Core\Field\FieldException + */ + protected function buildEntitySchema(ContentEntityTypeInterface $entity_type, array $field_storage_definitions) { + // Prepare basic information about the entity type. + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $this->storage->getCustomTableMapping($entity_type, $field_storage_definitions); + $tables = $this->getEntitySchemaTables($table_mapping); + + // Initialize the table schema. + $schema[$tables['base_table']] = $this->initializeBaseTable($entity_type); + if (isset($tables['revision_table'])) { + $schema[$tables['revision_table']] = $this->initializeRevisionTable($entity_type); + } + if (isset($tables['data_table'])) { + $schema[$tables['data_table']] = $this->initializeDataTable($entity_type); + } + if (isset($tables['revision_data_table'])) { + $schema[$tables['revision_data_table']] = $this->initializeRevisionDataTable($entity_type); + } + + // We need to act only on shared entity schema tables. + $table_names = array_diff($table_mapping->getTableNames(), $table_mapping->getDedicatedTableNames()); + foreach ($table_names as $table_name) { + if (!isset($schema[$table_name])) { + $schema[$table_name] = []; + } + foreach ($table_mapping->getFieldNames($table_name) as $field_name) { + if (!isset($field_storage_definitions[$field_name])) { + throw new FieldException("Field storage definition for '$field_name' could not be found."); } - foreach ($table_mapping->getFieldNames($table_name) as $field_name) { - if (!isset($this->fieldStorageDefinitions[$field_name])) { - throw new FieldException("Field storage definition for '$field_name' could not be found."); - } - // Add the schema for base field definitions. - elseif ($table_mapping->allowsSharedTableStorage($this->fieldStorageDefinitions[$field_name])) { - $column_names = $table_mapping->getColumnNames($field_name); - $storage_definition = $this->fieldStorageDefinitions[$field_name]; - $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names)); - } + // Add the schema for base field definitions. + elseif ($table_mapping->allowsSharedTableStorage($field_storage_definitions[$field_name])) { + $column_names = $table_mapping->getColumnNames($field_name); + $storage_definition = $field_storage_definitions[$field_name]; + $schema[$table_name] = array_merge_recursive($schema[$table_name], $this->getSharedTableFieldSchema($storage_definition, $table_name, $column_names)); } } + } - // Process tables after having gathered field information. - $this->processBaseTable($entity_type, $schema[$tables['base_table']]); - if (isset($tables['revision_table'])) { - $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]); - } - if (isset($tables['data_table'])) { - $this->processDataTable($entity_type, $schema[$tables['data_table']]); - } - if (isset($tables['revision_data_table'])) { - $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]); - } + // Process tables after having gathered field information. + $this->processBaseTable($entity_type, $schema[$tables['base_table']]); + if (isset($tables['revision_table'])) { + $this->processRevisionTable($entity_type, $schema[$tables['revision_table']]); + } + if (isset($tables['data_table'])) { + $this->processDataTable($entity_type, $schema[$tables['data_table']]); + } + if (isset($tables['revision_data_table'])) { + $this->processRevisionDataTable($entity_type, $schema[$tables['revision_data_table']]); + } - // Add an index for the 'published' entity key. - if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) { - $published_key = $entity_type->getKey('published'); - if ($published_key && !$this->fieldStorageDefinitions[$published_key]->hasCustomStorage()) { - $published_field_table = $table_mapping->getFieldTableName($published_key); - $id_key = $entity_type->getKey('id'); - if ($bundle_key = $entity_type->getKey('bundle')) { - $key = "{$published_key}_{$bundle_key}"; - $columns = [$published_key, $bundle_key, $id_key]; - } - else { - $key = $published_key; - $columns = [$published_key, $id_key]; - } - $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns; + // Add an index for the 'published' entity key. + if (is_subclass_of($entity_type->getClass(), EntityPublishedInterface::class)) { + $published_key = $entity_type->getKey('published'); + if ($published_key && !$field_storage_definitions[$published_key]->hasCustomStorage()) { + $published_field_table = $table_mapping->getFieldTableName($published_key); + $id_key = $entity_type->getKey('id'); + if ($bundle_key = $entity_type->getKey('bundle')) { + $key = "{$published_key}_{$bundle_key}"; + $columns = [$published_key, $bundle_key, $id_key]; + } + else { + $key = $published_key; + $columns = [$published_key, $id_key]; } + $schema[$published_field_table]['indexes'][$this->getEntityIndexName($entity_type, $key)] = $columns; } - - $this->schema[$entity_type_id] = $schema; } - return $this->schema[$entity_type_id]; + return $schema; } /** * Gets a list of entity type tables. * + * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping + * A table mapping object. + * * @return array * A list of entity type tables, keyed by table key. */ - protected function getEntitySchemaTables() { + protected function getEntitySchemaTables(TableMappingInterface $table_mapping) { + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ return array_filter([ - 'base_table' => $this->storage->getBaseTable(), - 'revision_table' => $this->storage->getRevisionTable(), - 'data_table' => $this->storage->getDataTable(), - 'revision_data_table' => $this->storage->getRevisionDataTable(), + 'base_table' => $table_mapping->getBaseTable(), + 'revision_table' => $table_mapping->getRevisionTable(), + 'data_table' => $table_mapping->getDataTable(), + 'revision_data_table' => $table_mapping->getRevisionDataTable(), ]); } @@ -1293,14 +1442,22 @@ protected function performFieldSchemaOperation($operation, FieldStorageDefinitio * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The storage definition of the field being created. + * @param bool $only_save + * (optional) Whether to skip modification of database tables and only save + * the schema data for future comparison. For internal use only. This is + * used by postUpdateEntityTypeSchema() after it has already fully created + * the dedicated tables. */ - protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition) { + protected function createDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, $only_save = FALSE) { $schema = $this->getDedicatedTableSchema($storage_definition); - foreach ($schema as $name => $table) { - // Check if the table exists because it might already have been - // created as part of the earlier entity type update event. - if (!$this->database->schema()->tableExists($name)) { - $this->database->schema()->createTable($name, $table); + + if (!$only_save) { + foreach ($schema as $name => $table) { + // Check if the table exists because it might already have been + // created as part of the earlier entity type update event. + if (!$this->database->schema()->tableExists($name)) { + $this->database->schema()->createTable($name, $table); + } } } $this->saveFieldSchemaData($storage_definition, $schema); @@ -1944,8 +2101,8 @@ protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field storage definition. * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type - * (optional) The entity type definition. Defaults to the one returned by - * the entity manager. + * (optional) The entity type definition. Defaults to the one provided by + * the entity storage. * * @return array * The schema definition for the table with the following keys: @@ -1960,10 +2117,11 @@ protected function addSharedTableFieldForeignKey(FieldStorageDefinitionInterface * @see hook_schema() */ protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $storage_definition, ContentEntityTypeInterface $entity_type = NULL) { + $entity_type = $entity_type ?: $this->entityType; $description_current = "Data storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; $description_revision = "Revision archive storage for {$storage_definition->getTargetEntityTypeId()} field {$storage_definition->getName()}."; - $id_definition = $this->fieldStorageDefinitions[$this->entityType->getKey('id')]; + $id_definition = $this->fieldStorageDefinitions[$entity_type->getKey('id')]; if ($id_definition->getType() == 'integer') { $id_schema = [ 'type' => 'int', @@ -1982,11 +2140,11 @@ protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $stor } // Define the revision ID schema. - if (!$this->entityType->isRevisionable()) { + if (!$entity_type->isRevisionable()) { $revision_id_schema = $id_schema; $revision_id_schema['description'] = 'The entity revision id this data is attached to, which for an unversioned entity type is the same as the entity id'; } - elseif ($this->fieldStorageDefinitions[$this->entityType->getKey('revision')]->getType() == 'integer') { + elseif ($this->fieldStorageDefinitions[$entity_type->getKey('revision')]->getType() == 'integer') { $revision_id_schema = [ 'type' => 'int', 'unsigned' => TRUE, @@ -2110,7 +2268,6 @@ protected function getDedicatedTableSchema(FieldStorageDefinitionInterface $stor $dedicated_table_schema = [$table_mapping->getDedicatedDataTableName($storage_definition) => $data_schema]; // If the entity type is revisionable, construct the revision table. - $entity_type = $entity_type ?: $this->entityType; if ($entity_type->isRevisionable()) { $revision_schema = $data_schema; $revision_schema['description'] = $description_revision; diff --git a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php index 7d8f53b..68afc5b 100644 --- a/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorageSchemaConverter.php @@ -3,14 +3,11 @@ namespace Drupal\Core\Entity\Sql; use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface; use Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface; -use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\KeyValueStore\KeyValueStoreInterface; -use Drupal\Core\Site\Settings; use Drupal\Core\StringTranslation\TranslatableMarkup; /** @@ -121,269 +118,29 @@ public function __construct($entity_type_id, EntityTypeManagerInterface $entity_ * Re-throws any exception raised during the update process. */ public function convertToRevisionable(array &$sandbox, array $fields_to_update = []) { - // If 'progress' is not set, then this will be the first run of the batch. - if (!isset($sandbox['progress'])) { - // Store the original entity type and field definitions in the $sandbox - // array so we can use them later in the update process. - $this->collectOriginalDefinitions($sandbox); + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); - // Create a temporary environment in which the new data will be stored. - $this->createTemporaryDefinitions($sandbox, $fields_to_update); - - // Create the updated entity schema using temporary tables. - /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */ - $storage = $this->entityTypeManager->getStorage($this->entityTypeId); - $storage->setTemporary(TRUE); - $storage->setEntityType($sandbox['temporary_entity_type']); - $storage->onEntityTypeCreate($sandbox['temporary_entity_type']); - } - - // Copy over the existing data to the new temporary tables. - $this->copyData($sandbox); - - // If the data copying has finished successfully, we can drop the temporary - // tables and call the appropriate update mechanisms. - if ($sandbox['#finished'] == 1) { - $this->entityTypeManager->useCaches(FALSE); - $actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId); - - // Rename the original tables so we can put them back in place in case - // anything goes wrong. - $backup_table_names = array_combine($sandbox['original_table_mapping']->getTableNames(), $sandbox['backup_table_mapping']->getTableNames()); - foreach ($backup_table_names as $original_table_name => $backup_table_name) { - $this->database->schema()->renameTable($original_table_name, $backup_table_name); - } - - // Put the new tables in place and update the entity type and field - // storage definitions. - try { - $storage = $this->entityTypeManager->createHandlerInstance($actual_entity_type->getStorageClass(), $actual_entity_type); - $current_table_mapping = $storage->getCustomTableMapping($actual_entity_type, $sandbox['updated_storage_definitions']); - - $table_name_mapping = array_combine($sandbox['temporary_table_mapping']->getTableNames(), $current_table_mapping->getTableNames()); - foreach ($table_name_mapping as $temp_table_name => $new_table_name) { - $this->database->schema()->renameTable($temp_table_name, $new_table_name); - } - - // Rename the tables in the cached entity schema data. - $entity_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []); - foreach ($entity_schema_data as $temp_table_name => $schema) { - if (isset($table_name_mapping[$temp_table_name])) { - $entity_schema_data[$table_name_mapping[$temp_table_name]] = $schema; - unset($entity_schema_data[$temp_table_name]); - } - } - $this->installedStorageSchema->set($this->entityTypeId . '.entity_schema_data', $entity_schema_data); - - // Rename the tables in the cached field schema data. - foreach ($sandbox['updated_storage_definitions'] as $storage_definition) { - $field_schema_data = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []); - foreach ($field_schema_data as $temp_table_name => $schema) { - if (isset($table_name_mapping[$temp_table_name])) { - $field_schema_data[$table_name_mapping[$temp_table_name]] = $schema; - unset($field_schema_data[$temp_table_name]); - } - } - $this->installedStorageSchema->set($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), $field_schema_data); - } - - // Instruct the entity schema handler that data migration has been - // handled already and update the entity type. - $actual_entity_type->set('requires_data_migration', FALSE); - $this->entityDefinitionUpdateManager->updateEntityType($actual_entity_type); - - // Update the field storage definitions. - $this->updateFieldStorageDefinitionsToRevisionable($actual_entity_type, $sandbox['original_storage_definitions'], $fields_to_update); - } - catch (\Exception $e) { - // Something went wrong, bring back the original tables. - foreach ($backup_table_names as $original_table_name => $backup_table_name) { - // We are in the 'original data recovery' phase, so we need to be sure - // that the initial tables can be properly restored. - if ($this->database->schema()->tableExists($original_table_name)) { - $this->database->schema()->dropTable($original_table_name); - } - - $this->database->schema()->renameTable($backup_table_name, $original_table_name); - } - - // Re-throw the original exception. - throw $e; - } - - // At this point the update process either finished successfully or any - // error has been handled already, so we can drop the backup entity - // tables. - foreach ($backup_table_names as $original_table_name => $backup_table_name) { - $this->database->schema()->dropTable($backup_table_name); - } - } - } - - /** - * Loads entities from the original storage and saves them to a temporary one. - * - * @param array &$sandbox - * The sandbox array from a hook_update_N() implementation. - * - * @throws \Drupal\Core\Entity\EntityStorageException - * Thrown in case of an error during the entity save process. - */ - protected function copyData(array &$sandbox) { - /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $temporary_table_mapping */ - $temporary_table_mapping = $sandbox['temporary_table_mapping']; - $temporary_entity_type = $sandbox['temporary_entity_type']; - $original_table_mapping = $sandbox['original_table_mapping']; - $original_entity_type = $sandbox['original_entity_type']; - - $original_base_table = $original_entity_type->getBaseTable(); - - $revision_id_key = $temporary_entity_type->getKey('revision'); - $revision_default_key = $temporary_entity_type->getRevisionMetadataKey('revision_default'); - $revision_translation_affected_key = $temporary_entity_type->getKey('revision_translation_affected'); - - // If 'progress' is not set, then this will be the first run of the batch. - if (!isset($sandbox['progress'])) { - $sandbox['progress'] = 0; - $sandbox['current_id'] = 0; - $sandbox['max'] = $this->database->select($original_base_table) - ->countQuery() - ->execute() - ->fetchField(); - } - - $id = $original_entity_type->getKey('id'); - - // Define the step size. - $step_size = Settings::get('entity_update_batch_size', 50); - - // Get the next entity IDs to migrate. - $entity_ids = $this->database->select($original_base_table) - ->fields($original_base_table, [$id]) - ->condition($id, $sandbox['current_id'], '>') - ->orderBy($id, 'ASC') - ->range(0, $step_size) - ->execute() - ->fetchAllKeyed(0, 0); - - /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */ - $storage = $this->entityTypeManager->getStorage($temporary_entity_type->id()); - $storage->setEntityType($original_entity_type); - $storage->setTableMapping($original_table_mapping); - - $entities = $storage->loadMultiple($entity_ids); - - // Now inject the temporary entity type definition and table mapping in the - // storage and re-save the entities. - $storage->setEntityType($temporary_entity_type); - $storage->setTableMapping($temporary_table_mapping); - - foreach ($entities as $entity_id => $entity) { - try { - // Set the revision ID to be same as the entity ID. - $entity->set($revision_id_key, $entity_id); - - // We had no revisions so far, so the existing data belongs to the - // default revision now. - $entity->set($revision_default_key, TRUE); - - // Set the 'revision_translation_affected' flag to TRUE to match the - // previous API return value: if the field was not defined the value - // returned was always TRUE. - if ($temporary_entity_type->isTranslatable()) { - $entity->set($revision_translation_affected_key, TRUE); - } - - // Treat the entity as new in order to make the storage do an INSERT - // rather than an UPDATE. - $entity->enforceIsNew(TRUE); - - // Finally, save the entity in the temporary storage. - $storage->save($entity); - } - catch (\Exception $e) { - // In case of an error during the save process, we need to roll back the - // original entity type and field storage definitions and clean up the - // temporary tables. - $this->restoreOriginalDefinitions($sandbox); - - foreach ($temporary_table_mapping->getTableNames() as $table_name) { - $this->database->schema()->dropTable($table_name); - } - - // Re-throw the original exception with a helpful message. - throw new EntityStorageException("The entity update process failed while processing the entity {$original_entity_type->id()}:$entity_id.", $e->getCode(), $e); - } - - $sandbox['progress']++; - $sandbox['current_id'] = $entity_id; - } - - // If we're not in maintenance mode, the number of entities could change at - // any time so make sure that we always use the latest record count. - $sandbox['max'] = $this->database->select($original_base_table) - ->countQuery() - ->execute() - ->fetchField(); - - $sandbox['#finished'] = empty($sandbox['max']) ? 1 : ($sandbox['progress'] / $sandbox['max']); - } - - /** - * Updates field definitions to be revisionable. - * - * @param \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type - * A content entity type definition. - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions - * An array of field storage definitions. - * @param array $fields_to_update - * (optional) An array of field names for which to enable revision support. - * Defaults to an empty array. - * @param bool $update_cached_definitions - * (optional) Whether to update the cached field storage definitions in the - * entity definition update manager. Defaults to TRUE. - * - * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] - * An array of updated field storage definitions. - */ - protected function updateFieldStorageDefinitionsToRevisionable(ContentEntityTypeInterface $entity_type, array $storage_definitions, array $fields_to_update = [], $update_cached_definitions = TRUE) { - $updated_storage_definitions = array_map(function ($storage_definition) { - return clone $storage_definition; - }, $storage_definitions); - - // Update the 'langcode' field manually, as it is configured in the base - // content entity field definitions. - if ($entity_type->hasKey('langcode')) { - $fields_to_update = array_merge([$entity_type->getKey('langcode')], $fields_to_update); - } - - foreach ($fields_to_update as $field_name) { - if (!$updated_storage_definitions[$field_name]->isRevisionable()) { - $updated_storage_definitions[$field_name]->setRevisionable(TRUE); + /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */ + $this->entityTypeManager->useCaches(FALSE); + $entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId); - if ($update_cached_definitions) { - $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($updated_storage_definitions[$field_name]); - } - } - } + /** @var \Drupal\Core\Entity\EntityLastInstalledSchemaRepositoryInterface $last_installed_schema_repository */ + $last_installed_schema_repository = \Drupal::service('entity.last_installed_schema.repository'); + $field_storage_definitions = $last_installed_schema_repository->getLastInstalledFieldStorageDefinitions('entity_test_update'); // Add the revision ID field. - $revision_field = BaseFieldDefinition::create('integer') - ->setName($entity_type->getKey('revision')) + $field_name = $entity_type->getKey('revision'); + $field_storage_definitions[$entity_type->getKey('revision')] = BaseFieldDefinition::create('integer') + ->setName($field_name) ->setTargetEntityTypeId($entity_type->id()) ->setTargetBundle(NULL) - ->setLabel(new TranslatableMarkup('Revision ID')) + ->setLabel(t('Revision ID')) ->setReadOnly(TRUE) ->setSetting('unsigned', TRUE); - if ($update_cached_definitions) { - $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_field); - } - $updated_storage_definitions[$entity_type->getKey('revision')] = $revision_field; - - // Add the default revision flag field. + // Add the 'revision_default' field. $field_name = $entity_type->getRevisionMetadataKey('revision_default'); - $storage_definition = BaseFieldDefinition::create('boolean') + $field_storage_definitions[$field_name] = BaseFieldDefinition::create('boolean') ->setName($field_name) ->setTargetEntityTypeId($entity_type->id()) ->setTargetBundle(NULL) @@ -393,15 +150,11 @@ protected function updateFieldStorageDefinitionsToRevisionable(ContentEntityType ->setTranslatable(FALSE) ->setRevisionable(TRUE); - if ($update_cached_definitions) { - $this->entityDefinitionUpdateManager->installFieldStorageDefinition($field_name, $entity_type->id(), $entity_type->getProvider(), $storage_definition); - } - $updated_storage_definitions[$field_name] = $storage_definition; - // Add the 'revision_translation_affected' field if needed. if ($entity_type->isTranslatable()) { - $revision_translation_affected_field = BaseFieldDefinition::create('boolean') - ->setName($entity_type->getKey('revision_translation_affected')) + $field_name = $entity_type->getKey('revision_translation_affected'); + $field_storage_definitions[$field_name] = BaseFieldDefinition::create('boolean') + ->setName($field_name) ->setTargetEntityTypeId($entity_type->id()) ->setTargetBundle(NULL) ->setLabel(new TranslatableMarkup('Revision translation affected')) @@ -409,88 +162,15 @@ protected function updateFieldStorageDefinitionsToRevisionable(ContentEntityType ->setReadOnly(TRUE) ->setRevisionable(TRUE) ->setTranslatable(TRUE); - - if ($update_cached_definitions) { - $this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_translation_affected_field->getName(), $entity_type->id(), $entity_type->getProvider(), $revision_translation_affected_field); - } - $updated_storage_definitions[$entity_type->getKey('revision_translation_affected')] = $revision_translation_affected_field; - } - - return $updated_storage_definitions; - } - - /** - * Collects the original definitions of an entity type and its fields. - * - * @param array &$sandbox - * A sandbox array from a hook_update_N() implementation. - */ - protected function collectOriginalDefinitions(array &$sandbox) { - $original_entity_type = $this->lastInstalledSchemaRepository->getLastInstalledDefinition($this->entityTypeId); - $original_storage_definitions = $this->lastInstalledSchemaRepository->getLastInstalledFieldStorageDefinitions($this->entityTypeId); - - /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */ - $storage = $this->entityTypeManager->getStorage($this->entityTypeId); - $original_table_mapping = $storage->getCustomTableMapping($original_entity_type, $original_storage_definitions); - $backup_table_mapping = $storage->getCustomTableMapping($original_entity_type, $original_storage_definitions, 'old_'); - - $sandbox['original_entity_type'] = $original_entity_type; - $sandbox['original_storage_definitions'] = $original_storage_definitions; - $sandbox['original_table_mapping'] = $original_table_mapping; - $sandbox['backup_table_mapping'] = $backup_table_mapping; - - $sandbox['original_entity_schema_data'] = $this->installedStorageSchema->get($this->entityTypeId . '.entity_schema_data', []); - foreach ($original_storage_definitions as $storage_definition) { - $sandbox['original_field_schema_data'][$storage_definition->getName()] = $this->installedStorageSchema->get($this->entityTypeId . '.field_schema_data.' . $storage_definition->getName(), []); } - } - - /** - * Restores the entity type, field storage definitions and their schema data. - * - * @param array $sandbox - * The sandbox array from a hook_update_N() implementation. - */ - protected function restoreOriginalDefinitions(array $sandbox) { - $original_entity_type = $sandbox['original_entity_type']; - $original_storage_definitions = $sandbox['original_storage_definitions']; - $original_entity_schema_data = $sandbox['original_entity_schema_data']; - $original_field_schema_data = $sandbox['original_field_schema_data']; - $this->lastInstalledSchemaRepository->setLastInstalledDefinition($original_entity_type); - $this->lastInstalledSchemaRepository->setLastInstalledFieldStorageDefinitions($original_entity_type->id(), $original_storage_definitions); - - $this->installedStorageSchema->set($original_entity_type->id() . '.entity_schema_data', $original_entity_schema_data); - foreach ($original_field_schema_data as $field_name => $field_schema_data) { - $this->installedStorageSchema->set($original_entity_type->id() . '.field_schema_data.' . $field_name, $field_schema_data); + // Mark various fields as revisionable. + $field_storage_definitions[$entity_type->getKey('langcode')]->setRevisionable(TRUE); + foreach ($fields_to_update as $field_name) { + $field_storage_definitions[$field_name]->setRevisionable(TRUE); } - } - - /** - * Creates temporary entity type, field storage and table mapping objects. - * - * @param array &$sandbox - * A sandbox array from a hook_update_N() implementation. - * @param string[] $fields_to_update - * (optional) An array of field names that should be converted to be - * revisionable. Note that the 'langcode' field, if present, is updated - * automatically. Defaults to an empty array. - */ - protected function createTemporaryDefinitions(array &$sandbox, array $fields_to_update) { - // Make sure to get the latest entity type definition from code. - $this->entityTypeManager->useCaches(FALSE); - $actual_entity_type = $this->entityTypeManager->getDefinition($this->entityTypeId); - - $temporary_entity_type = clone $actual_entity_type; - $updated_storage_definitions = $this->updateFieldStorageDefinitionsToRevisionable($temporary_entity_type, $sandbox['original_storage_definitions'], $fields_to_update, FALSE); - - /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */ - $storage = $this->entityTypeManager->getStorage($this->entityTypeId); - $temporary_table_mapping = $storage->getCustomTableMapping($temporary_entity_type, $updated_storage_definitions, 'tmp_'); - $sandbox['temporary_entity_type'] = $temporary_entity_type; - $sandbox['temporary_table_mapping'] = $temporary_table_mapping; - $sandbox['updated_storage_definitions'] = $updated_storage_definitions; + $entity_definition_update_manager->updateEntityTypeSchema($entity_type, $field_storage_definitions, $sandbox); } } diff --git a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php index cf18e2b..c744e66 100644 --- a/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageSchemaTest.php @@ -409,7 +409,7 @@ public function testGetSchemaRevisionable() { ->method('getRevisionMetadataKeys') ->will($this->returnValue([])); - $this->storage->expects($this->exactly(2)) + $this->storage->expects($this->exactly(1)) ->method('getRevisionTable') ->will($this->returnValue('entity_test_revision')); @@ -505,6 +505,7 @@ public function testGetSchemaTranslatable() { 'id' => 'id', 'langcode' => 'langcode', ], + 'translatable' => TRUE, ]); $this->storage->expects($this->any()) @@ -622,24 +623,22 @@ public function testGetSchemaRevisionableTranslatable() { 'revision' => 'revision_id', 'langcode' => 'langcode', ], + 'revision_data_table' => 'entity_test_revision_field_data', ], ]) - ->setMethods(['getRevisionMetadataKeys']) + ->setMethods(['isRevisionable', 'isTranslatable', 'getRevisionMetadataKeys']) ->getMock(); $this->entityType->expects($this->any()) - ->method('getRevisionMetadataKeys') - ->will($this->returnValue([])); + ->method('isRevisionable') + ->will($this->returnValue(TRUE)); + $this->entityType->expects($this->any()) + ->method('isTranslatable') + ->will($this->returnValue(TRUE)); - $this->storage->expects($this->exactly(3)) + $this->storage->expects($this->exactly(2)) ->method('getRevisionTable') ->will($this->returnValue('entity_test_revision')); - $this->storage->expects($this->once()) - ->method('getDataTable') - ->will($this->returnValue('entity_test_field_data')); - $this->storage->expects($this->once()) - ->method('getRevisionDataTable') - ->will($this->returnValue('entity_test_revision_field_data')); $this->setUpStorageDefinition('revision_id', [ 'columns' => [