Issue #2260457 by alexpott, beejeebus: Fixed Allow config entities to remove dependent configuration keys when dependencies are deleted due to module uninstall.

8.0.x
Nathaniel Catchpole 2014-09-16 10:05:37 +01:00
parent 32d5530e58
commit f022decb56
16 changed files with 367 additions and 16 deletions

View File

@ -10,6 +10,7 @@ namespace Drupal\Core\Config;
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 @@ class ConfigManager implements ConfigManagerInterface {
*/
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 . '.');

View File

@ -372,6 +372,13 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
return $this->addDependencyTrait($type, $name);
}
/**
* {@inheritdoc}
*/
public function getDependencies() {
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
@ -379,4 +386,10 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
return $this->getEntityType()->getConfigPrefix() . '.' . $this->id();
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
}
}

View File

@ -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'];

View File

@ -157,4 +157,37 @@ interface ConfigEntityInterface extends EntityInterface {
*/
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();
}

View File

@ -8,10 +8,11 @@
namespace Drupal\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ThirdPartySettingsTrait;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Entity\Display\EntityDisplayInterface;
use Drupal\field\Entity\FieldInstanceConfig;
use Drupal\Core\Config\Entity\ThirdPartySettingsTrait;
use Drupal\field\FieldInstanceConfigInterface;
/**
* Provides a common base class for entity view and form displays.
@ -381,4 +382,32 @@ abstract class EntityDisplayBase extends ConfigEntityBase implements EntityDispl
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();
}
}
}

View File

@ -145,6 +145,69 @@ class ConfigDependencyTest extends DrupalUnitTestBase {
}
/**
* 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.
*

View File

@ -136,4 +136,24 @@ class ConfigTest extends ConfigEntityBase implements ConfigTestInterface {
}
}
/**
* {@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();
}
}
}

View File

@ -364,4 +364,45 @@ class EntityDisplayTest extends KernelTestBase {
$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));
}
}

View File

@ -219,4 +219,46 @@ class EntityFormDisplayTest extends DrupalUnitTestBase {
$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));
}
}

View File

@ -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

View File

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\field_plugins_test\Plugin\Field\FieldFormatter\TextTrimmedFormatter.
*/
namespace Drupal\field_plugins_test\Plugin\Field\FieldFormatter;
use Drupal\text\Plugin\Field\FieldFormatter\TextTrimmedFormatter;
/**
* Plugin implementation of the 'field_plugins_test_text_formatter' formatter.
*
* @FieldFormatter(
* id = "field_plugins_test_text_formatter",
* label = @Translation("Test Trimmed"),
* field_types = {
* "text",
* "text_long",
* "text_with_summary"
* },
* quickedit = {
* "editor" = "form"
* }
* )
*/
class TestTextTrimmedFormatter extends TextTrimmedFormatter {
}

View File

@ -0,0 +1,25 @@
<?php
/**
* @file
* Contains \Drupal\field_plugins_test\Plugin\Field\FieldWidget\TestTextfieldWidget.
*/
namespace Drupal\field_plugins_test\Plugin\Field\FieldWidget;
use Drupal\text\Plugin\Field\FieldWidget\TextfieldWidget;
/**
* Plugin implementation of the 'field_plugins_test_text_widget' widget.
*
* @FieldWidget(
* id = "field_plugins_test_text_widget",
* label = @Translation("Test Text field"),
* field_types = {
* "text",
* "string"
* },
* )
*/
class TestTextfieldWidget extends TextfieldWidget {
}

View File

@ -147,8 +147,8 @@ class ModulesUninstallConfirmForm extends ConfirmFormBase {
$form['entities'] = array(
'#type' => '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,

View File

@ -49,7 +49,7 @@ class UninstallTest extends WebTestBase {
$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 @@ class UninstallTest extends WebTestBase {
$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) {

View File

@ -1099,6 +1099,20 @@ class ViewUI implements ViewStorageInterface {
public function getConfigDependencyName() {
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
return $this->storage->onDependencyRemoval($dependencies);
}
/**
* {@inheritdoc}
*/
public function getDependencies() {
return $this->storage->getDependencies();
}
/**
* {@inheritdoc}
*/

View File

@ -170,12 +170,12 @@ class ConfigEntityBaseUnitTest extends UnitTestCase {
// 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 @@ class ConfigEntityBaseUnitTest extends UnitTestCase {
$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));
}