Commit 0bc927f2 authored by catch's avatar catch

Issue #2420107 by alexpott: Determine which config entities can be fixed and...

Issue #2420107 by alexpott: Determine which config entities can be fixed and which will be deleted when a dependency is removed
parent 70e4656d
......@@ -93,6 +93,16 @@ public function createSnapshot(StorageInterface $source_storage, StorageInterfac
*/
public function uninstall($type, $name);
/**
* Creates and populates a ConfigDependencyManager object.
*
* The configuration dependency manager is populated with data from the active
* store.
*
* @return \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
public function getConfigDependencyManager();
/**
* Finds config entities that are dependent on extensions or entities.
*
......@@ -101,8 +111,8 @@ public function uninstall($type, $name);
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of entity it
* should be a list of full configuration object names.
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityDependency[]
* An array of configuration entity dependency objects.
......@@ -117,14 +127,38 @@ public function findConfigEntityDependents($type, array $names);
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of entity it
* should be a list of full configuration object names.
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityInterface[]
* An array of dependencies as configuration entities.
*/
public function findConfigEntityDependentsAsEntities($type, array $names);
/**
* Lists which config entities to update and delete on removal of a dependency.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'config'
* or 'content'.
* @param array $names
* The specific names to check. If $type equals 'module' or 'theme' then it
* should be a list of module names or theme names. In the case of 'config'
* or 'content' it should be a list of configuration dependency names.
* @param bool $dry_run
* If set to FALSE the entities returned in the list of updates will be
* modified. In order to make the changes the caller needs to save them. If
* set to TRUE the entities returned will not be modified.
*
* @return array
* An array with the keys: 'update', 'delete' and 'unchanged'. The value of
* each is a list of configuration entities that need to have that action
* applied when the supplied dependencies are removed. Updates need to be
* processed before deletes. The order of the deletes is significant and
* must be processed in the returned order.
*/
public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE);
/**
* Determines if the provided collection supports configuration entities.
*
......
......@@ -286,4 +286,37 @@ public function setData(array $data) {
return $this;
}
/**
* Updates one of the lightweight ConfigEntityDependency objects.
*
* @param $name
* The configuration dependency name.
* @param array $dependencies
* The configuration dependencies. The array is structured like this:
* @code
* array(
* 'config => array(
* // An array of configuration entity object names.
* ),
* 'content => array(
* // An array of content entity configuration dependency names. The default
* // format is "ENTITY_TYPE_ID:BUNDLE:UUID".
* ),
* 'module' => array(
* // An array of module names.
* ),
* 'theme' => array(
* // An array of theme names.
* ),
* );
* @endcode
*
* @return $this
*/
public function updateData($name, array $dependencies) {
$this->graph = NULL;
$this->data[$name] = new ConfigEntityDependency($name, ['dependencies' => $dependencies]);
return $this;
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\config\Tests;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\simpletest\KernelTestBase;
/**
......@@ -21,7 +22,7 @@ class ConfigDependencyTest extends KernelTestBase {
*
* @var array
*/
public static $modules = array('system', 'config_test');
public static $modules = array('system', 'config_test', 'entity_test', 'user');
/**
* Tests that calculating dependencies for system module.
......@@ -189,6 +190,10 @@ public function testConfigEntityUninstall() {
$this->assertFalse($storage->load('entity1'), 'Entity 1 deleted');
$this->assertFalse($storage->load('entity2'), 'Entity 2 deleted');
// Set a more complicated test where dependencies will be fixed.
\Drupal::state()->set('config_test.fix_dependencies', array($entity1->getConfigDependencyName()));
// Entity1 will be deleted because it depends on node.
$entity1 = $storage->create(
array(
'id' => 'entity1',
......@@ -200,6 +205,10 @@ public function testConfigEntityUninstall() {
)
);
$entity1->save();
// Entity2 has a dependency on Entity1 but it can be fixed because
// \Drupal\config_test\Entity::onDependencyRemoval() will remove the
// dependency before config entities are deleted.
$entity2 = $storage->create(
array(
'id' => 'entity2',
......@@ -211,16 +220,109 @@ public function testConfigEntityUninstall() {
)
);
$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.
// Entity3 will be unchanged because it is dependent on Entity2 which can
// be fixed.
$entity3 = $storage->create(
array(
'id' => 'entity3',
'dependencies' => array(
'enforced' => array(
'config' => array($entity2->getConfigDependencyName()),
),
),
)
);
$entity3->save();
// Entity4's config dependency will be fixed but it will still be deleted
// because it also depends on the node module.
$entity4 = $storage->create(
array(
'id' => 'entity4',
'dependencies' => array(
'enforced' => array(
'config' => array($entity1->getConfigDependencyName()),
'module' => array('node', 'config_test')
),
),
)
);
$entity4->save();
// Do a dry run using
// \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval().
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('module', ['node']);
$this->assertEqual($entity1->uuid(), $config_entities['delete'][0]->uuid(), 'Entity 1 will be deleted.');
$this->assertEqual($entity2->uuid(), reset($config_entities['update'])->uuid(), 'Entity 2 will be updated.');
$this->assertEqual($entity3->uuid(), reset($config_entities['unchanged'])->uuid(), 'Entity 3 is not changed.');
$this->assertEqual($entity4->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 4 will be deleted.');
// Perform the uninstall.
$config_manager->uninstall('module', 'node');
// Test that expected actions have been performed.
$this->assertFalse($storage->load('entity1'), 'Entity 1 deleted');
$entity2 = $storage->load('entity2');
$this->assertTrue($entity2, 'Entity 2 not deleted');
$this->assertEqual($entity2->calculateDependencies()['config'], array(), 'Entity 2 dependencies updated to remove dependency on Entity1.');
$entity3 = $storage->load('entity3');
$this->assertTrue($entity3, 'Entity 3 not deleted');
$this->assertEqual($entity3->calculateDependencies()['config'], [$entity2->getConfigDependencyName()], 'Entity 3 still depends on Entity 2.');
$this->assertFalse($storage->load('entity4'), 'Entity 4 deleted');
}
/**
* Tests getConfigEntitiesToChangeOnDependencyRemoval() with content entities.
*
* At the moment there is no runtime code that calculates configuration
* dependencies on content entity delete because this calculation is expensive
* and all content dependencies are soft. This test ensures that the code
* works for content entities.
*
* @see \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval()
*/
public function testContentEntityDelete() {
$this->installEntitySchema('entity_test');
/** @var \Drupal\Core\Config\ConfigManagerInterface $config_manager */
$config_manager = \Drupal::service('config.manager');
$content_entity = EntityTest::create();
$content_entity->save();
/** @var \Drupal\Core\Config\Entity\ConfigEntityStorage $storage */
$storage = $this->container->get('entity.manager')->getStorage('config_test');
$entity1 = $storage->create(
array(
'id' => 'entity1',
'dependencies' => array(
'enforced' => array(
'content' => array($content_entity->getConfigDependencyName())
),
),
)
);
$entity1->save();
$entity2 = $storage->create(
array(
'id' => 'entity2',
'dependencies' => array(
'enforced' => array(
'config' => array($entity1->getConfigDependencyName())
),
),
)
);
$entity2->save();
// Create a configuration entity that is not in the dependency chain.
$entity3 = $storage->create(array('id' => 'entity3'));
$entity3->save();
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('content', [$content_entity->getConfigDependencyName()]);
$this->assertEqual($entity1->uuid(), $config_entities['delete'][0]->uuid(), 'Entity 1 will be deleted.');
$this->assertEqual($entity2->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 2 will be deleted.');
$this->assertTrue(empty($config_entities['update']), 'No dependencies of the content entity will be updated.');
$this->assertTrue(empty($config_entities['unchanged']), 'No dependencies of the content entity will be unchanged.');
}
/**
......
......@@ -123,6 +123,9 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti
*/
public function onDependencyRemoval(array $dependencies) {
$changed = parent::onDependencyRemoval($dependencies);
if (!isset($this->dependencies['enforced']['config'])) {
return $changed;
}
$fix_deps = \Drupal::state()->get('config_test.fix_dependencies', array());
foreach ($dependencies['config'] as $entity) {
if (in_array($entity->getConfigDependencyName(), $fix_deps)) {
......
......@@ -145,43 +145,83 @@ public function buildForm(array $form, FormStateInterface $form_state) {
}, $this->modules),
);
$form['entities'] = array(
// Get the dependent entities.
$entity_types = array();
$dependent_entities = $this->configManager->getConfigEntitiesToChangeOnDependencyRemoval('module', $this->modules);
$form['entity_updates'] = array(
'#type' => 'details',
'#title' => $this->t('Affected configuration'),
'#description' => $this->t('The listed configuration will be updated if possible, or deleted.'),
'#title' => $this->t('Configuration updates'),
'#description' => $this->t('The listed configuration will be updated.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#access' => FALSE,
);
// Get the dependent entities.
$entity_types = array();
$dependent_entities = $this->configManager->findConfigEntityDependentsAsEntities('module', $this->modules);
foreach ($dependent_entities as $entity) {
foreach ($dependent_entities['update'] as $entity) {
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
$entity_type_id = $entity->getEntityTypeId();
if (!isset($form['entity_updates'][$entity_type_id])) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
// Store the ID and label to sort the entity types and entities later.
$label = $entity_type->getLabel();
$entity_types[$entity_type_id] = $label;
$form['entity_updates'][$entity_type_id] = array(
'#theme' => 'item_list',
'#title' => $label,
'#items' => array(),
);
}
$form['entity_updates'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id();
}
if (!empty($dependent_entities['update'])) {
$form['entity_updates']['#access'] = TRUE;
// Add a weight key to the entity type sections.
asort($entity_types, SORT_FLAG_CASE);
$weight = 0;
foreach ($entity_types as $entity_type_id => $label) {
$form['entity_updates'][$entity_type_id]['#weight'] = $weight;
// Sort the list of entity labels alphabetically.
sort($form['entity_updates'][$entity_type_id]['#items'], SORT_FLAG_CASE);
$weight++;
}
}
$form['entity_deletes'] = array(
'#type' => 'details',
'#title' => $this->t('Configuration deletions'),
'#description' => $this->t('The listed configuration will be deleted.'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
'#access' => FALSE,
);
foreach ($dependent_entities['delete'] as $entity) {
$entity_type_id = $entity->getEntityTypeId();
if (!isset($form['entities'][$entity_type_id])) {
if (!isset($form['entity_deletes'][$entity_type_id])) {
$entity_type = $this->entityManager->getDefinition($entity_type_id);
// Store the ID and label to sort the entity types and entities later.
$label = $entity_type->getLabel();
$entity_types[$entity_type_id] = $label;
$form['entities'][$entity_type_id] = array(
$form['entity_deletes'][$entity_type_id] = array(
'#theme' => 'item_list',
'#title' => $label,
'#items' => array(),
);
}
$form['entities'][$entity_type_id]['#items'][] = $entity->label();
$form['entity_deletes'][$entity_type_id]['#items'][] = $entity->label() ?: $entity->id();
}
if (!empty($dependent_entities)) {
$form['entities']['#access'] = TRUE;
if (!empty($dependent_entities['delete'])) {
$form['entity_deletes']['#access'] = TRUE;
// Add a weight key to the entity type sections.
asort($entity_types, SORT_FLAG_CASE);
$weight = 0;
foreach ($entity_types as $entity_type_id => $label) {
$form['entities'][$entity_type_id]['#weight'] = $weight;
$form['entity_deletes'][$entity_type_id]['#weight'] = $weight;
// Sort the list of entity labels alphabetically.
sort($form['entities'][$entity_type_id]['#items'], SORT_FLAG_CASE);
sort($form['entity_deletes'][$entity_type_id]['#items'], SORT_FLAG_CASE);
$weight++;
}
}
......
......@@ -45,7 +45,9 @@ function testUninstallPage() {
$this->drupalLogin($account);
// Create a node type.
$node_type = entity_create('node_type', array('type' => 'uninstall_blocker'));
$node_type = entity_create('node_type', array('type' => 'uninstall_blocker', 'name' => 'Uninstall blocker'));
// Create a dependency that can be fixed.
$node_type->setThirdPartySetting('module_test', 'key', 'value');
$node_type->save();
// Add a node to prevent node from being uninstalled.
$node = entity_create('node', array('type' => 'uninstall_blocker'));
......@@ -57,13 +59,14 @@ function testUninstallPage() {
$this->assertText(\Drupal::translation()->translate('The following reasons prevents Node from being uninstalled: There is content for the entity type: Content'), 'Content prevents uninstalling node module.');
// Delete the node to allow node to be uninstalled.
$node->delete();
$node_type->delete();
// Uninstall module_test.
$edit = array();
$edit['uninstall[module_test]'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->assertNoText(\Drupal::translation()->translate('Affected configuration'), 'No configuration deletions listed on the module install confirmation page.');
$this->assertNoText(\Drupal::translation()->translate('Configuration deletions'), 'No configuration deletions listed on the module install confirmation page.');
$this->assertText(\Drupal::translation()->translate('Configuration updates'), 'Configuration updates listed on the module install confirmation page.');
$this->assertText($node_type->label(), String::format('The entity label "!label" found.', array('!label' => $node_type->label())));
$this->drupalPostForm(NULL, NULL, t('Uninstall'));
$this->assertText(t('The selected modules have been uninstalled.'), 'Modules status has been updated.');
......@@ -73,11 +76,12 @@ function testUninstallPage() {
$edit = array();
$edit['uninstall[node]'] = TRUE;
$this->drupalPostForm('admin/modules/uninstall', $edit, t('Uninstall'));
$this->assertText(\Drupal::translation()->translate('Affected configuration'), 'Configuration deletions listed on the module install confirmation page.');
$this->assertText(\Drupal::translation()->translate('Configuration deletions'), 'Configuration deletions listed on the module install confirmation page.');
$this->assertNoText(\Drupal::translation()->translate('Configuration updates'), 'No configuration updates listed on the module install confirmation page.');
$entity_types = array();
foreach ($node_dependencies as $entity) {
$label = $entity->label();
$label = $entity->label() ?: $entity->id();
$this->assertText($label, String::format('The entity label "!label" found.', array('!label' => $label)));
$entity_types[] = $entity->getEntityTypeId();
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment