Commit 9bcfe6d5 authored by catch's avatar catch
parent 07c81f12
...@@ -292,78 +292,85 @@ public function findConfigEntityDependentsAsEntities($type, array $names, Config ...@@ -292,78 +292,85 @@ public function findConfigEntityDependentsAsEntities($type, array $names, Config
* {@inheritdoc} * {@inheritdoc}
*/ */
public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) { public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) {
// Determine the current list of dependent configuration entities and set up
// initial values.
$dependency_manager = $this->getConfigDependencyManager(); $dependency_manager = $this->getConfigDependencyManager();
$dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
$original_dependencies = $dependents;
$delete_uuids = [];
// Store the list of dependents in three separate variables. This allows us
// to determine how the dependency graph changes as entities are fixed by
// calling the onDependencyRemoval() method.
// The list of original dependents on $names. This list never changes.
$original_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
// The current list of dependents on $names. This list is recalculated when
// calling an entity's onDependencyRemoval() method results in the entity
// changing. This list is passed to each entity's onDependencyRemoval()
// method as the list of affected entities.
$current_dependents = $original_dependents;
// The list of dependents to process. This list changes as entities are
// processed and are either fixed or deleted.
$dependents_to_process = $original_dependents;
// Initialize other variables.
$affected_uuids = [];
$return = [ $return = [
'update' => [], 'update' => [],
'delete' => [], 'delete' => [],
'unchanged' => [], 'unchanged' => [],
]; ];
// Create a map of UUIDs to $original_dependencies key so that we can remove // Try to fix the dependents and find out what will happen to the dependency
// fixed dependencies. // graph. Entities are processed in the order of most dependent first. For
$uuid_map = []; // example, this ensures that Menu UI third party dependencies on node types
foreach ($original_dependencies as $key => $entity) { // are fixed before processing the node type's other dependents.
$uuid_map[$entity->uuid()] = $key; while ($dependent = array_pop($dependents_to_process)) {
}
// 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 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 */ /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */
if ($dry_run) { if ($dry_run) {
// Clone the entity so any changes do not change any static caches. // Clone the entity so any changes do not change any static caches.
$dependent = clone $dependent; $dependent = clone $dependent;
} }
$fixed = FALSE; $fixed = FALSE;
if ($this->callOnDependencyRemoval($dependent, $original_dependencies, $type, $names)) { if ($this->callOnDependencyRemoval($dependent, $current_dependents, $type, $names)) {
// Recalculate dependencies and update the dependency graph data. // Recalculate dependencies and update the dependency graph data.
$dependent->calculateDependencies(); $dependent->calculateDependencies();
$dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies()); $dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies());
// Based on the updated data rebuild the list of dependents. This will // Based on the updated data rebuild the list of current dependents.
// remove entities that are no longer dependent after the recalculation. // This will remove entities that are no longer dependent after the
$dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager); // recalculation.
// Remove any entities that we've already marked for deletion. $current_dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager);
$dependents = array_filter($dependents, function ($dependent) use ($delete_uuids) { // Rebuild the list of entities that we need to process using the new
return !in_array($dependent->uuid(), $delete_uuids); // list of current dependents and removing any entities that we've
// already processed.
$dependents_to_process = array_filter($current_dependents, function ($current_dependent) use ($affected_uuids) {
return !in_array($current_dependent->uuid(), $affected_uuids);
}); });
// Ensure that the dependency has actually been fixed. It is possible // Ensure that the dependent has actually been fixed. It is possible
// that the dependent has multiple dependencies that cause it to be in // that other dependencies cause it to still be in the list.
// the dependency chain.
$fixed = TRUE; $fixed = TRUE;
foreach ($dependents as $key => $entity) { foreach ($dependents_to_process as $key => $entity) {
if ($entity->uuid() == $dependent->uuid()) { if ($entity->uuid() == $dependent->uuid()) {
$fixed = FALSE; $fixed = FALSE;
unset($dependents[$key]); unset($dependents_to_process[$key]);
break; break;
} }
} }
if ($fixed) { if ($fixed) {
// Remove the fixed dependency from the list of original dependencies. $affected_uuids[] = $dependent->uuid();
unset($original_dependencies[$uuid_map[$dependent->uuid()]]);
$return['update'][] = $dependent; $return['update'][] = $dependent;
} }
} }
// If the entity cannot be fixed then it has to be deleted. // If the entity cannot be fixed then it has to be deleted.
if (!$fixed) { if (!$fixed) {
$delete_uuids[] = $dependent->uuid(); $affected_uuids[] = $dependent->uuid();
// Deletes should occur in the order of the least dependent first. For // Deletes should occur in the order of the least dependent first. For
// example, this ensures that fields are removed before field storages. // example, this ensures that fields are removed before field storages.
array_unshift($return['delete'], $dependent); array_unshift($return['delete'], $dependent);
} }
} }
// Use the lists of UUIDs to filter the original list to work out which // Use the list of affected UUIDs to filter the original list to work out
// configuration entities are unchanged. // which configuration entities are unchanged.
$return['unchanged'] = array_filter($original_dependencies, function ($dependent) use ($delete_uuids) { $return['unchanged'] = array_filter($original_dependents, function ($dependent) use ($affected_uuids) {
return !(in_array($dependent->uuid(), $delete_uuids)); return !(in_array($dependent->uuid(), $affected_uuids));
}); });
return $return; return $return;
......
...@@ -117,9 +117,12 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti ...@@ -117,9 +117,12 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onDependencyRemoval(array $dependencies) { public function onDependencyRemoval(array $dependencies) {
// Record which entities have this method called on. // Record which entities have this method called on and what dependencies
// are passed.
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []); $called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$called[] = $this->id(); $called[$this->id()] = $dependencies;
$called[$this->id()]['config'] = array_keys($called[$this->id()]['config']);
$called[$this->id()]['content'] = array_keys($called[$this->id()]['content']);
\Drupal::state()->set('config_test.on_dependency_removal_called', $called); \Drupal::state()->set('config_test.on_dependency_removal_called', $called);
$changed = parent::onDependencyRemoval($dependencies); $changed = parent::onDependencyRemoval($dependencies);
......
...@@ -235,9 +235,9 @@ public function providerConfigEntityUninstallComplex() { ...@@ -235,9 +235,9 @@ public function providerConfigEntityUninstallComplex() {
// Ensure that alphabetical order has no influence on dependency fixing and // Ensure that alphabetical order has no influence on dependency fixing and
// removal. // removal.
return [ return [
[['a', 'b', 'c', 'd']], [['a', 'b', 'c', 'd', 'e']],
[['d', 'c', 'b', 'a']], [['e', 'd', 'c', 'b', 'a']],
[['c', 'd', 'a', 'b']], [['e', 'c', 'd', 'a', 'b']],
]; ];
} }
...@@ -316,6 +316,25 @@ public function testConfigEntityUninstallComplex(array $entity_id_suffixes) { ...@@ -316,6 +316,25 @@ public function testConfigEntityUninstallComplex(array $entity_id_suffixes) {
); );
$entity_4->save(); $entity_4->save();
// Entity 5 will be fixed because it is dependent on entity 3, which is
// unchanged, and entity 1 which will be fixed because
// \Drupal\config_test\Entity::onDependencyRemoval() will remove the
// dependency.
$entity_5 = $storage->create(
[
'id' => 'entity_' . $entity_id_suffixes[4],
'dependencies' => [
'enforced' => [
'config' => [
$entity_1->getConfigDependencyName(),
$entity_3->getConfigDependencyName(),
],
],
],
]
);
$entity_5->save();
// Set a more complicated test where dependencies will be fixed. // Set a more complicated test where dependencies will be fixed.
\Drupal::state()->set('config_test.fix_dependencies', [$entity_1->getConfigDependencyName()]); \Drupal::state()->set('config_test.fix_dependencies', [$entity_1->getConfigDependencyName()]);
\Drupal::state()->set('config_test.on_dependency_removal_called', []); \Drupal::state()->set('config_test.on_dependency_removal_called', []);
...@@ -323,14 +342,22 @@ public function testConfigEntityUninstallComplex(array $entity_id_suffixes) { ...@@ -323,14 +342,22 @@ public function testConfigEntityUninstallComplex(array $entity_id_suffixes) {
// Do a dry run using // Do a dry run using
// \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval(). // \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval().
$config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('module', ['node']); $config_entities = $config_manager->getConfigEntitiesToChangeOnDependencyRemoval('module', ['node']);
// Assert that \Drupal\config_test\Entity\ConfigTest::onDependencyRemoval()
// is called as expected and with the correct dependencies.
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$this->assertArrayNotHasKey($entity_3->id(), $called, 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.');
$this->assertSame([$entity_1->id(), $entity_4->id(), $entity_2->id(), $entity_5->id()], array_keys($called), 'The most dependent entites have ConfigEntityInterface::onDependencyRemoval() called first.');
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_1->id()]);
$this->assertSame(['config' => [$entity_1->getConfigDependencyName()], 'content' => [], 'module' => [], 'theme' => []], $called[$entity_2->id()]);
$this->assertSame(['config' => [$entity_1->getConfigDependencyName()], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_4->id()]);
$this->assertSame(['config' => [$entity_1->getConfigDependencyName()], 'content' => [], 'module' => [], 'theme' => []], $called[$entity_5->id()]);
$this->assertEqual($entity_1->uuid(), $config_entities['delete'][1]->uuid(), 'Entity 1 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_2->uuid(), $config_entities['update'][0]->uuid(), 'Entity 2 will be updated.');
$this->assertEqual($entity_3->uuid(), reset($config_entities['unchanged'])->uuid(), 'Entity 3 is not changed.'); $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.'); $this->assertEqual($entity_4->uuid(), $config_entities['delete'][0]->uuid(), 'Entity 4 will be deleted.');
$this->assertEqual($entity_5->uuid(), $config_entities['update'][1]->uuid(), 'Entity 5 is updated.');
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$this->assertFalse(in_array($entity_3->id(), $called), 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.');
$this->assertSame([$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 // Perform a module rebuild so we can know where the node module is located
// and uninstall it. // and uninstall it.
...@@ -443,8 +470,11 @@ public function testConfigEntityUninstallThirdParty() { ...@@ -443,8 +470,11 @@ public function testConfigEntityUninstallThirdParty() {
$this->assertSame($expected, $config_entity_ids); $this->assertSame($expected, $config_entity_ids);
$called = \Drupal::state()->get('config_test.on_dependency_removal_called', []); $called = \Drupal::state()->get('config_test.on_dependency_removal_called', []);
$this->assertFalse(in_array($entity_3->id(), $called), 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.'); $this->assertArrayNotHasKey($entity_3->id(), $called, 'ConfigEntityInterface::onDependencyRemoval() is not called for entity 3.');
$this->assertSame([$entity_1->id(), $entity_4->id(), $entity_2->id()], $called, 'The most dependent entities have ConfigEntityInterface::onDependencyRemoval() called first.'); $this->assertSame([$entity_1->id(), $entity_4->id(), $entity_2->id()], array_keys($called), 'The most dependent entities have ConfigEntityInterface::onDependencyRemoval() called first.');
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_1->id()]);
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_2->id()]);
$this->assertSame(['config' => [], 'content' => [], 'module' => ['node'], 'theme' => []], $called[$entity_4->id()]);
// Perform a module rebuild so we can know where the node module is located // Perform a module rebuild so we can know where the node module is located
// and uninstall it. // and uninstall it.
......
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