From 884aeef38b805ee6c4d2ab9370db770171451303 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Fri, 24 Jun 2016 16:32:22 +0100 Subject: [PATCH] Issue #2754477 by alexpott, mr.baileys, swentel: Unexpected config entity delete due to dependency calculations --- core/lib/Drupal/Core/Config/ConfigManager.php | 9 +- .../Config/Entity/ConfigDependencyManager.php | 28 +++-- ...m_display.taxonomy_term.forums.default.yml | 1 + .../Core/Config/ConfigDependencyTest.php | 115 ++++++++++++------ .../Core/Config/ConfigImporterTest.php | 2 +- 5 files changed, 101 insertions(+), 54 deletions(-) diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index 1ab9fdb55f8d..11d7c84398cf 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -307,8 +307,9 @@ public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names // Try to fix any dependencies and find out what will happen to the // dependency graph. Entities are processed in the order of most dependent - // first. For example, this ensures that fields are removed before - // field storages. + // first. For example, this ensures that Menu UI third party dependencies on + // node types are fixed before processing the node type's other + // dependencies. while ($dependent = array_pop($dependents)) { /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */ if ($dry_run) { @@ -346,7 +347,9 @@ public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names // If the entity cannot be fixed then it has to be deleted. if (!$fixed) { $delete_uuids[] = $dependent->uuid(); - $return['delete'][] = $dependent; + // Deletes should occur in the order of the least dependent first. For + // example, this ensures that fields are removed before field storages. + array_unshift($return['delete'], $dependent); } } // Use the lists of UUIDs to filter the original list to work out which diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php b/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php index 377f7c4d00d2..6b3793e60bb5 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigDependencyManager.php @@ -173,7 +173,9 @@ public function getDependentEntities($type, $name) { // dependent is at the top. For example, this ensures that fields are // always after field storages. This is because field storages need to be // created before a field. - return array_reverse(array_intersect_key($this->graph, $dependencies)); + $graph = $this->getGraph(); + uasort($graph, array($this, 'sortGraph')); + return array_replace(array_intersect_key($graph, $dependencies), $dependencies); } /** @@ -188,7 +190,8 @@ public function sortAll() { // Sort by reverse weight and alphabetically. The most dependent entities // are last and entities with the same weight are alphabetically ordered. uasort($graph, array($this, 'sortGraph')); - return array_keys($graph); + // Use array_intersect_key() to exclude modules and themes from the list. + return array_reverse(array_keys(array_intersect_key($graph, $this->data))); } /** @@ -228,9 +231,11 @@ protected function createGraphConfigEntityDependencies($entities_to_check) { $graph = $this->getGraph(); foreach ($entities_to_check as $entity) { - if (isset($graph[$entity]) && !empty($graph[$entity]['reverse_paths'])) { - foreach ($graph[$entity]['reverse_paths'] as $dependency => $value) { - $dependent_entities[$dependency] = $this->data[$dependency]; + if (isset($graph[$entity]) && !empty($graph[$entity]['paths'])) { + foreach ($graph[$entity]['paths'] as $dependency => $value) { + if (isset($this->data[$dependency])) { + $dependent_entities[$dependency] = $this->data[$dependency]; + } } } } @@ -248,12 +253,13 @@ protected function getGraph() { $graph = array(); foreach ($this->data as $entity) { $graph_key = $entity->getConfigDependencyName(); - $graph[$graph_key]['edges'] = array(); - $dependencies = $entity->getDependencies('config'); - if (!empty($dependencies)) { - foreach ($dependencies as $dependency) { - $graph[$graph_key]['edges'][$dependency] = TRUE; - } + if (!isset($graph[$graph_key])) { + $graph[$graph_key]['edges'] = []; + } + // Include all dependencies in the graph so that topographical sorting + // works. + foreach (array_merge($entity->getDependencies('config'), $entity->getDependencies('module'), $entity->getDependencies('theme')) as $dependency) { + $graph[$dependency]['edges'][$graph_key] = TRUE; } } $graph_object = new Graph($graph); diff --git a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml index a2f890d30872..b18c869becd0 100644 --- a/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml +++ b/core/modules/forum/config/optional/core.entity_form_display.taxonomy_term.forums.default.yml @@ -2,6 +2,7 @@ langcode: en status: true dependencies: config: + - field.field.taxonomy_term.forums.forum_container - taxonomy.vocabulary.forums module: - text diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigDependencyTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigDependencyTest.php index 2fd6301f5c7f..d30b3c53b7d9 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigDependencyTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigDependencyTest.php @@ -188,7 +188,8 @@ 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'); + $storage = $this->container->get('entity.manager') + ->getStorage('config_test'); // Test dependencies between modules. $entity1 = $storage->create( array( @@ -221,14 +222,42 @@ public function testConfigEntityUninstall() { $config_manager->uninstall('module', 'node'); $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())); - \Drupal::state()->set('config_test.on_dependency_removal_called', []); - // Entity1 will be deleted because it depends on node. - $entity1 = $storage->create( + /** + * Data provider for self::testConfigEntityUninstallComplex(). + */ + public function providerConfigEntityUninstallComplex() { + // Ensure that alphabetical order has no influence on dependency fixing and + // removal. + return [ + [['a', 'b', 'c', 'd']], + [['d', 'c', 'b', 'a']], + [['c', 'd', 'a', 'b']], + ]; + } + + /** + * Tests complex configuration entity dependency handling during uninstall. + * + * Configuration entities can be deleted or updated during module uninstall + * because they have dependencies on the module. + * + * @param array $entity_id_suffixes + * The suffixes to add to the 4 entities created by the test. + * + * @dataProvider providerConfigEntityUninstallComplex + */ + public function testConfigEntityUninstallComplex(array $entity_id_suffixes) { + /** @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'); + // Entity 1 will be deleted because it depends on node. + $entity_1 = $storage->create( array( - 'id' => 'entity1', + 'id' => 'entity_' . $entity_id_suffixes[0], 'dependencies' => array( 'enforced' => array( 'module' => array('node', 'config_test') @@ -236,77 +265,85 @@ public function testConfigEntityUninstall() { ), ) ); - $entity1->save(); + $entity_1->save(); - // Entity2 has a dependency on Entity1 but it can be fixed because + // Entity 2 has a dependency on entity 1 but it can be fixed because // \Drupal\config_test\Entity::onDependencyRemoval() will remove the // dependency before config entities are deleted. - $entity2 = $storage->create( + $entity_2 = $storage->create( array( - 'id' => 'entity2', + 'id' => 'entity_' . $entity_id_suffixes[1], 'dependencies' => array( 'enforced' => array( - 'config' => array($entity1->getConfigDependencyName()), + 'config' => array($entity_1->getConfigDependencyName()), ), ), ) ); - $entity2->save(); + $entity_2->save(); - // Entity3 will be unchanged because it is dependent on Entity2 which can + // Entity 3 will be unchanged because it is dependent on entity 2 which can // be fixed. The ConfigEntityInterface::onDependencyRemoval() method will // not be called for this entity. - $entity3 = $storage->create( + $entity_3 = $storage->create( array( - 'id' => 'entity3', + 'id' => 'entity_' . $entity_id_suffixes[2], 'dependencies' => array( 'enforced' => array( - 'config' => array($entity2->getConfigDependencyName()), + 'config' => array($entity_2->getConfigDependencyName()), ), ), ) ); - $entity3->save(); + $entity_3->save(); - // Entity4's config dependency will be fixed but it will still be deleted + // Entity 4's config dependency will be fixed but it will still be deleted // because it also depends on the node module. - $entity4 = $storage->create( + $entity_4 = $storage->create( array( - 'id' => 'entity4', + 'id' => 'entity_' . $entity_id_suffixes[3], 'dependencies' => array( 'enforced' => array( - 'config' => array($entity1->getConfigDependencyName()), + 'config' => array($entity_1->getConfigDependencyName()), 'module' => array('node', 'config_test') ), ), ) ); - $entity4->save(); + $entity_4->save(); + + // Set a more complicated test where dependencies will be fixed. + \Drupal::state()->set('config_test.fix_dependencies', array($entity_1->getConfigDependencyName())); + \Drupal::state()->set('config_test.on_dependency_removal_called', []); // 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.'); + $this->assertEqual($entity_1->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 1 will be deleted.'); + $this->assertEqual($entity_2->uuid(), reset($config_entities['update'])->uuid(), 'Entity 2 will be updated.'); + $this->assertEqual($entity_3->uuid(), reset($config_entities['unchanged'])->uuid(), 'Entity 3 is not changed.'); + $this->assertEqual($entity_4->uuid(), $config_entities['delete'][0]->uuid(), 'Entity 4 will be deleted.'); $called = \Drupal::state()->get('config_test.on_dependency_removal_called', []); - $this->assertFalse(in_array($entity3->id(), $called), 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.'); - $this->assertIdentical(['entity1', 'entity2', 'entity4'], $called, 'The most dependent entites have ConfigEntityInterface::onDependencyRemoval() called first.'); + $this->assertFalse(in_array($entity_3->id(), $called), 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.'); + $this->assertIdentical([$entity_1->id(), $entity_4->id(), $entity_2->id()], $called, 'The most dependent entites have ConfigEntityInterface::onDependencyRemoval() called first.'); + // Perform a module rebuild so we can know where the node module is located + // and uninstall it. + // @todo Remove as part of https://www.drupal.org/node/2186491 + system_rebuild_module_data(); // 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()->getDependencies()['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()->getDependencies()['config'], [$entity2->getConfigDependencyName()], 'Entity 3 still depends on Entity 2.'); - $this->assertFalse($storage->load('entity4'), 'Entity 4 deleted'); + $this->assertFalse($storage->load($entity_1->id()), 'Entity 1 deleted'); + $entity_2 = $storage->load($entity_2->id()); + $this->assertTrue($entity_2, 'Entity 2 not deleted'); + $this->assertEqual($entity_2->calculateDependencies()->getDependencies()['config'], array(), 'Entity 2 dependencies updated to remove dependency on entity 1.'); + $entity_3 = $storage->load($entity_3->id()); + $this->assertTrue($entity_3, 'Entity 3 not deleted'); + $this->assertEqual($entity_3->calculateDependencies()->getDependencies()['config'], [$entity_2->getConfigDependencyName()], 'Entity 3 still depends on entity 2.'); + $this->assertFalse($storage->load($entity_4->id()), 'Entity 4 deleted'); } /** @@ -456,8 +493,8 @@ public function testContentEntityDelete() { $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->assertEqual($entity1->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 1 will be deleted.'); + $this->assertEqual($entity2->uuid(), $config_entities['delete'][0]->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.'); } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php index 945e994e3ba7..8fff1fa7d92e 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php @@ -348,8 +348,8 @@ function testSecondaryUpdateDeletedDeleterFirst() { $updates = $this->configImporter->reset()->getStorageComparer()->getChangelist('update'); $expected = array( $name_deleter, - $name_deletee, $name_other, + $name_deletee, ); $this->assertIdentical($expected, $updates); -- GitLab