Issue #2260457 by alexpott, beejeebus: Fixed Allow config entities to remove dependent configuration keys when dependencies are deleted due to module uninstall.
parent
32d5530e58
commit
f022decb56
|
@ -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 . '.');
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue