diff --git a/core/core.services.yml b/core/core.services.yml index 05be1bb..a07a0d8 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/Entity/DynamicallyFieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php index a5a9a94..9aaf35e 100644 --- a/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/DynamicallyFieldableEntityStorageInterface.php @@ -20,7 +20,8 @@ * * For example, configurable fields defined and exposed by field.module. */ -interface DynamicallyFieldableEntityStorageInterface extends EntityStorageInterface, FieldStorageDefinitionListenerInterface { +interface DynamicallyFieldableEntityStorageInterface extends FieldableEntityStorageInterface, FieldStorageDefinitionListenerInterface { + /** * Reacts to the creation of a field. * @@ -68,31 +69,6 @@ public function onFieldDefinitionDelete(FieldDefinitionInterface $field_definiti public function purgeFieldData(FieldDefinitionInterface $field_definition, $batch_size); /** - * Determines the number of entities with values for a given field. - * - * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition - * The field for which to count data records. - * @param bool $as_bool - * (Optional) Optimises the query for checking whether there are any records - * or not. Defaults to FALSE. - * - * @return bool|int - * The number of entities. If $as_bool parameter is TRUE then the - * value will either be TRUE or FALSE. - * - * @see \Drupal\Core\Entity\FieldableEntityStorageInterface::purgeFieldData() - */ - public function countFieldData($storage_definition, $as_bool = FALSE); - - /** - * Determines if the storage contains any data. - * - * @return bool - * TRUE if the storage contains data, FALSE if not. - */ - public function hasData(); - - /** * Performs final cleanup after all data of a field has been purged. * * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage_definition diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php index 24a74be..5330ad5 100644 --- a/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php +++ b/core/lib/Drupal/Core/Entity/FieldableEntityInterface.php @@ -7,8 +7,6 @@ namespace Drupal\Core\Entity; -use Drupal\Core\TypedData\ComplexDataInterface; - /** * Interface for entities having fields. * diff --git a/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php new file mode 100644 index 0000000..dc35ff7 --- /dev/null +++ b/core/lib/Drupal/Core/Entity/FieldableEntityStorageInterface.php @@ -0,0 +1,46 @@ +entityManager = $entity_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function validate($module_name) { + $reasons = array(); + + foreach ($this->entityManager->getDefinitions() 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 ($storage instanceof FieldableEntityStorageInterface && $storage->countFieldData($storage_definition, TRUE)) { + $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/field/field.module b/core/modules/field/field.module index 143e8c5..9264d31 100644 --- a/core/modules/field/field.module +++ b/core/modules/field/field.module @@ -118,40 +118,6 @@ function field_cron() { } /** - * Implements hook_system_info_alter(). - * - * Goes through a list of all modules that provide a field type and makes them - * required if there are any active fields of that type. - */ -function field_system_info_alter(&$info, Extension $file, $type) { - // It is not safe to call entity_load_multiple_by_properties() during - // maintenance mode. - if ($type == 'module' && !defined('MAINTENANCE_MODE')) { - $field_storages = entity_load_multiple_by_properties('field_storage_config', array('module' => $file->getName(), 'include_deleted' => TRUE)); - if ($field_storages) { - $info['required'] = TRUE; - - // Provide an explanation message (only mention pending deletions if there - // remains no actual, non-deleted fields) - $non_deleted = FALSE; - foreach ($field_storages as $field_storage) { - if (empty($field_storage->deleted)) { - $non_deleted = TRUE; - break; - } - } - if ($non_deleted) { - $explanation = t('Fields type(s) in use'); - } - else { - $explanation = t('Fields pending deletion'); - } - $info['explanation'] = $explanation; - } - } -} - -/** * Implements hook_entity_field_storage_info(). */ function field_entity_field_storage_info(\Drupal\Core\Entity\EntityTypeInterface $entity_type) { diff --git a/core/modules/field/field.services.yml b/core/modules/field/field.services.yml new file mode 100644 index 0000000..7c33168 --- /dev/null +++ b/core/modules/field/field.services.yml @@ -0,0 +1,6 @@ +services: + field_module_uninstall_validator: + class: Drupal\field\ModuleUninstallValidator + tags: + - { name: module_install.uninstall_validator } + arguments: ['@entity.manager', '@string_translation'] diff --git a/core/modules/field/src/ModuleUninstallValidator.php b/core/modules/field/src/ModuleUninstallValidator.php new file mode 100644 index 0000000..90f917e --- /dev/null +++ b/core/modules/field/src/ModuleUninstallValidator.php @@ -0,0 +1,67 @@ +entityManager = $entity_manager; + $this->stringTranslation = $string_translation; + } + + /** + * {@inheritdoc} + */ + public function validate($module_name) { + $reasons = array(); + + // It is not safe to call entity_load_multiple_by_properties() during + // maintenance mode. + if (!defined('MAINTENANCE_MODE')) { + $field_storages = $this->entityManager + ->getStorage('field_storage_config') + ->loadByProperties(array('module' => $module_name, 'include_deleted' => TRUE)); + + if ($field_storages) { + // Provide an explanation message (only mention pending deletions if there + // remains no actual, non-deleted fields) + $non_deleted = FALSE; + foreach ($field_storages as $field_storage) { + if (empty($field_storage->deleted)) { + $non_deleted = TRUE; + break; + } + } + + $reasons[] = $non_deleted ? $this->t('Fields type(s) in use') : $this->t('Fields pending deletion'); + } + } + + return $reasons; + } + +} 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()); +}