diff --git a/core/core.services.yml b/core/core.services.yml index 98ddc9e..5fbcecf 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -297,6 +297,11 @@ services: tags: - { name: module_install.uninstall_validator } arguments: ['@entity.manager', '@string_translation'] + field_uninstall_validator: + class: Drupal\Core\Field\FieldModuleUninstallValidator + tags: + - { name: module_install.uninstall_validator } + arguments: ['@entity.manager', '@string_translation'] theme_handler: class: Drupal\Core\Extension\ThemeHandler arguments: ['@app.root', '@config.factory', '@module_handler', '@state', '@info_parser', '@logger.channel.default', '@asset.css.collection_optimizer', '@config.installer', '@config.manager', '@router.builder_indicator'] diff --git a/core/lib/Drupal/Core/Field/FieldModuleUninstallValidator.php b/core/lib/Drupal/Core/Field/FieldModuleUninstallValidator.php new file mode 100644 index 0000000..b9afd4a --- /dev/null +++ b/core/lib/Drupal/Core/Field/FieldModuleUninstallValidator.php @@ -0,0 +1,66 @@ +entityManager = $entity_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function validate($module_name) { + $entity_types = $this->entityManager->getDefinitions(); + $reasons = array(); + foreach ($entity_types as $entity_type_id => $entity_type) { + // We skip entity types defined by the module as there may be no content + // for being able to uninstall them anyway. + // See \Drupal\Core\Entity\ContentUninstallValidator. + if ($entity_type->getProvider() != $module_name && $entity_type instanceof ContentEntityTypeInterface) { + foreach ($this->entityManager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) { + if ($storage_definition->getProvider() == $module_name) { + $storage = $this->entityManager->getStorage($entity_type_id); + // If the content is not dynamically fieldable, fallback to checking + // whether the entity has content in general. + $fieldable = $storage instanceof DynamicallyFieldableEntityStorageInterface; + if (($fieldable && $storage->countFieldData($storage_definition, TRUE)) || (!$fieldable && $storage->hasData())) { + $reasons[] = $this->t('There is content for the field @field-name on entity type @entity_type.', array( + '@field-name' => $storage_definition->getName(), + '@entity_type' => $entity_type->getLabel(), + )); + } + } + } + } + } + return $reasons; + } + +} diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php index 62612dd..8bf777c 100644 --- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php +++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php @@ -40,7 +40,7 @@ function testSetUp() { $table = 'entity_test'; // Verify that specified $modules have been loaded. - $this->assertTrue(function_exists('entity_test_entity_bundle_info'), 'entity_test.module was loaded.'); + $this->assertTrue(function_exists('entity_test_entity_bundle_info'), 'entity_test_extra.module was loaded.'); // Verify that there is a fixed module list. $this->assertIdentical(array_keys(\Drupal::moduleHandler()->getModuleList()), $modules); $this->assertIdentical(\Drupal::moduleHandler()->getImplementations('entity_bundle_info'), ['entity_test']); diff --git a/core/modules/system/src/Tests/Field/FieldModuleUninstallValidatorTest.php b/core/modules/system/src/Tests/Field/FieldModuleUninstallValidatorTest.php new file mode 100644 index 0000000..d9feabc --- /dev/null +++ b/core/modules/system/src/Tests/Field/FieldModuleUninstallValidatorTest.php @@ -0,0 +1,141 @@ +installSchema('user', 'users_data'); + $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager'); + + // Setup some fields for entity_test_extra to create. + $definitions['extra_base_field'] = BaseFieldDefinition::create('string') + ->setName('extra_base_field') + ->setTargetEntityTypeId('entity_test') + ->setTargetBundle('entity_test'); + $this->state->set('entity_test.additional_base_field_definitions', $definitions); + // @todo: Use better field definition classes once there are any. + $definitions['extra_bundle_field'] = FieldStorageDefinition::create('string') + ->setName('extra_bundle_field') + ->setTargetEntityTypeId('entity_test') + ->setTargetBundle('entity_test'); + $this->state->set('entity_test.additional_field_storage_definitions', $definitions); + $this->state->set('entity_test.entity_test.additional_bundle_field_definitions', $definitions); + $this->entityManager->clearCachedDefinitions(); + } + + /** + * Tests uninstall entity_test module with and without content for the field. + */ + public function testUninstallingModule() { + // Test uninstall works fine without content. + $this->assertModuleInstallUninstall('entity_test_extra'); + + // Test uninstalling works fine with content having no field values. + $entity = $this->entityManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + ]); + $entity->save(); + $this->assertModuleInstallUninstall('entity_test_extra'); + $entity->delete(); + + // Verify uninstall works fine without content again. + $this->assertModuleInstallUninstall('entity_test_extra'); + // Verify uninstalling entity_test is not possible when there is content for + // the base field. + $this->enableModules(['entity_test_extra']); + $this->entityDefinitionUpdateManager->applyUpdates(); + $entity = $this->entityManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + 'extra_base_field' => $this->randomString(), + ]); + $entity->save(); + + try { + $this->getModuleInstaller()->uninstall(array('entity_test_extra')); + $this->fail('Module uninstallation fails as the module provides a base field which has content.'); + } + catch (ModuleUninstallValidatorException $e) { + $this->pass('Module uninstallation fails as the module provides a base field which has content.'); + } + + // Verify uninstalling entity_test is not possible when there is content for + // the bundle field. + $entity->delete(); + $this->assertModuleInstallUninstall('entity_test_extra'); + $this->enableModules(['entity_test_extra']); + $this->entityDefinitionUpdateManager->applyUpdates(); + $entity = $this->entityManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + 'extra_bundle_field' => $this->randomString(), + ]); + $entity->save(); + try { + $this->getModuleInstaller()->uninstall(array('entity_test_extra')); + $this->fail('Module uninstallation fails as the module provides a bundle field which has content.'); + } + catch (ModuleUninstallValidatorException $e) { + $this->pass('Module uninstallation fails as the module provides a bundle field which has content.'); + } + } + + /** + * Asserts the given module can be installed and uninstalled. + * + * @param string $module_name + * The module to install and uninstall. + */ + protected function assertModuleInstallUninstall($module_name) { + $this->enableModules([$module_name]); + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->assertTrue($this->getModuleHandler()->moduleExists($module_name), $module_name .' module is enabled.'); + $this->getModuleInstaller()->uninstall([$module_name]); + $this->entityDefinitionUpdateManager->applyUpdates(); + $this->assertFalse($this->getModuleHandler()->moduleExists($module_name), $module_name . ' module is disabled.'); + } + + /** + * Returns the ModuleHandler. + * + * @return \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected function getModuleHandler() { + return $this->container->get('module_handler'); + } + + /** + * Returns the ModuleInstaller. + * + * @return \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected function getModuleInstaller() { + return $this->container->get('module_installer'); + } + +} diff --git a/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.info.yml b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.info.yml new file mode 100644 index 0000000..b5c1cc3 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.info.yml @@ -0,0 +1,8 @@ +name: 'Entity test extra' +type: module +description: 'Provides extra fields for entity test entity types.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - entity_test diff --git a/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module new file mode 100644 index 0000000..d55aab3 --- /dev/null +++ b/core/modules/system/tests/modules/entity_test_extra/entity_test_extra.module @@ -0,0 +1,29 @@ +get($entity_type->id() . '.additional_base_field_definitions', array()); +} + +/** + * Implements hook_entity_field_storage_info(). + */ +function entity_test_extra_entity_field_storage_info(EntityTypeInterface $entity_type) { + return \Drupal::state()->get($entity_type->id() . '.additional_field_storage_definitions', array()); +} + +/** + * Implements hook_entity_bundle_field_info(). + */ +function entity_test_extra_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { + return \Drupal::state()->get($entity_type->id() . '.' . $bundle . '.additional_bundle_field_definitions', array()); +}