diff --git a/core/core.services.yml b/core/core.services.yml index 8527b27..4b5281a 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -261,6 +261,9 @@ services: parent: container.trait tags: - { name: plugin_manager_cache_clear } + entity.schema.manager: + class: Drupal\Core\Entity\Schema\ContentEntitySchemaManager + arguments: ['@entity.manager', '@state'] 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 945ffe7..e633f8b 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -13,6 +13,7 @@ use Drupal\Core\Config\FileStorage; use Drupal\Core\Config\ConfigException; use Drupal\Core\DrupalKernel; +use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Page\DefaultHtmlPageRenderer; use Drupal\Core\Utility\Error; use Drupal\Component\Uuid\Uuid; @@ -278,6 +279,33 @@ function update_do_one($module, $number, $dependency_map, &$context) { } /** + * Performs entity 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_schema($module, $number, &$context) { + try { + \Drupal::service('entity.schema.manager')->applyChanges(); + } + 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_schema'; + } +} + +/** * Starts the database update batch process. * * @param $start @@ -312,6 +340,13 @@ function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $ } } + // First of all perform entity schema updates, if needed, so that subsequent + // updates work with a correct entity schema. + $operations = array(); + if (\Drupal::service('entity.schema.manager')->getChangeList()) { + $operations[] = array('update_entity_schema', array('system', '0 - Update entity schema')); + } + // Resolve any update dependencies to determine the actual updates that will // be run and the order they will be run in. $updates = update_resolve_dependencies($start); @@ -325,7 +360,7 @@ function update_batch($start, $redirect = NULL, $url = NULL, $batch = array(), $ $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 @@ -556,15 +591,17 @@ function update_get_update_function_list($starting_updates) { // Go through each module and find all updates that we need (including the // first update that was requested and any updates that run after it). $update_functions = array(); - foreach ($starting_updates as $module => $version) { - $update_functions[$module] = array(); - $updates = drupal_get_schema_versions($module); - if ($updates !== FALSE) { - $max_version = max($updates); - if ($version <= $max_version) { - foreach ($updates as $update) { - if ($update >= $version) { - $update_functions[$module][$update] = $module . '_update_' . $update; + if ($starting_updates) { + foreach ($starting_updates as $module => $version) { + $update_functions[$module] = array(); + $updates = drupal_get_schema_versions($module); + if ($updates !== FALSE) { + $max_version = max($updates); + if ($version <= $max_version) { + foreach ($updates as $update) { + if ($update >= $version) { + $update_functions[$module][$update] = $module . '_update_' . $update; + } } } } diff --git a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php index f0ff2ae..cfcec80 100644 --- a/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php @@ -13,6 +13,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Entity\Schema\ContentEntitySchemaHandler; +use Drupal\Core\Entity\Schema\ContentEntitySchemaProviderInterface; use Drupal\Core\Entity\Sql\DefaultTableMapping; use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; use Drupal\Core\Field\FieldDefinitionInterface; @@ -35,7 +36,7 @@ * * @ingroup entity_api */ -class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface { +class ContentEntityDatabaseStorage extends ContentEntityStorageBase implements SqlEntityStorageInterface, ContentEntitySchemaProviderInterface { /** * The mapping of field columns to SQL tables. @@ -1412,6 +1413,34 @@ protected function deleteFieldItemsRevision(EntityInterface $entity) { /** * {@inheritdoc} */ + public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + return $this->schemaHandler()->requiresEntitySchemaChanges($definition, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldSchemaChanges($definition, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + return $this->schemaHandler()->requiresEntityDataMigration($definition, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + return $this->schemaHandler()->requiresFieldDataMigration($definition, $original); + } + + /** + * {@inheritdoc} + */ public function onEntityTypeDefinitionCreate() { $this->schemaHandler()->createEntitySchema($this->entityType); } 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/Schema/ContentEntitySchemaHandler.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php index 2143433..30db7f0 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandler.php @@ -20,7 +20,7 @@ /** * Defines a schema handler that supports revisionable, translatable entities. */ -class ContentEntitySchemaHandler implements ContentEntitySchemaHandlerInterface { +class ContentEntitySchemaHandler implements ContentEntitySchemaHandlerInterface, ContentEntitySchemaProviderInterface { /** * The entity manager. @@ -95,6 +95,65 @@ public function __construct(EntityManagerInterface $entity_manager, ContentEntit /** * {@inheritdoc} */ + public function requiresEntitySchemaChanges(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + return !$original || + $original->getStorageClass() != $definition->getStorageClass() || + $original->isRevisionable() != $definition->isRevisionable() || + $original->isTranslatable() != $definition->isTranslatable(); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldSchemaChanges(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + return !$original || + $original->getSchema() != $definition->getSchema() || + $original->isRevisionable() != $definition->isRevisionable() || + $original->hasCustomStorage() != $definition->hasCustomStorage() || + $this->requiresFieldDataMigration($definition, $original); + } + + /** + * {@inheritdoc} + */ + public function requiresEntityDataMigration(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + // A change in the storage class may or may not imply a data migration. We + // assume it does. This method should be overridden otherwise. Basically the + // only schema change that does not imply a data migration is from + // revisionable to non revisionable, as in that case we just need to drop + // revision tables. + return $original->getStorageClass() != $definition->getStorageClass() || + !$original->isRevisionable() && $definition->isRevisionable() || + $original->isTranslatable() != $definition->isTranslatable(); + } + + /** + * {@inheritdoc} + */ + public function requiresFieldDataMigration(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + $table_mapping = $this->storage->getTableMapping(); + + // If the field changes its custom storage status, we will need to create or + // drop its schema. In any case we cannot migrate its data as custom storage + // is involved. Otherwise if a field is moved from a shared table to a + // dedicated table or viceversa we need a data migration. + $custom_storage = $original->hasCustomStorage() || $definition->hasCustomStorage(); + $shared_table_changed = $table_mapping->allowsSharedTableStorage($original) != $table_mapping->allowsSharedTableStorage($definition); + $dedicated_table_changed = $table_mapping->requiresDedicatedTableStorage($original) != $table_mapping->requiresDedicatedTableStorage($definition); + if (!$custom_storage && ($shared_table_changed || $dedicated_table_changed)) { + return TRUE; + } + // If columns change we may need data manipulation, which we cannot handle. + if ($original->getColumns() != $definition->getColumns()) { + return TRUE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ public function createEntitySchema(ContentEntityTypeInterface $entity_type) { $this->checkEntityType($entity_type); $schema_handler = $this->database->schema(); diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php index cb2fb71..33a82b6 100644 --- a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaHandlerInterface.php @@ -11,7 +11,7 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; /** - * Defines an interface for handling the storage schema of entities. + * Defines an interface for handling the storage schema of content entities. */ interface ContentEntitySchemaHandlerInterface { diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php new file mode 100644 index 0000000..8305a1e --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManager.php @@ -0,0 +1,389 @@ +entityManager = $entity_manager; + $this->state = $state; + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionCreate(ContentEntityTypeInterface $definition) { + $entity_type_id = $definition->id(); + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new entity type definition. + $storage->onEntityTypeDefinitionCreate(); + // Store the current definitions to be able to track changes. + $this->saveEntityTypeDefinition($definition); + if ($definition->isFieldable()) { + $this->saveFieldStorageDefinitions($entity_type_id, $this->entityManager->getFieldStorageDefinitions($entity_type_id)); + } + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionUpdate(ContentEntityTypeInterface $definition, ContentEntityTypeInterface $original) { + $entity_type_id = $definition->id(); + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new entity type definition. + $storage->onEntityTypeDefinitionUpdate($original); + // Store the current definitions to be able to track changes. + $this->saveEntityTypeDefinition($definition); + } + + /** + * {@inheritdoc} + */ + public function onEntityTypeDefinitionDelete(ContentEntityTypeInterface $definition) { + $entity_type_id = $definition->id(); + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the entity type definition deletion. + $storage->onEntityTypeDefinitionDelete(); + // Store the current definitions to be able to track changes. + $this->deleteEntityTypeDefinition($entity_type_id); + // Ensure we delete any data concerning this entity type. It might have + // switched from fieldable to non-fieldable during its life cycle. + $this->deleteFieldStorageDefinitions($entity_type_id); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionCreate(FieldStorageDefinitionInterface $definition) { + $entity_type_id = $definition->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new field storage definition. + $storage->onFieldStorageDefinitionCreate($definition); + // Update our field storage definitions. + $definitions = $this->loadFieldStorageDefinitions($entity_type_id); + $definitions[$definition->getName()] = $definition; + $this->saveFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionUpdate(FieldStorageDefinitionInterface $definition, FieldStorageDefinitionInterface $original) { + $entity_type_id = $definition->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the new field storage definition. + $storage->onFieldStorageDefinitionUpdate($definition, $original); + // Update our field storage definitions. + $definitions = $this->loadFieldStorageDefinitions($entity_type_id); + $definitions[$definition->getName()] = $definition; + $this->saveFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * {@inheritdoc} + */ + public function onFieldStorageDefinitionDelete(FieldStorageDefinitionInterface $definition) { + $entity_type_id = $definition->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */ + $storage = $this->entityManager->getStorage($entity_type_id); + // Notify the storage layer of the removed field storage definition. + $storage->onFieldStorageDefinitionDelete($definition); + // Update our field storage definitions. + $definitions = $this->loadFieldStorageDefinitions($entity_type_id); + unset($definitions[$definition->getName()]); + $this->saveFieldStorageDefinitions($entity_type_id, $definitions); + } + + /** + * {@inheritdoc} + */ + public function getChangeList($entity_type_id = NULL) { + $change_list = array(); + $definitions = array_filter($this->entityManager->getDefinitions(), function($definition) { return $definition instanceof ContentEntityTypeInterface; }); + $entity_type_ids = isset($entity_type_id) ? array($entity_type_id) : array_keys($definitions); + + foreach ($entity_type_ids as $entity_type_id) { + $definition = $definitions[$entity_type_id]; + $storage = $this->entityManager->getStorage($entity_type_id); + + if ($definition instanceof ContentEntityTypeInterface && $storage instanceof ContentEntitySchemaProviderInterface) { + // Check whether there are changes in the entity type definition that + // would affect entity schema. + $original = $this->loadEntityTypeDefinition($definition->id()); + if ($storage->requiresEntitySchemaChanges($original, $definition)) { + $change_list[$entity_type_id]['entity_type'] = static::ENTITY_TYPE_UPDATED; + if ($storage->requiresEntityDataMigration($original, $definition)) { + $change_list[$entity_type_id]['data_migration'] = TRUE; + } + } + + // Check whether there are changes in the field storage definitions that + // would affect entity schema. We skip definitions with custom storage + // as they do not affect entity schema. + if ($definition->isFieldable()) { + $field_changes = array(); + $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + + // Detect created field storage definitions. + $created = array_filter(array_diff_key($storage_definitions, $original_storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); }); + $field_changes = array_merge($field_changes, array_map(function() { return static::FIELD_STORAGE_DEFINITION_CREATED; }, $created)); + + // Detect deleted field storage definitions. + $deleted = array_filter(array_diff_key($original_storage_definitions, $storage_definitions), function(FieldStorageDefinitionInterface $definition) { return !$definition->hasCustomStorage(); }); + $field_changes = array_merge($field_changes, array_map(function() { return static::FIELD_STORAGE_DEFINITION_DELETED; }, $deleted)); + + // Now compare field storage definitions. + foreach (array_intersect_key($storage_definitions, $original_storage_definitions) as $field_name => $definition) { + /** @var \Drupal\Core\Field\FieldStorageDefinitionInterface $definition */ + if (!$definition->hasCustomStorage()) { + $original = $this->loadFieldStorageDefinitions($definition->getTargetEntityTypeId())[$definition->getName()]; + if ($storage->requiresFieldSchemaChanges($original, $definition)) { + $field_changes[$field_name] = static::FIELD_STORAGE_DEFINITION_UPDATED; + if ($storage->requiresFieldDataMigration($original, $definition)) { + $change_list[$entity_type_id]['data_migration'] = TRUE; + } + } + } + } + + if ($field_changes) { + $change_list[$entity_type_id]['field_storage_definitions'] = $field_changes; + } + } + } + } + + return array_filter($change_list); + } + + /** + * {@inheritdoc} + */ + public function applyChanges($entity_type_id = NULL) { + foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) { + $storage = $this->entityManager->getStorage($entity_type_id); + + try { + $has_data = $storage->hasData(); + } + catch (DatabaseExceptionWrapper $e) { + // The entity schema might be corrupted. In this case it is safer to + // assume there is data available, to avoid performing unrecoverable + // operations. + $has_data = TRUE; + } + + // We do not allow any kind of schema change that would imply a data + // migration. + if (empty($change_list['data_migration']) || !$has_data) { + // Process entity type definition changes. + if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::ENTITY_TYPE_UPDATED) { + /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $definition */ + $definition = $this->entityManager->getDefinition($entity_type_id); + $this->onEntityTypeDefinitionUpdate($definition, $this->loadEntityTypeDefinition($entity_type_id)); + } + + // Process field storage definition changes. + if (!empty($change_list['field_storage_definitions'])) { + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); + + foreach ($change_list['field_storage_definitions'] as $field_name => $change) { + switch ($change) { + case static::FIELD_STORAGE_DEFINITION_CREATED: + $this->onFieldStorageDefinitionCreate($storage_definitions[$field_name]); + break; + + case static::FIELD_STORAGE_DEFINITION_UPDATED: + $this->onFieldStorageDefinitionUpdate($storage_definitions[$field_name], $original_storage_definitions[$field_name]); + break; + + case static::FIELD_STORAGE_DEFINITION_DELETED: + $this->onFieldStorageDefinitionDelete($storage_definitions[$field_name]); + break; + } + } + } + } + else { + $args = array('@entity_type_id' => $entity_type_id); + $message = String::format('Changes for the @entity_type_id entity type involve a data migration and cannot be applied.', $args); + throw new EntityStorageException($message); + } + } + } + + /** + * {@inheritdoc} + */ + public function getChangeSummary($entity_type_id = NULL) { + $summary = array(); + + foreach ($this->getChangeList($entity_type_id) as $entity_type_id => $change_list) { + // Process entity type definition changes. + if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::ENTITY_TYPE_UPDATED) { + $definition = $this->entityManager->getDefinition($entity_type_id); + $summary[$entity_type_id][] = $this->t('The %entity_type entity type has schema changes.', array('%entity_type' => $definition->getLabel())); + } + + // Process field storage definition changes. + if (!empty($change_list['field_storage_definitions'])) { + $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->loadFieldStorageDefinitions($entity_type_id); + + foreach ($change_list['field_storage_definitions'] as $field_name => $change) { + $definition = isset($storage_definitions[$field_name]) ? $storage_definitions[$field_name] : $original_storage_definitions[$field_name]; + $args = array('%field_name' => $definition->getLabel()); + + switch ($change) { + case static::FIELD_STORAGE_DEFINITION_CREATED: + $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', $args); + break; + + case static::FIELD_STORAGE_DEFINITION_UPDATED: + $summary[$entity_type_id][] = $this->t('The %field_name field has schema changes.', $args); + break; + + case static::FIELD_STORAGE_DEFINITION_DELETED: + $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', $args); + break; + } + } + } + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function getSystemRequirements($phase) { + $requirements = array( + 'title' => t('Entity schema'), + ); + + if ($this->getChangeList()) { + $requirements['value'] = $this->t('Out of date'); + $requirements['severity'] = REQUIREMENT_ERROR; + $requirements['description'] = $requirements['update']['description'] = $this->t('Some entity types have schema updates to install. You should run the database update script immediately.', array('@update' => base_path() . 'core/update.php')); + } + else { + $requirements['value'] = $this->t('Up to date'); + } + + return $requirements; + } + + /** + * TODO + * @param $entity_type_id + * + * @return \Drupal\Core\Entity\ContentEntityTypeInterface + * The stored entity type definition. + */ + protected function loadEntityTypeDefinition($entity_type_id) { + return $this->state->get('entity.schema.manager.entity_type.' . $entity_type_id); + } + + /** + * TODO + */ + protected function saveEntityTypeDefinition(ContentEntityTypeInterface $definition) { + $this->state->set('entity.schema.manager.entity_type.' . $definition->id(), $definition); + } + + /** + * TODO + * @param $entity_type_id + */ + protected function deleteEntityTypeDefinition($entity_type_id) { + $this->state->delete('entity.schema.manager.entity_type.' . $entity_type_id); + } + + /** + * TODO + * @param $entity_type_id + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + */ + protected function loadFieldStorageDefinitions($entity_type_id) { + return $this->state->get('entity.schema.manager.field_storage_definitions.' . $entity_type_id); + } + + /** + * TODO + * @param $entity_type_id + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions + */ + protected function saveFieldStorageDefinitions($entity_type_id, array $storage_definitions) { + $this->state->set('entity.schema.manager.field_storage_definitions.' . $entity_type_id, $storage_definitions); + } + + /** + * TODO + * @param $entity_type_id + */ + protected function deleteFieldStorageDefinitions($entity_type_id) { + $this->state->delete('entity.schema.manager.field_storage_definitions.' . $entity_type_id); + } + +} diff --git a/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php new file mode 100644 index 0000000..f9a2e34 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/Schema/ContentEntitySchemaManagerInterface.php @@ -0,0 +1,74 @@ +getDefinitions() as $entity_type) { - if ($entity_type->getProvider() == $module) { - $entity_manager->getStorage($entity_type->id())->onEntityTypeDefinitionCreate(); + foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) { + if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getProvider() == $module) { + $entity_schema_manager->onEntityTypeDefinitionCreate($entity_type); } } @@ -968,11 +970,12 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Remove all configuration belonging to the module. \Drupal::service('config.manager')->uninstall('module', $module); + /** @var \Drupal\Core\Entity\Schema\ContentEntitySchemaManagerInterface $entity_schema_manager */ + $entity_schema_manager = \Drupal::service('entity.schema.manager'); // Remove any entity schemas belonging to the module. - $entity_manager = \Drupal::entityManager(); - foreach ($entity_manager->getDefinitions() as $entity_type) { - if ($entity_type->getProvider() == $module) { - $entity_manager->getStorage($entity_type->id())->onEntityTypeDefinitionDelete(); + foreach (\Drupal::entityManager()->getDefinitions() as $entity_type) { + if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getProvider() == $module) { + $entity_schema_manager->onEntityTypeDefinitionDelete($entity_type); } } diff --git a/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php b/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php new file mode 100644 index 0000000..6791f37 --- /dev/null +++ b/core/lib/Drupal/Core/Update/Form/UpdateEntitySchemaForm.php @@ -0,0 +1,77 @@ +getChangeSummary(); + if ($summary) { + $entity_manager = \Drupal::entityManager(); + foreach ($summary as $entity_type_id => $items) { + $definition = $entity_manager->getDefinition($entity_type_id); + $form['summary'][$entity_type_id] = array( + '#type' => 'details', + '#title' => $definition->getLabel(), + ); + $form['summary'][$entity_type_id]['changes'] = array( + '#theme' => 'item_list', + '#items' => $items, + ); + } + } + else { + $form['summary'] = array('#markup' => $this->t('No entity schema changes available.')); + } + + $form['op'] = array( + '#type' => 'hidden', + '#value' => 'selection', + ); + + $form['entity_schema_updates'] = array( + '#type' => 'hidden', + '#value' => 1, + ); + + $form['actions'] = array('#type' => 'actions'); + $form['actions']['submit'] = array( + '#type' => 'submit', + '#value' => $this->t('Continue'), + '#button_type' => 'primary', + // This is necessary to use the hidden element to determine the next op. + '#name' => 'submit', + ); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + +} diff --git a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php b/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php index 574ec6c..fad0616 100644 --- a/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php +++ b/core/lib/Drupal/Core/Update/Form/UpdateScriptSelectionForm.php @@ -9,6 +9,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; /** * Provides the list of available database module updates. @@ -25,7 +26,7 @@ public function getFormID() { /** * {@inheritdoc} */ - public function buildForm(array $form, FormStateInterface $form_state) { + public function buildForm(array $form, FormStateInterface $form_state, $force_updates = FALSE) { $count = 0; $incompatible_count = 0; $form['start'] = array( @@ -88,7 +89,7 @@ public function buildForm(array $form, FormStateInterface $form_state) { drupal_set_message('Some of the pending updates cannot be applied because their dependencies were not met.', 'warning'); } - if (empty($count)) { + if (empty($count) && !$force_updates) { drupal_set_message(t('No pending updates.')); unset($form); $form['links'] = array( @@ -100,20 +101,29 @@ public function buildForm(array $form, FormStateInterface $form_state) { update_flush_all_caches(); } else { - $form['help'] = array( - '#markup' => '

The version of Drupal you are updating from has been automatically detected.

', - '#weight' => -5, - ); - if ($incompatible_count) { - $form['start']['#title'] = format_plural( - $count, - '1 pending update (@number_applied to be applied, @number_incompatible skipped)', - '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', - array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) + if ($count > 0) { + $form['help'] = array( + '#markup' => '

The version of Drupal you are updating from has been automatically detected.

', + '#weight' => -5, ); + if ($incompatible_count) { + $form['start']['#title'] = format_plural( + $count, + '1 pending update (@number_applied to be applied, @number_incompatible skipped)', + '@count pending updates (@number_applied to be applied, @number_incompatible skipped)', + array('@number_applied' => $count - $incompatible_count, '@number_incompatible' => $incompatible_count) + ); + } + else { + $form['start']['#title'] = format_plural($count, '1 pending update', '@count pending updates'); + } } else { - $form['start']['#title'] = format_plural($count, '1 pending update', '@count pending updates'); + unset($form); + $form['help'] = array( + '#markup' => '

No module update available.

', + '#weight' => -5, + ); } $form['actions'] = array('#type' => 'actions'); $form['actions']['submit'] = array( diff --git a/core/modules/field/src/Entity/FieldStorageConfig.php b/core/modules/field/src/Entity/FieldStorageConfig.php index 8e32657..2f86ddf 100644 --- a/core/modules/field/src/Entity/FieldStorageConfig.php +++ b/core/modules/field/src/Entity/FieldStorageConfig.php @@ -292,8 +292,8 @@ protected function preSaveNew(EntityStorageInterface $storage) { // definition is passed to the various hooks and written to config. $this->settings += $field_type_manager->getDefaultSettings($this->type); - // Notify the entity storage. - $entity_manager->getStorage($this->entity_type)->onFieldStorageDefinitionCreate($this); + // Notify the entity schema manager. + \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionCreate($this); } /** @@ -336,10 +336,10 @@ 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 schema manager. The controller 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); + \Drupal::service('entity.schema.manager')->onFieldStorageDefinitionUpdate($this, $this->original); } /** @@ -408,7 +408,7 @@ 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::service('entity.schema.manager')->onFieldStorageDefinitionDelete($field); $field->deleted = TRUE; } } diff --git a/core/modules/system/system.install b/core/modules/system/system.install index 4c94516..831d4b3 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -452,6 +452,9 @@ function system_requirements($phase) { } } } + + // Check entity schema status. + $requirements['entity_schema'] = \Drupal::service('entity.schema.manager')->getSystemRequirements($phase); } // Verify the update.php access setting diff --git a/core/modules/taxonomy/src/TermSchemaHandler.php b/core/modules/taxonomy/src/TermSchemaHandler.php index 57c70f5..4192769 100644 --- a/core/modules/taxonomy/src/TermSchemaHandler.php +++ b/core/modules/taxonomy/src/TermSchemaHandler.php @@ -21,18 +21,20 @@ class TermSchemaHandler extends ContentEntitySchemaHandler { protected function getEntitySchema(ContentEntityTypeInterface $entity_type) { $schema = parent::getEntitySchema($entity_type); - // Marking the respective fields as NOT NULL makes the indexes more - // performant. - $schema['taxonomy_term_field_data']['fields']['weight']['not null'] = TRUE; - $schema['taxonomy_term_field_data']['fields']['name']['not null'] = TRUE; + if (isset($schema['taxonomy_term_field_data'])) { + // Marking the respective fields as NOT NULL makes the indexes more + // performant. + $schema['taxonomy_term_field_data']['fields']['weight']['not null'] = TRUE; + $schema['taxonomy_term_field_data']['fields']['name']['not null'] = TRUE; - unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__vid__target_id']); - unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__description__format']); - $schema['taxonomy_term_field_data']['indexes'] += array( - 'taxonomy_term__tree' => array('vid', 'weight', 'name'), - 'taxonomy_term__vid_name' => array('vid', 'name'), - 'taxonomy_term__name' => array('name'), - ); + unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__vid__target_id']); + unset($schema['taxonomy_term_field_data']['indexes']['taxonomy_term_field__description__format']); + $schema['taxonomy_term_field_data']['indexes'] += array( + 'taxonomy_term__tree' => array('vid', 'weight', 'name'), + 'taxonomy_term__vid_name' => array('vid', 'name'), + 'taxonomy_term__name' => array('name'), + ); + } $schema['taxonomy_term_hierarchy'] = array( 'description' => 'Stores the hierarchical relationship between terms.', diff --git a/core/update.php b/core/update.php index 4e8157c..3376a2a 100644 --- a/core/update.php +++ b/core/update.php @@ -51,12 +51,22 @@ /** * Renders a form with a list of available database updates. */ +function update_entity_schema_page() { + $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateEntitySchemaForm'); + $build['#title'] = 'Drupal entity schema updates'; + return $build; +} + +/** + * Renders a form with a list of available database updates. + */ function update_selection_page() { // Make sure there is no stale theme registry. \Drupal::cache()->deleteAll(); - $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateScriptSelectionForm'); - $build['#title'] = 'Drupal database update'; + $force_updates = (bool) \Drupal::request()->get('entity_schema_updates'); + $build = \Drupal::formBuilder()->getForm('Drupal\Core\Update\Form\UpdateScriptSelectionForm', $force_updates); + $build['#title'] = 'Drupal module updates'; return $build; } @@ -198,6 +208,9 @@ function update_info_page() { $keyvalue->get('update_available_release')->deleteAll(); $token = \Drupal::csrfToken()->get('update'); + $entity_schema_updates = count(\Drupal::service('entity.schema.manager')->getChangeList()); + $op = $entity_schema_updates ? 'entity_schema' : 'selection'; + $output = '

Use this utility to update your database whenever a new release of Drupal or a module is installed.

For more detailed information, see the upgrading handbook. If you are unsure what these terms mean you should probably contact your hosting provider.

'; $output .= "
    \n"; $output .= "
  1. Back up your code. Hint: when backing up module code, do not leave that backup in the 'modules' or 'sites/*/modules' directories as this may confuse Drupal's auto-discovery mechanism.
  2. \n"; @@ -206,7 +219,7 @@ function update_info_page() { $output .= "
  3. Install your new files in the appropriate location, as described in the handbook.
  4. \n"; $output .= "
\n"; $output .= "

When you have performed the steps above, you may proceed.

\n"; - $form_action = check_url(drupal_current_script_url(array('op' => 'selection', 'token' => $token))); + $form_action = check_url(drupal_current_script_url(array('op' => $op, 'token' => $token))); $output .= '
'; $output .= "\n"; @@ -260,7 +273,8 @@ function update_task_list($active = NULL) { $tasks = array( 'requirements' => 'Verify requirements', 'info' => 'Overview', - 'select' => 'Review updates', + 'entity_schema' => 'Review entity schema updates', + 'selection' => 'Review module updates', 'run' => 'Run updates', 'finished' => 'Review log', ); @@ -369,11 +383,13 @@ function update_task_list($active = NULL) { switch ($op) { // update.php ops. + case 'entity_schema': case 'selection': $token = $request->query->get('token'); if (isset($token) && \Drupal::csrfToken()->validate($token, 'update')) { - $regions['sidebar_first'] = update_task_list('select'); - $output = update_selection_page(); + $regions['sidebar_first'] = update_task_list($op); + $task_callback = 'update_' . $op . '_page'; + $output = $task_callback(); break; }