diff -u b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php --- b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManager.php @@ -73,30 +73,26 @@ // Process entity type definition changes. if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { $entity_type = $this->entityManager->getDefinition($entity_type_id); - // @todo Support non-storage-schema-changing definition updates too: - // https://www.drupal.org/node/2336895. - $summary[$entity_type_id][] = $this->t('The %entity_type entity type has definition updates that require schema changes.', array('%entity_type' => $entity_type->getLabel())); + $summary[$entity_type_id][] = $this->t('Update the %entity_type entity type.', array('%entity_type' => $entity_type->getLabel())); } // Process field storage definition changes. if (!empty($change_list['field_storage_definitions'])) { $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); - $original_storage_definitions = $this->entityManager->getInstalledFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id); foreach ($change_list['field_storage_definitions'] as $field_name => $change) { switch ($change) { case static::DEFINITION_CREATED: - $summary[$entity_type_id][] = $this->t('The %field_name field has been created.', array('%field_name' => $storage_definitions[$field_name]->getLabel())); + $summary[$entity_type_id][] = $this->t('Create the %field_name field.', array('%field_name' => $storage_definitions[$field_name]->getLabel())); break; case static::DEFINITION_UPDATED: - // @todo Support non-storage-schema-changing definition updates too: - // https://www.drupal.org/node/2336895. - $summary[$entity_type_id][] = $this->t('The %field_name field has storage definition updates that require schema changes.', array('%field_name' => $storage_definitions[$field_name]->getLabel())); + $summary[$entity_type_id][] = $this->t('Update the %field_name field.', array('%field_name' => $storage_definitions[$field_name]->getLabel())); break; case static::DEFINITION_DELETED: - $summary[$entity_type_id][] = $this->t('The %field_name field has been deleted.', array('%field_name' => $original_storage_definitions[$field_name]->getLabel())); + $summary[$entity_type_id][] = $this->t('Delete the %field_name field.', array('%field_name' => $original_storage_definitions[$field_name]->getLabel())); break; } } @@ -114,14 +110,14 @@ // Process entity type definition changes. if (!empty($change_list['entity_type']) && $change_list['entity_type'] == static::DEFINITION_UPDATED) { $entity_type = $this->entityManager->getDefinition($entity_type_id); - $original = $this->entityManager->getInstalledDefinition($entity_type_id); + $original = $this->entityManager->getLastInstalledDefinition($entity_type_id); $this->entityManager->onEntityTypeUpdate($entity_type, $original); } // Process field storage definition changes. if (!empty($change_list['field_storage_definitions'])) { $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); - $original_storage_definitions = $this->entityManager->getInstalledFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id); foreach ($change_list['field_storage_definitions'] as $field_name => $change) { switch ($change) { @@ -160,7 +156,7 @@ $change_list = array(); foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { - $original = $this->entityManager->getInstalledDefinition($entity_type_id); + $original = $this->entityManager->getLastInstalledDefinition($entity_type_id); // @todo Support non-storage-schema-changing definition updates too: // https://www.drupal.org/node/2336895. if ($this->requiresEntityStorageSchemaChanges($entity_type, $original)) { @@ -170,7 +166,7 @@ if ($entity_type->isFieldable()) { $field_changes = array(); $storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id); - $original_storage_definitions = $this->entityManager->getInstalledFieldStorageDefinitions($entity_type_id); + $original_storage_definitions = $this->entityManager->getLastInstalledFieldStorageDefinitions($entity_type_id); // Detect created field storage definitions. foreach (array_diff_key($storage_definitions, $original_storage_definitions) as $field_name => $storage_definition) { diff -u b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php --- b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityDefinitionUpdateManagerInterface.php @@ -24,9 +24,9 @@ * invoke those events. This interface is for managing that. * * @see \Drupal\Core\Entity\EntityManagerInterface::getDefinition() - * @see \Drupal\Core\Entity\EntityManagerInterface::getInstalledDefinition() + * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledDefinition() * @see \Drupal\Core\Entity\EntityManagerInterface::getFieldStorageDefinitions() - * @see \Drupal\Core\Entity\EntityManagerInterface::getInstalledFieldStorageDefinitions() + * @see \Drupal\Core\Entity\EntityManagerInterface::getLastInstalledFieldStorageDefinitions() * @see \Drupal\Core\Entity\EntityTypeListenerInterface * @see \Drupal\Core\Field\FieldStorageDefinitionListenerInterface */ diff -u b/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php --- b/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -205,6 +205,7 @@ $this->clearCachedBundles(); $this->clearCachedFieldDefinitions(); $this->classNameEntityTypeMap = array(); + $this->handlers = array(); } /** @@ -985,9 +986,9 @@ $storage->onEntityTypeCreate($entity_type); } - $this->setInstalledDefinition($entity_type); + $this->setLastInstalledDefinition($entity_type); if ($entity_type->isFieldable()) { - $this->setInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id)); + $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $this->getFieldStorageDefinitions($entity_type_id)); } } @@ -1004,7 +1005,7 @@ $storage->onEntityTypeUpdate($entity_type, $original); } - $this->setInstalledDefinition($entity_type); + $this->setLastInstalledDefinition($entity_type); } /** @@ -1020,7 +1021,7 @@ $storage->onEntityTypeDelete($entity_type); } - $this->deleteInstalledDefinition($entity_type_id); + $this->deleteLastInstalledDefinition($entity_type_id); } /** @@ -1036,7 +1037,7 @@ $storage->onFieldStorageDefinitionCreate($storage_definition); } - $this->setInstalledFieldStorageDefinition($storage_definition); + $this->setLastInstalledFieldStorageDefinition($storage_definition); $this->clearCachedFieldDefinitions(); } @@ -1053,7 +1054,7 @@ $storage->onFieldStorageDefinitionUpdate($storage_definition, $original); } - $this->setInstalledFieldStorageDefinition($storage_definition); + $this->setLastInstalledFieldStorageDefinition($storage_definition); $this->clearCachedFieldDefinitions(); } @@ -1070,7 +1071,7 @@ $storage->onFieldStorageDefinitionDelete($storage_definition); } - $this->deleteInstalledFieldStorageDefinition($storage_definition); + $this->deleteLastInstalledFieldStorageDefinition($storage_definition); $this->clearCachedFieldDefinitions(); } @@ -1131,7 +1132,7 @@ /** * {@inheritdoc} */ - public function getInstalledDefinition($entity_type_id) { + public function getLastInstalledDefinition($entity_type_id) { return $this->state->get('entity.manager.' . $entity_type_id . '.entity_type'); } @@ -1141,7 +1142,7 @@ * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * The entity type definition. */ - protected function setInstalledDefinition(EntityTypeInterface $entity_type) { + protected function setLastInstalledDefinition(EntityTypeInterface $entity_type) { $entity_type_id = $entity_type->id(); $this->state->set('entity.manager.' . $entity_type_id . '.entity_type', $entity_type); } @@ -1152,7 +1153,7 @@ * @param string $entity_type_id * The entity type definition identifier. */ - protected function deleteInstalledDefinition($entity_type_id) { + protected function deleteLastInstalledDefinition($entity_type_id) { $this->state->delete('entity.manager.' . $entity_type_id . '.entity_type'); // Clean up field storage definitions as well. Even if the entity type // isn't currently fieldable, there might be legacy definitions or an @@ -1163,7 +1164,7 @@ /** * {@inheritdoc} */ - public function getInstalledFieldStorageDefinitions($entity_type_id) { + public function getLastInstalledFieldStorageDefinitions($entity_type_id) { return $this->state->get('entity.manager.' . $entity_type_id . '.field_storage_definitions'); } @@ -1175,7 +1176,7 @@ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $storage_definitions * An array of field storage definitions. */ - protected function setInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) { + protected function setLastInstalledFieldStorageDefinitions($entity_type_id, array $storage_definitions) { $this->state->set('entity.manager.' . $entity_type_id . '.field_storage_definitions', $storage_definitions); } @@ -1185,11 +1186,11 @@ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field storage definition. */ - protected function setInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + protected function setLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { $entity_type_id = $storage_definition->getTargetEntityTypeId(); - $definitions = $this->getInstalledFieldStorageDefinitions($entity_type_id); + $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id); $definitions[$storage_definition->getName()] = $storage_definition; - $this->setInstalledFieldStorageDefinitions($entity_type_id, $definitions); + $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); } /** @@ -1198,11 +1199,11 @@ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition * The field storage definition. */ - protected function deleteInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { + protected function deleteLastInstalledFieldStorageDefinition(FieldStorageDefinitionInterface $storage_definition) { $entity_type_id = $storage_definition->getTargetEntityTypeId(); - $definitions = $this->getInstalledFieldStorageDefinitions($entity_type_id); + $definitions = $this->getLastInstalledFieldStorageDefinitions($entity_type_id); unset($definitions[$storage_definition->getName()]); - $this->setInstalledFieldStorageDefinitions($entity_type_id, $definitions); + $this->setLastInstalledFieldStorageDefinitions($entity_type_id, $definitions); } } diff -u b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php --- b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php @@ -96,7 +96,8 @@ * definitions for which database tables were created. * * Application management code can check if getFieldStorageDefinitions() - * differs from getInstalledFieldStorageDefinitions() and decide whether to: + * differs from getLastInstalledFieldStorageDefinitions() and decide whether + * to: * - Invoke the appropriate * \Drupal\Core\Field\FieldStorageDefinitionListenerInterface * events so that handlers react to the new definitions. @@ -113,7 +114,7 @@ * * @see \Drupal\Core\Entity\EntityTypeListenerInterface */ - public function getInstalledFieldStorageDefinitions($entity_type_id); + public function getLastInstalledFieldStorageDefinitions($entity_type_id); /** * Returns a lightweight map of fields across bundles. @@ -334,7 +335,7 @@ * definition for which database tables were created. * * Application management code can check if getDefinition() differs from - * getInstalledDefinition() and decide whether to: + * getLastInstalledDefinition() and decide whether to: * - Invoke the appropriate \Drupal\Core\Entity\EntityTypeListenerInterface * event so that handlers react to the new definition. * - Raise a warning that the application state is incompatible with the @@ -350,7 +351,7 @@ * * @see \Drupal\Core\Entity\EntityTypeListenerInterface */ - public function getInstalledDefinition($entity_type_id); + public function getLastInstalledDefinition($entity_type_id); /** * {@inheritdoc} diff -u b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php --- b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php +++ b/core/lib/Drupal/Core/Entity/Schema/EntityStorageSchemaInterface.php @@ -6,6 +6,7 @@ */ namespace Drupal\Core\Entity\Schema; + use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeListenerInterface; @@ -16,12 +17,9 @@ * storage backend's schema that the entity type's storage handler needs for * storing its entities. For example, if the storage handler is for a SQL * backend, then the storage schema handler is responsible for creating the - * needed tables. - * - * During the life of the website, an entity type's definition can change in a - * way that requires changes to the storage schema, so the handler must react - * to the various \Drupal\Core\Entity\EntityTypeListenerInterface events - * accordingly. + * needed tables. During the application lifetime, an entity type's definition + * can change in a way that requires changes to the storage schema, so this + * interface defines methods for that as well. * * @see \Drupal\Core\Entity\EntityStorageInterface */ @@ -43,16 +41,18 @@ /** * Checks if existing data would be lost if the schema changes were applied. * - * This is only called if requiresEntityStorageSchemaChanges() returns TRUE, - * since if there are no schema changes, then no data needs to be migrated, - * so this function does not need to recheck whether schema changes are - * needed. + * If there are no schema changes needed, then no data needs to be migrated, + * but it is not the responsibility of this function to recheck what + * requiresEntityStorageSchemaChanges() checks. Rather, the meaning of what + * this function returns when requiresEntityStorageSchemaChanges() returns + * FALSE is undefined. Callers are expected to only call this function when + * requiresEntityStorageSchemaChanges() is TRUE. * - * This function can return TRUE if any of these conditions apply: + * This function can return FALSE if any of these conditions apply: * - There are no existing entities for the entity type. * - There are existing entities, but the schema changes can be applied - * without losing their data (e.g., if the schema change can involve - * altering tables instead of dropping and recreating them). + * without losing their data (e.g., if the schema changes can be performed + * by altering tables rather than dropping and recreating them). * - The only entity data that would be lost are ones that are not valid for * the new definition (e.g., if changing an entity type from revisionable * to non-revisionable, then it's okay to drop data for the non-default diff -u b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php --- b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php +++ b/core/lib/Drupal/Core/Entity/Schema/FieldableEntityStorageSchemaInterface.php @@ -31,19 +31,21 @@ /** * Checks if existing data would be lost if the schema changes were applied. * - * This is only called if requiresFieldStorageSchemaChanges() returns TRUE, - * since if there are no schema changes, then no data needs to be migrated, - * so this function does not need to recheck whether schema changes are - * needed. + * If there are no schema changes needed, then no data needs to be migrated, + * but it is not the responsibility of this function to recheck what + * requiresFieldStorageSchemaChanges() checks. Rather, the meaning of what + * this function returns when requiresFieldStorageSchemaChanges() returns + * FALSE is undefined. Callers are expected to only call this function when + * requiresFieldStorageSchemaChanges() is TRUE. * - * This function can return TRUE if any of these conditions apply: + * This function can return FALSE if any of these conditions apply: * - There are no existing entities for the entity type to which this field * is attached. * - There are existing entities, but none with existing values for this * field. * - There are existing field values, but the schema changes can be applied - * without losing them (e.g., if the schema change can involve altering - * tables instead of dropping and recreating them). + * without losing them (e.g., if the schema changes can be performed by + * altering tables rather than dropping and recreating them). * - The only field values that would be lost are ones that are not valid for * the new definition (e.g., if changing a field from revisionable to * non-revisionable, then it's okay to drop data for the non-default diff -u b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php --- b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php @@ -299,11 +299,7 @@ ), $all_fields); $revisionable = $this->entityType->isRevisionable(); - // @todo Remove the data table check once all entity types are using - // entity query and we have a views data handler. See: - // - https://drupal.org/node/2068325 - // - https://drupal.org/node/1740492 - $translatable = $this->entityType->getDataTable() && $this->entityType->isTranslatable(); + $translatable = $this->entityType->isTranslatable(); if (!$revisionable && !$translatable) { // The base layout stores all the base field values in the base table. $table_mapping->setFieldNames($this->baseTable, $all_fields); diff -u b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php --- b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/Sql/SqlContentEntityStorageTest.php @@ -598,10 +598,10 @@ // This allows to re-use the data provider. $entity_keys['langcode'] = 'langcode'; - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('isTranslatable') ->will($this->returnValue(TRUE)); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); $this->entityType->expects($this->any()) @@ -658,10 +658,10 @@ $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names); $this->fieldDefinitions = $this->mockFieldDefinitions($field_names); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('isTranslatable') ->will($this->returnValue(TRUE)); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); $this->entityType->expects($this->any()) @@ -720,13 +720,13 @@ 'langcode' => 'langcode', ); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('isRevisionable') ->will($this->returnValue(TRUE)); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('isTranslatable') ->will($this->returnValue(TRUE)); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); $this->entityType->expects($this->any()) @@ -841,13 +841,13 @@ $revisionable_field_names = array('description', 'owner'); $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, $revision_metadata_field_names), array('isRevisionable' => TRUE)); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('isRevisionable') ->will($this->returnValue(TRUE)); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('isTranslatable') ->will($this->returnValue(TRUE)); - $this->entityType->expects($this->exactly(2)) + $this->entityType->expects($this->atLeastOnce()) ->method('getDataTable') ->will($this->returnValue('entity_test_field_data')); $this->entityType->expects($this->any()) only in patch2: unchanged: --- a/core/modules/field/tests/modules/field_test/field_test.entity.inc +++ b/core/modules/field/tests/modules/field_test/field_test.entity.inc @@ -18,11 +18,17 @@ function field_test_entity_type_alter(array &$entity_types) { /** * Helper function to enable entity translations. */ -function field_test_entity_info_translatable($entity_type = NULL, $translatable = NULL) { +function field_test_entity_info_translatable($entity_type_id = NULL, $translatable = NULL) { $stored_value = &drupal_static(__FUNCTION__, array()); - if (isset($entity_type)) { - $stored_value[$entity_type] = $translatable; - \Drupal::entityManager()->clearCachedDefinitions(); + if (isset($entity_type_id)) { + $entity_manager = \Drupal::entityManager(); + $original = $entity_manager->getDefinition($entity_type_id); + $stored_value[$entity_type_id] = $translatable; + if ($translatable != $original->isTranslatable()) { + $entity_manager->clearCachedDefinitions(); + $entity_type = $entity_manager->getDefinition($entity_type_id); + $entity_manager->onEntityTypeUpdate($entity_type, $original); + } } return $stored_value; } only in patch2: unchanged: --- /dev/null +++ b/core/modules/system/src/Tests/Entity/EntityDefinitionUpdateTest.php @@ -0,0 +1,116 @@ +entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager'); + $this->database = $this->container->get('database'); + } + + /** + * Tests when no definition update is needed. + */ + public function testNoUpdates() { + // Install every entity type's schema. + foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { + $this->installEntitySchema($entity_type_id); + } + + // Ensure that the definition update manager reports no updates. + $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.'); + $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), array(), 'EntityDefinitionUpdateManager reports an empty change summary.'); + + // Ensure that applyUpdates() runs without error (it's not expected to do + // anything when there aren't updates). + $this->entityDefinitionUpdateManager->applyUpdates(); + } + + /** + * Tests updating entity schema when there are no existing entities. + */ + public function testUpdateWithoutData() { + // Install every entity type's schema. Start with entity_test_rev not + // supporting revisions, and ensure its revision table isn't created. + $this->state->set('entity_test.entity_test_rev.disable_revisable', TRUE); + $this->entityManager->clearCachedDefinitions(); + foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { + $this->installEntitySchema($entity_type_id); + } + $this->assertFalse($this->database->schema()->tableExists('entity_test_rev_revision'), 'Revision table not created for entity_test_rev.'); + + // Restore entity_test_rev back to supporting revisions and ensure the + // definition update manager reports that an update is needed. + $this->state->delete('entity_test.entity_test_rev.disable_revisable'); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $expected = array( + 'entity_test_rev' => array( + t('Update the %entity_type entity type.', array('%entity_type' => $this->entityManager->getDefinition('entity_test_rev')->getLabel())), + ), + ); + $this->assertIdentical($this->entityDefinitionUpdateManager->getChangeSummary(), $expected, 'EntityDefinitionUpdateManager reports the expected change summary.'); + + // Run the update and ensure the revision table is created. + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->assertTrue($this->database->schema()->tableExists('entity_test_rev_revision'), 'Revision table created for entity_test_rev.'); + } + + /** + * Tests updating entity schema when there are existing entities. + */ + public function testUpdateWithData() { + // Install every entity type's schema. Start with entity_test_rev not + // supporting revisions. + $this->state->set('entity_test.entity_test_rev.disable_revisable', TRUE); + $this->entityManager->clearCachedDefinitions(); + foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) { + $this->installEntitySchema($entity_type_id); + } + + // Save an entity. + $this->entityManager->getStorage('entity_test_rev')->create()->save(); + + // Restore entity_test_rev back to supporting revisions and try to apply + // the update. It's expected to throw an exception. + $this->state->delete('entity_test.entity_test_rev.disable_revisable'); + try { + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->fail('EntityStorageException thrown when trying to apply an update that requires data migration.'); + } + catch (EntityStorageException $e) { + $this->pass('EntityStorageException thrown when trying to apply an update that requires data migration.'); + } + } + +} only in patch2: unchanged: --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -5,20 +5,21 @@ * Configuration system that lets administrators modify the workings of the site. */ +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Cache\Cache; +use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Extension\Extension; use Drupal\Core\Extension\ExtensionDiscovery; -use Drupal\Core\Form\FormState; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Menu\MenuTreeParameters; +use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\StringTranslation\TranslationWrapper; use Drupal\Core\Url; -use Drupal\Core\Block\BlockPluginInterface; use Drupal\user\UserInterface; -use Symfony\Component\HttpFoundation\RedirectResponse; use GuzzleHttp\Exception\RequestException; +use Symfony\Component\HttpFoundation\RedirectResponse; /** * New users will be set to the default time zone at registration. @@ -1032,11 +1033,48 @@ function system_sort_themes($a, $b) { * Implements hook_system_info_alter(). */ function system_system_info_alter(&$info, Extension $file, $type) { - // Remove page-top and page-bottom from the blocks UI since they are reserved for - // modules to populate from outside the blocks system. - if ($type == 'theme') { - $info['regions_hidden'][] = 'page_top'; - $info['regions_hidden'][] = 'page_bottom'; + switch ($type) { + case 'theme': + // Remove page-top and page-bottom from the blocks UI since they are + // reserved for modules to populate from outside the blocks system. + $info['regions_hidden'][] = 'page_top'; + $info['regions_hidden'][] = 'page_bottom'; + break; + + case 'module': + // @todo Unify this with field_system_info_alter() once purging is + // supported for any field. See https://www.drupal.org/node/2282119. + $module_name = $file->getName(); + if ($module_name != 'field') { + $entity_manager = \Drupal::entityManager(); + foreach ($entity_manager->getDefinitions() as $entity_type_id => $entity_type) { + try { + // We skip entity-defining modules, otherwise deleting all entities + // would be required before being able to uninstall them. + if ($entity_type instanceof ContentEntityTypeInterface && $entity_type->getProvider() != $module_name) { + $storage_definitions = $entity_manager->getFieldStorageDefinitions($entity_type_id); + // @todo Make it easier to query if there are entities: + // https://www.drupal.org/node/2337753. + $has_data = $entity_manager->getStorage($entity_type_id)->countFieldData($storage_definitions[$entity_type->getKey('id')], TRUE); + if ($has_data) { + foreach ($storage_definitions as $storage_definition) { + if ($storage_definition->getProvider() == $module_name) { + $info['required'] = TRUE; + $info['explanation'] = t('Fields type(s) in use'); + break; + } + } + } + } + } + catch (PluginNotFoundException $e) { + // This may happen if the current module is being installed. In this + // case we can safely ignore it, as we are interested in blocking + // module uninstallation. + } + } + } + break; } } only in patch2: unchanged: --- a/core/modules/system/tests/modules/entity_test/entity_test.module +++ b/core/modules/system/tests/modules/entity_test/entity_test.module @@ -71,6 +71,14 @@ function entity_test_entity_type_alter(array &$entity_types) { $entity_types[$entity_type]->set('translation', $translation); } } + + // Optionally allow testing an entity type definition being updated from + // revisable to not or vice versa. + if (\Drupal::state()->get('entity_test.entity_test_rev.disable_revisable')) { + $keys = $entity_types['entity_test_rev']->getKeys(); + unset($keys['revision']); + $entity_types['entity_test_rev']->set('entity_keys', $keys); + } } /**