diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index cde0eab..226178c 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -10,6 +10,7 @@ use Drupal\Component\Diff\Diff; use Drupal\Component\Serialization\Yaml; use Drupal\Core\Config\Entity\ConfigDependencyManager; +use Drupal\Core\Config\Entity\ConfigEntityDependency; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -178,14 +179,47 @@ public function createSnapshot(StorageInterface $source_storage, StorageInterfac */ public function uninstall($type, $name) { // Remove all dependent configuration entities. - $dependent_entities = $this->findConfigEntityDependentsAsEntities($type, array($name)); + $extension_dependent_entities = $this->findConfigEntityDependentsAsEntities($type, array($name)); + // Give config entities a chance to become independent of the entities we + // are going to delete. + foreach ($extension_dependent_entities as $entity) { + $entity_dependencies = $entity->getDependencies(); + if (empty($entity_dependencies)) { + // No dependent entities nothing to do. + continue; + } + // Work out if any of the entity's dependencies are going to be affected + // by the uninstall. + $affected_dependencies = array( + 'entity' => array(), + 'module' => array(), + 'theme' => array(), + ); + if (isset($entity_dependencies['entity'])) { + foreach ($extension_dependent_entities as $extension_dependent_entity) { + if (in_array($extension_dependent_entity->getConfigDependencyName(), $entity_dependencies['entity'])) { + $affected_dependencies['entity'][] = $extension_dependent_entity; + } + } + } + // Check if the extension being uninstalled is a dependency of the entity. + if (isset($entity_dependencies[$type]) && in_array($name, $entity_dependencies[$type])) { + $affected_dependencies[$type] = array($name); + } + // Inform the entity. + $entity->onDependencyRemoval($affected_dependencies); + } + + // Recalculate the dependencies, some config entities may have fixed their + // dependencies on the to-be-removed entities. + $extension_dependent_entities = $this->findConfigEntityDependentsAsEntities($type, array($name)); // Reverse the array to that entities are removed in the correct order of // dependence. For example, this ensures that field instances are removed // before fields. - foreach (array_reverse($dependent_entities) as $entity) { - $entity->setUninstalling(TRUE); - $entity->delete(); + foreach (array_reverse($extension_dependent_entities) as $extension_dependent_entity) { + $extension_dependent_entity->setUninstalling(TRUE); + $extension_dependent_entity->delete(); } $config_names = $this->configFactory->listAll($name . '.'); diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index 3a6bbf4..c329032 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -375,8 +375,21 @@ protected function addDependency($type, $name) { /** * {@inheritdoc} */ + public function getDependencies() { + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ public function getConfigDependencyName() { return $this->getEntityType()->getConfigPrefix() . '.' . $this->id(); } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + } + } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependency.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependency.php index 604e21c..7da6ecb 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependency.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependency.php @@ -34,9 +34,9 @@ class ConfigEntityDependency { * @param string $name * The configuration entity's configuration object name. * @param array $values - * The configuration entity's values. + * (optional) The configuration entity's values. */ - public function __construct($name, $values) { + public function __construct($name, $values = array()) { $this->name = $name; if (isset($values['dependencies'])) { $this->dependencies = $values['dependencies']; diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php index 60f9661..a315eba 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php @@ -157,4 +157,37 @@ public function calculateDependencies(); */ public function getConfigDependencyName(); + /** + * Informs the entity that entities it depends on will be deleted. + * + * This method allows configuration entities to remove dependencies instead + * of being deleted themselves. Configuration entities can use this method to + * avoid being unnecessarily deleted during an extension uninstallation. + * Implementations should save the entity if dependencies have been + * successfully removed. For example, entity displays remove references to + * widgets and formatters if the plugin that supplies them depends on a + * module that is being uninstalled. + * + * @todo https://www.drupal.org/node/2336727 this method is only fired during + * extension uninstallation but it could be used during config entity + * deletion too. + * + * @param array $dependencies + * An array of dependencies that will be deleted keyed by dependency type. + * Dependency types are, for example, entity, module and theme. + * + * @see \Drupal\Core\Config\ConfigManager::uninstall() + * @see \Drupal\Core\Entity\EntityDisplayBase::onDependencyRemoval() + */ + public function onDependencyRemoval(array $dependencies); + + /** + * Gets the configuration dependencies. + * + * @return array + * An array of dependencies. If $type not set all dependencies will be + * returned keyed by $type. + */ + public function getDependencies(); + } diff --git a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php index 3710507..f80ad7c 100644 --- a/core/lib/Drupal/Core/Entity/EntityDisplayBase.php +++ b/core/lib/Drupal/Core/Entity/EntityDisplayBase.php @@ -11,6 +11,7 @@ use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Entity\Display\EntityDisplayInterface; use Drupal\field\Entity\FieldInstanceConfig; +use Drupal\field\FieldInstanceConfigInterface; /** * Provides a common base class for entity view and form displays. @@ -378,4 +379,32 @@ private function fieldHasDisplayOptions(FieldDefinitionInterface $definition) { return $definition->getDisplayOptions($this->displayContext); } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = FALSE; + foreach ($dependencies['entity'] as $entity) { + if ($entity instanceof FieldInstanceConfigInterface) { + // Remove components for fields that are being deleted. + $this->removeComponent($entity->getName()); + unset($this->hidden[$entity->getName()]); + $changed = TRUE; + } + } + foreach ($this->getComponents() as $name => $component) { + if (isset($component['type']) && $definition = $this->pluginManager->getDefinition($component['type'], FALSE)) { + if (in_array($definition['provider'], $dependencies['module'])) { + // Revert to the defaults if the plugin that supplies the widget or + // formatter depends on a module that is being uninstalled. + $this->setComponent($name); + $changed = TRUE; + } + } + } + if ($changed) { + $this->save(); + } + } + } diff --git a/core/modules/config/src/Tests/ConfigDependencyTest.php b/core/modules/config/src/Tests/ConfigDependencyTest.php index 52c8cea..8da5b24 100644 --- a/core/modules/config/src/Tests/ConfigDependencyTest.php +++ b/core/modules/config/src/Tests/ConfigDependencyTest.php @@ -146,6 +146,69 @@ public function testDependencyMangement() { } /** + * Tests ConfigManager::uninstall() and config entity dependency management. + */ + public function testConfigEntityUninstall() { + /** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */ + $config_manager = \Drupal::service('config.manager'); + /** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */ + $storage = $this->container->get('entity.manager')->getStorage('config_test'); + // Test dependencies between modules. + $entity1 = $storage->create( + array( + 'id' => 'entity1', + 'test_dependencies' => array( + 'module' => array('node', 'config_test') + ), + ) + ); + $entity1->save(); + $entity2 = $storage->create( + array( + 'id' => 'entity2', + 'test_dependencies' => array( + 'entity' => array($entity1->getConfigDependencyName()), + ), + ) + ); + $entity2->save(); + // Test that doing a config uninstall of the node module deletes entity2 + // since it is dependent on entity1 which is dependent on the node module. + $config_manager->uninstall('module', 'node'); + $this->assertFalse($storage->load('entity1'), 'Entity 1 deleted'); + $this->assertFalse($storage->load('entity2'), 'Entity 2 deleted'); + + $entity1 = $storage->create( + array( + 'id' => 'entity1', + 'test_dependencies' => array( + 'module' => array('node', 'config_test') + ), + ) + ); + $entity1->save(); + $entity2 = $storage->create( + array( + 'id' => 'entity2', + 'test_dependencies' => array( + 'entity' => array($entity1->getConfigDependencyName()), + ), + ) + ); + $entity2->save(); + \Drupal::state()->set('config_test.fix_dependencies', array($entity1->getConfigDependencyName())); + // Test that doing a config uninstall of the node module does not delete + // entity2 since the state setting allows + // \Drupal\config_test\Entity::onDependencyRemoval() to remove the + // dependency before config entities are deleted during the uninstall. + $config_manager->uninstall('module', 'node'); + $this->assertFalse($storage->load('entity1'), 'Entity 1 deleted'); + $entity2 = $storage->load('entity2'); + $this->assertTrue($entity2, 'Entity 2 not deleted'); + $this->assertEqual($entity2->calculateDependencies()['entity'], array(), 'Entity 2 dependencies updated to remove dependency on Entity1.'); + } + + /** * Gets a list of identifiers from an array of configuration entities. * * @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $dependents diff --git a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php index 0a0d2f1..b022cc6 100644 --- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php +++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php @@ -136,4 +136,24 @@ public function calculateDependencies() { } } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $changed = FALSE; + $fix_deps = \Drupal::state()->get('config_test.fix_dependencies', array()); + foreach ($dependencies['entity'] as $entity) { + if (in_array($entity->getConfigDependencyName(), $fix_deps)) { + $key = array_search($entity->getConfigDependencyName(), $this->test_dependencies['entity']); + if ($key !== FALSE) { + $changed = TRUE; + unset($this->test_dependencies['entity'][$key]); + } + } + } + if ($changed) { + $this->save(); + } + } + } diff --git a/core/modules/entity/src/Tests/EntityDisplayTest.php b/core/modules/entity/src/Tests/EntityDisplayTest.php index 2e3657d..32eb231 100644 --- a/core/modules/entity/src/Tests/EntityDisplayTest.php +++ b/core/modules/entity/src/Tests/EntityDisplayTest.php @@ -359,4 +359,45 @@ public function testDeleteFieldInstance() { $this->assertFalse($display->getComponent($field_name)); } + /** + * Tests \Drupal\entity\EntityDisplayBase::onDependencyRemoval(). + */ + public function testOnDependencyRemoval() { + $this->enableModules(array('field_plugins_test')); + + $field_name = 'test_field'; + // Create a field and an instance. + $field = entity_create('field_storage_config', array( + 'name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'text' + )); + $field->save(); + $instance = entity_create('field_instance_config', array( + 'field_storage' => $field, + 'bundle' => 'entity_test', + )); + $instance->save(); + + entity_create('entity_view_display', array( + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + ))->setComponent($field_name, array('type' => 'field_plugins_test_text_formatter'))->save(); + + // Check the component exists and is of the correct type. + $display = entity_get_display('entity_test', 'entity_test', 'default'); + $this->assertEqual($display->getComponent($field_name)['type'], 'field_plugins_test_text_formatter'); + + // Removing the field_plugins_test module should change the component to use + // the default formatter for test fields. + \Drupal::service('config.manager')->uninstall('module', 'field_plugins_test'); + $display = entity_get_display('entity_test', 'entity_test', 'default'); + $this->assertEqual($display->getComponent($field_name)['type'], 'text_default'); + + // Removing the text module should remove the field from the view display. + \Drupal::service('config.manager')->uninstall('module', 'text'); + $display = entity_get_display('entity_test', 'entity_test', 'default'); + $this->assertFalse($display->getComponent($field_name)); + } } diff --git a/core/modules/entity/src/Tests/EntityFormDisplayTest.php b/core/modules/entity/src/Tests/EntityFormDisplayTest.php index 6723a49..a6dfc07 100644 --- a/core/modules/entity/src/Tests/EntityFormDisplayTest.php +++ b/core/modules/entity/src/Tests/EntityFormDisplayTest.php @@ -219,4 +219,46 @@ public function testDeleteFieldInstance() { $this->assertFalse($display->getComponent($field_name)); } + /** + * Tests \Drupal\entity\EntityDisplayBase::onDependencyRemoval(). + */ + public function testOnDependencyRemoval() { + $this->enableModules(array('field_plugins_test')); + + $field_name = 'test_field'; + // Create a field and an instance. + $field = entity_create('field_storage_config', array( + 'name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'text' + )); + $field->save(); + $instance = entity_create('field_instance_config', array( + 'field_storage' => $field, + 'bundle' => 'entity_test', + )); + $instance->save(); + + entity_create('entity_form_display', array( + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'default', + ))->setComponent($field_name, array('type' => 'field_plugins_test_text_widget'))->save(); + + // Check the component exists and is of the correct type. + $display = entity_get_form_display('entity_test', 'entity_test', 'default'); + $this->assertEqual($display->getComponent($field_name)['type'], 'field_plugins_test_text_widget'); + + // Removing the field_plugins_test module should change the component to use + // the default widget for test fields. + \Drupal::service('config.manager')->uninstall('module', 'field_plugins_test'); + $display = entity_get_form_display('entity_test', 'entity_test', 'default'); + $this->assertEqual($display->getComponent($field_name)['type'], 'text_textfield'); + + // Removing the text module should remove the field from the form display. + \Drupal::service('config.manager')->uninstall('module', 'text'); + $display = entity_get_form_display('entity_test', 'entity_test', 'default'); + $this->assertFalse($display->getComponent($field_name)); + } + } diff --git a/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.info.yml b/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.info.yml new file mode 100644 index 0000000..86a6ec8 --- /dev/null +++ b/core/modules/field/tests/modules/field_plugins_test/field_plugins_test.info.yml @@ -0,0 +1,8 @@ +name: 'Field Plugins Test' +type: module +description: 'Support module for the field and entity display tests.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - text diff --git a/core/modules/field/tests/modules/field_plugins_test/src/Plugin/Field/FieldFormatter/TestTextTrimmedFormatter.php b/core/modules/field/tests/modules/field_plugins_test/src/Plugin/Field/FieldFormatter/TestTextTrimmedFormatter.php new file mode 100644 index 0000000..b8550f0 --- /dev/null +++ b/core/modules/field/tests/modules/field_plugins_test/src/Plugin/Field/FieldFormatter/TestTextTrimmedFormatter.php @@ -0,0 +1,29 @@ + 'details', - '#title' => $this->t('Configuration deletions'), - '#description' => $this->t('The listed configuration will be deleted.'), + '#title' => $this->t('Affected configuration'), + '#description' => $this->t('The listed configuration will be updated if possible, or deleted.'), '#collapsible' => TRUE, '#collapsed' => TRUE, '#access' => FALSE, diff --git a/core/modules/system/src/Tests/Module/UninstallTest.php b/core/modules/system/src/Tests/Module/UninstallTest.php index 300351c..e57bff0 100644 --- a/core/modules/system/src/Tests/Module/UninstallTest.php +++ b/core/modules/system/src/Tests/Module/UninstallTest.php @@ -49,7 +49,7 @@ function testUninstallPage() { $edit = array(); $edit['uninstall[module_test]'] = TRUE; $this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall')); - $this->assertNoText(\Drupal::translation()->translate('Configuration deletions'), 'No configuration deletions listed on the module install confirmation page.'); + $this->assertNoText(\Drupal::translation()->translate('Affected configuration'), 'No configuration deletions listed on the module install confirmation page.'); $this->drupalPostForm(NULL, NULL, t('Uninstall')); $this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.'); @@ -59,7 +59,7 @@ function testUninstallPage() { $edit = array(); $edit['uninstall[node]'] = TRUE; $this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall')); - $this->assertText(\Drupal::translation()->translate('Configuration deletions'), 'Configuration deletions listed on the module install confirmation page.'); + $this->assertText(\Drupal::translation()->translate('Affected configuration'), 'Configuration deletions listed on the module install confirmation page.'); $entity_types = array(); foreach ($node_dependencies as $entity) { diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index a1aff59..feea58f 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -1102,6 +1102,20 @@ public function getConfigDependencyName() { /** * {@inheritdoc} */ + public function onDependencyRemoval(array $dependencies) { + return $this->storage->onDependencyRemoval($dependencies); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() { + return $this->storage->getDependencies(); + } + + /** + * {@inheritdoc} + */ public function getCacheTag() { $this->storage->getCacheTag(); } diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php index aef8d23..492d884 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php @@ -170,12 +170,12 @@ public function testPreSaveDuringSync() { // synchronization. $this->entity->set('dependencies', array('module' => array('node'))); $this->entity->preSave($storage); - $this->assertEmpty($this->entity->get('dependencies')); + $this->assertEmpty($this->entity->getDependencies()); $this->entity->setSyncing(TRUE); $this->entity->set('dependencies', array('module' => array('node'))); $this->entity->preSave($storage); - $dependencies = $this->entity->get('dependencies'); + $dependencies = $this->entity->getDependencies(); $this->assertContains('node', $dependencies['module']); } @@ -188,19 +188,19 @@ public function testAddDependency() { $method->invoke($this->entity, 'module', $this->provider); $method->invoke($this->entity, 'module', 'core'); $method->invoke($this->entity, 'module', 'node'); - $dependencies = $this->entity->get('dependencies'); + $dependencies = $this->entity->getDependencies(); $this->assertNotContains($this->provider, $dependencies['module']); $this->assertNotContains('core', $dependencies['module']); $this->assertContains('node', $dependencies['module']); // Test sorting of dependencies. $method->invoke($this->entity, 'module', 'action'); - $dependencies = $this->entity->get('dependencies'); + $dependencies = $this->entity->getDependencies(); $this->assertEquals(array('action', 'node'), $dependencies['module']); // Test sorting of dependency types. $method->invoke($this->entity, 'entity', 'system.action.id'); - $dependencies = $this->entity->get('dependencies'); + $dependencies = $this->entity->getDependencies(); $this->assertEquals(array('entity', 'module'), array_keys($dependencies)); }