diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index 4ab0fd7..4e83d2e 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -94,6 +94,9 @@ _core_config_info: default_config_hash: type: string label: 'Default configuration hash' + dependency_resolved: + type: boolean + label: 'Signals if a config entity has resolved its dependencies' config_object: type: mapping diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index e5b471d..49f406d 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -21,7 +21,7 @@ /** * The ConfigManager provides helper functions for the configuration system. */ -class ConfigManager implements ConfigManagerInterface { +class ConfigManager implements ConfigManagerInterface, ConfigManagerDependencyFinderInterface { use StringTranslationTrait; /** @@ -297,10 +297,18 @@ public function findConfigEntityDependentsAsEntities($type, array $names, Config * {@inheritdoc} */ public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names, $dry_run = TRUE) { + return $this->getConfigEntitiesToChange(ConfigManagerInterface::REMOVE, $type, $names, $dry_run); + } + + /** + * {@inheritdoc} + */ + public function getConfigEntitiesToChange($operation, $type, array $names, $dry_run = TRUE) { + $this->normalizeNames($type, $names); // Determine the current list of dependent configuration entities and set up // initial values. $dependency_manager = $this->getConfigDependencyManager(); - $dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager); + $dependents = $this->findConfigEntityDependentsAsEntities($type, array_keys($names), $dependency_manager); $original_dependencies = $dependents; $update_uuids = []; @@ -318,18 +326,18 @@ public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names // Clone the entity so any changes do not change any static caches. $dependent = clone $dependent; } - if ($this->callOnDependencyRemoval($dependent, $original_dependencies, $type, $names)) { + if ($this->callOnDependencyResolver($operation, $dependent, $original_dependencies, $type, $names)) { // Recalculate dependencies and update the dependency graph data. $dependent->calculateDependencies(); $dependency_manager->updateData($dependent->getConfigDependencyName(), $dependent->getDependencies()); // Based on the updated data rebuild the list of dependents. - $dependents = $this->findConfigEntityDependentsAsEntities($type, $names, $dependency_manager); + $dependents = $this->findConfigEntityDependentsAsEntities($type, array_keys($names), $dependency_manager); // Ensure that the dependency has actually been fixed. It is possible // that the dependent has multiple dependencies that cause it to be in // the dependency chain. $fixed = TRUE; foreach ($dependents as $entity) { - if ($entity->uuid() == $dependent->uuid()) { + if (($entity->uuid() == $dependent->uuid()) && !$dependent->isDependencyResolved()) { $fixed = FALSE; break; } @@ -340,14 +348,24 @@ public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names } } } - // Now that we've fixed all the possible dependencies the remaining need to - // be deleted. Reverse the deletes so that entities are removed in the - // correct order of dependence. For example, this ensures that fields are - // removed before field storages. - $return['delete'] = array_reverse($dependents); - $delete_uuids = array_map(function($dependent) { - return $dependent->uuid(); - }, $return['delete']); + + // Now that we've fixed all the possible dependencies, for the remaining we + // need to apply the default action depending on the operation. + if ($operation == ConfigManagerInterface::REMOVE) { + // On remove the default action of unresolved dependencies is deletion. + // Reverse the deletes so that entities are removed in the correct order + // of dependence. For example, this ensures that fields are removed before + // field storages. + $return['delete'] = array_reverse($dependents); + $delete_uuids = array_map(function ($dependent) { + return $dependent->uuid(); + }, $return['delete']); + } + else { + // All other actions will leave the entities unchanged. + $delete_uuids = []; + } + // Use the lists of UUIDs to filter the original list to work out which // configuration entities are unchanged. $return['unchanged'] = array_filter($original_dependencies, function ($dependent) use ($delete_uuids, $update_uuids) { @@ -369,13 +387,16 @@ public function getConfigCollectionInfo() { } /** - * Calls an entity's onDependencyRemoval() method. + * Calls an entity's dependency resolver method. * - * A helper method to call onDependencyRemoval() with the correct list of - * affected entities. This list should only contain dependencies on the - * entity. Configuration and content entity dependencies will be converted - * into entity objects. + * A helper method to call the entity's dependency resolver method, such as: + * onDependencyRemoval() or onDependencyUpdate(). The methods are called with + * the correct list of affected entities. This list should only contain + * dependencies on the entity. Configuration and content entity dependencies + * will be converted into entity objects. * + * @param string $operation + * Can be one of the ConfigManagerInterface constants: REMOVE, UPDATE. * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity * The entity to call onDependencyRemoval() on. * @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $dependent_entities @@ -386,45 +407,28 @@ public function getConfigCollectionInfo() { * @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. + * or 'content' it should be a list of configuration dependency names. When + * type equals 'config' or 'content', the caller can choose to pass the + * names as array keys and the entire entities as values. * * @return bool * TRUE if the entity has changed as a result of calling the * onDependencyRemoval() method, FALSE if not. */ - protected function callOnDependencyRemoval(ConfigEntityInterface $entity, array $dependent_entities, $type, array $names) { + protected function callOnDependencyResolver($operation, ConfigEntityInterface $entity, array $dependent_entities, $type, array $names) { $entity_dependencies = $entity->getDependencies(); if (empty($entity_dependencies)) { // No dependent entities nothing to do. return FALSE; } - $affected_dependencies = array( - 'config' => array(), - 'content' => array(), - 'module' => array(), - 'theme' => array(), - ); + $affected_dependencies = ['config' => [], 'content' => [], 'module' => [], 'theme' => []]; // Work out if any of the entity's dependencies are going to be affected. if (isset($entity_dependencies[$type])) { // Work out which dependencies the entity has in common with the provided // $type and $names. - $affected_dependencies[$type] = array_intersect($entity_dependencies[$type], $names); - - // If the dependencies are entities we need to convert them into objects. - if ($type == 'config' || $type == 'content') { - $affected_dependencies[$type] = array_map(function ($name) use ($type) { - if ($type == 'config') { - return $this->loadConfigEntityByName($name); - } - else { - // Ignore the bundle. - list($entity_type_id,, $uuid) = explode(':', $name); - return $this->entityManager->loadEntityByConfigTarget($entity_type_id, $uuid); - } - }, $affected_dependencies[$type]); - } + $affected_dependencies[$type] = array_intersect_key($names, array_flip($entity_dependencies[$type])); } // Merge any other configuration entities into the list of affected @@ -432,21 +436,17 @@ protected function callOnDependencyRemoval(ConfigEntityInterface $entity, array if (isset($entity_dependencies['config'])) { foreach ($dependent_entities as $dependent_entity) { if (in_array($dependent_entity->getConfigDependencyName(), $entity_dependencies['config'])) { - $affected_dependencies['config'][] = $dependent_entity; + $affected_dependencies['config'][$dependent_entity->getConfigDependencyName()] = $dependent_entity; } } } - // Key the entity arrays by config dependency name to make searching easy. - foreach (['config', 'content'] as $dependency_type) { - $affected_dependencies[$dependency_type] = array_combine( - array_map(function ($entity) { return $entity->getConfigDependencyName(); }, $affected_dependencies[$dependency_type]), - $affected_dependencies[$dependency_type] - ); - } - // Inform the entity. - return $entity->onDependencyRemoval($affected_dependencies); + $method = $operation === ConfigManagerInterface::REMOVE ? 'onDependencyRemoval' : 'onDependencyUpdate'; + $changed = $entity->$method($affected_dependencies); + /** @var \Drupal\Core\Config\Entity\ConfigEntityDependencyResolverInterface $entity */ + $entity->setDependencyResolved($changed); + return $changed; } /** @@ -477,4 +477,48 @@ public function findMissingContentDependencies() { return $missing_dependencies; } + /** + * Normalize a list of dependency names. + * + * Each item will be keyed by the dependency name. The value will be the same + * dependency name when $type equals 'module' and 'theme' or the full entity + * object when $type equals 'config' and 'content'. + * + * @param string $type + * The type of dependency. Either 'module', 'theme', 'config' or 'content'. + * @param array $names + * A list of dependency names to be normalized 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. When type equals 'config' or 'content', the caller can + * pass the names as array keys and the full entity objects as values. + */ + protected function normalizeNames($type, array &$names) { + foreach ($names as $key => $value) { + // The name has been pass as string, in the value. + if (is_string($value)) { + if ($type == 'config') { + $names[$value] = $this->loadConfigEntityByName($value); + } + elseif ($type == 'content') { + // Ignore the bundle. + list($entity_type_id, , $uuid) = explode(':', $value); + $names[$value] = $this->entityManager->loadEntityByConfigTarget($entity_type_id, $uuid); + } + else { + $names[$value] = $value; + } + unset($names[$key]); + } + // The name has been passed as key and the configuration entity as value. + elseif ($value instanceof ConfigEntityInterface && is_string($key)) { + // Nothing needs to be done. + } + // Malformed item. + else { + unset($names[$key]); + } + } + } + } diff --git a/core/lib/Drupal/Core/Config/ConfigManagerDependencyFinderInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerDependencyFinderInterface.php new file mode 100644 index 0000000..9882882 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigManagerDependencyFinderInterface.php @@ -0,0 +1,49 @@ +calculateDependencies(); + // Fix any dependencies. + $config_entities = $this->getConfigManager()->getConfigEntitiesToChange(ConfigManagerInterface::UPDATE, 'config', [$this->getConfigDependencyName() => $this], FALSE); + if (!empty($config_entities['update'])) { + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent_entity */ + foreach ($config_entities['update'] as $dependent_entity) { + $dependent_entity->save(); + } + } } } @@ -498,6 +509,13 @@ public function onDependencyRemoval(array $dependencies) { /** * {@inheritdoc} + */ + public function onDependencyUpdate(array $dependencies) { + return FALSE; + } + + /** + * {@inheritdoc} * * Override to never invalidate the entity's cache tag; the config system * already invalidates it. @@ -628,4 +646,24 @@ public function save() { return $return; } + /** + * {@inheritdoc} + */ + public function setDependencyResolved($resolved) { + if ($resolved) { + $this->_core['dependency_resolved'] = TRUE; + } + else { + unset($this->_core['dependency_resolved']); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function isDependencyResolved() { + return isset($this->_core['dependency_resolved']) ? $this->_core['dependency_resolved'] : FALSE; + } + } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependencyResolverInterface.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependencyResolverInterface.php new file mode 100644 index 0000000..533244c --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependencyResolverInterface.php @@ -0,0 +1,63 @@ +isNew()) { - return; - } - - /** @var \Drupal\filter\FilterFormatInterface $original */ - $original = \Drupal::entityManager() - ->getStorage('filter_format') - ->loadUnchanged($format->getOriginalId()); - - // If the text format status is the same, return early. - if (($status = $format->status()) === $original->status()) { - return; - } - - /** @var \Drupal\editor\EditorInterface $editor */ - if ($editor = Editor::load($format->id())) { - $editor->setStatus($status)->save(); - } -} diff --git a/core/modules/editor/src/Entity/Editor.php b/core/modules/editor/src/Entity/Editor.php index 0100671..fa5d254 100644 --- a/core/modules/editor/src/Entity/Editor.php +++ b/core/modules/editor/src/Entity/Editor.php @@ -185,4 +185,20 @@ public function setImageUploadSettings(array $image_upload_settings) { return $this; } + /** + * {@inheritdoc} + */ + public function onDependencyUpdate(array $dependencies) { + $changed = parent::onDependencyUpdate($dependencies); + + if (!empty($format = $dependencies['config'][$this->getFilterFormat()->getConfigDependencyName()])) { + if (($status = $format->status()) !== $format->original->status()) { + $this->setStatus($status); + $changed = TRUE; + } + } + + return $changed; + } + } diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php index feca9e9..9e8766c 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php @@ -7,6 +7,7 @@ namespace Drupal\Tests\Core\Config\Entity; +use Drupal\Core\Config\ConfigManagerDependencyFinderInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Language\Language; use Drupal\Tests\Core\Plugin\Fixtures\TestConfigurablePlugin; @@ -132,6 +133,8 @@ protected function setUp() { $container->set('language_manager', $this->languageManager); $container->set('cache_tags.invalidator', $this->cacheTagsInvalidator); $container->set('config.typed', $this->typedConfigManager); + $config_manager = $this->getMock(ConfigManagerDependencyFinderInterface::class); + $container->set('config.manager', $config_manager); \Drupal::setContainer($container); $this->entity = $this->getMockForAbstractClass('\Drupal\Core\Config\Entity\ConfigEntityBase', array($values, $this->entityTypeId)); diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php index 508f39b..ecb3788 100644 --- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php @@ -8,6 +8,7 @@ namespace Drupal\Tests\Core\Config\Entity { use Drupal\Core\Cache\Cache; +use Drupal\Core\Config\ConfigManager; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Language\Language; @@ -176,7 +177,9 @@ protected function setUp() { ->method('getDefinition') ->will($this->returnValue(array('mapping' => array('id' => '', 'uuid' => '', 'dependencies' => '')))); - $this->configManager = $this->getMock('Drupal\Core\Config\ConfigManagerInterface'); + $this->configManager = $this->getMockBuilder(ConfigManager::class) + ->disableOriginalConstructor() + ->getMock(); $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager') ->disableOriginalConstructor()