diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index cde0eab..bf40b38 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 (array_reverse($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['entity'], $affected_dependencies['module'], $affected_dependencies['theme']); + } + + // 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 4c0286a..7d30ade 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -368,8 +368,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 $entities = array(), array $modules = array(), array $themes = array()) { + } + } 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 b355cb5..b288328 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityInterface.php @@ -156,4 +156,35 @@ 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. For example, when a module is being + * uninstalled configuration entities can use this method to avoid being + * unnecessarily deleted. + * @see \Drupal\Core\Config\ConfigManager::uninstall() + * + * @param \Drupal\Core\Config\ConfigEntityInterface[] $entities + * An array of entity dependencies. + * @param array $modules + * An array of module names. + * @param array $themes + * An array of theme names. + */ + public function onDependencyRemoval(array $entities = array(), array $modules = array(), array $themes = array()); + + /** + * Gets the configuration dependencies. + * + * @param $type + * (optional) The type of dependencies to return. Can be either 'module', + * 'theme', or 'entity' + * + * @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/modules/config/src/Tests/ConfigDependencyTest.php b/core/modules/config/src/Tests/ConfigDependencyTest.php index 0cb93e8..fd3f320 100644 --- a/core/modules/config/src/Tests/ConfigDependencyTest.php +++ b/core/modules/config/src/Tests/ConfigDependencyTest.php @@ -152,6 +152,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 b5f2dea..c1ca46e 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 $entities = array(), array $modules = array(), array $themes = array()) { + $changed = FALSE; + $fix_deps = \Drupal::state()->get('config_test.fix_dependencies', array()); + foreach ($entities 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/EntityDisplayBase.php b/core/modules/entity/src/EntityDisplayBase.php index ff6d40e..db363da 100644 --- a/core/modules/entity/src/EntityDisplayBase.php +++ b/core/modules/entity/src/EntityDisplayBase.php @@ -379,4 +379,32 @@ private function fieldHasDisplayOptions(FieldDefinitionInterface $definition) { return $definition->getDisplayOptions($this->displayContext); } + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $entities = array(), array $modules = array(), array $themes = array()) { + $changed = FALSE; + foreach ($entities as $entity) { + if ($entity instanceof FieldInstanceConfig) { + // 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'], $modules)) { + // 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/entity/src/Tests/EntityDisplayTest.php b/core/modules/entity/src/Tests/EntityDisplayTest.php index f0c8e0c..df0d3fb 100644 --- a/core/modules/entity/src/Tests/EntityDisplayTest.php +++ b/core/modules/entity/src/Tests/EntityDisplayTest.php @@ -365,4 +365,45 @@ public function testDeleteFieldInstance() { $this->assertFalse($display->getComponent($field_name)); } + /** + * Tests \Drupal\entity\EntityDisplayBase::onDependencyRemoval(). + */ + public function testOnDependencyRemoval() { + $this->enableModules(array('field_sql_storage', 'field_plugins_test')); + + $field_name = 'test_field'; + // Create a field and an instance. + $field = entity_create('field_config', array( + 'name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'text' + )); + $field->save(); + $instance = entity_create('field_instance_config', array( + 'field' => $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 6caf775..96be17f 100644 --- a/core/modules/entity/src/Tests/EntityFormDisplayTest.php +++ b/core/modules/entity/src/Tests/EntityFormDisplayTest.php @@ -228,4 +228,46 @@ public function testDeleteFieldInstance() { $this->assertFalse($display->getComponent($field_name)); } + /** + * Tests \Drupal\entity\EntityDisplayBase::onDependencyRemoval(). + */ + public function testOnDependencyRemoval() { + $this->enableModules(array('field_sql_storage', 'field_plugins_test')); + + $field_name = 'test_field'; + // Create a field and an instance. + $field = entity_create('field_config', array( + 'name' => $field_name, + 'entity_type' => 'entity_test', + 'type' => 'text' + )); + $field->save(); + $instance = entity_create('field_instance_config', array( + 'field' => $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..78051e3 --- /dev/null +++ b/core/modules/field/tests/modules/field_plugins_test/src/Plugin/Field/FieldFormatter/TestTextTrimmedFormatter.php @@ -0,0 +1,31 @@ + '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 bbb4399..9fdd985 100644 --- a/core/modules/system/src/Tests/Module/UninstallTest.php +++ b/core/modules/system/src/Tests/Module/UninstallTest.php @@ -55,7 +55,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.'); @@ -65,7 +65,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 3b9cb81..ce80a7c 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -1103,6 +1103,20 @@ public function getConfigDependencyName() { /** * {@inheritdoc} */ + public function onDependencyRemoval(array $entities = array(), array $modules = array(), array $themes = array()) { + return $this->storage->onDependencyRemoval($entities, $modules, $themes); + } + + /** + * {@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 a824b3a..8391428 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php @@ -183,12 +183,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']); } @@ -201,19 +201,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)); }