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..52875bb 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -12,6 +12,7 @@ use Drupal\Core\Config\Entity\ConfigDependencyManager; use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; @@ -21,7 +22,7 @@ /** * The ConfigManager provides helper functions for the configuration system. */ -class ConfigManager implements ConfigManagerInterface { +class ConfigManager implements ConfigManagerInterface, ConfigManagerDependencyFinderInterface { use StringTranslationTrait; /** @@ -297,39 +298,43 @@ 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) { + $names = $this->getNormalizedNames($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 = []; - $return = [ - 'update' => [], - 'delete' => [], - 'unchanged' => [], - ]; + $return = ['update' => [], 'delete' => [], 'unchanged' => []]; // Try to fix any dependencies and find out what will happen to the // dependency graph. foreach ($dependents as $dependent) { - /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent */ + /** @var \Drupal\Core\Config\Entity\ConfigEntityBase $dependent */ if ($dry_run) { // 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; } @@ -338,16 +343,33 @@ public function getConfigEntitiesToChangeOnDependencyRemoval($type, array $names $return['update'][] = $dependent; $update_uuids[] = $dependent->uuid(); } + // Clear the 'dependency_resolved' flag. + $dependent->setDependencyResolved(FALSE); } } - // 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 + // 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']); + // Fix back the $return['update'] list. An entity marked for update can be + // also in $return['delete'] list. The removal take precedence. + $return['update'] = array_filter($return['update'], function(ConfigEntityInterface $entity) use ($delete_uuids) { + return !in_array($entity->uuid(), $delete_uuids); + }); + } + 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 +391,15 @@ 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 onDependencyUpdating(). The methods are called + * with the correct list of affected entities. This list should only contain + * dependencies on the entity. * + * @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 @@ -384,47 +408,29 @@ public function getConfigCollectionInfo() { * 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. + * A list of specific items, keyed by the configuration dependency names. If + * $type equals 'module' or 'theme' then the value is the same as the key. + * In the case of 'config' or 'content' the values are the full dependency + * entities. * * @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 +438,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' : 'onDependencyUpdating'; + $changed = $entity->$method($affected_dependencies); + /** @var \Drupal\Core\Config\Entity\ConfigEntityDependencyResolverInterface $entity */ + $entity->setDependencyResolved($changed); + return $changed; } /** @@ -477,4 +479,52 @@ 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. For entity dependencies, the caller can, alternatively, + * pass the full entity object instead of its dependency name. + * + * @return array + * The normalized list of dependencies, keyed by dependency name. If $type + * equals 'module' or 'theme', the value is the same as the key. In the case + * of 'config' or 'content' it should be the full dependency entity. + */ + protected function getNormalizedNames($type, array $names) { + $normalized = []; + foreach ($names as $key => $value) { + if (is_string($value)) { + // The name has been passed as a string, in the value. + if ($type == 'config') { + $normalized[$value] = $this->loadConfigEntityByName($value); + } + elseif ($type == 'content') { + // Ignore the bundle. + list($entity_type_id, , $uuid) = explode(':', $value); + $normalized[$value] = $this->entityManager->loadEntityByConfigTarget($entity_type_id, $uuid); + } + else { + // Module or theme. The value is the same as the key. + $normalized[$value] = $value; + } + } + elseif ($value instanceof EntityInterface) { + // The name has been passed as a full config or content entity. + /** @var \Drupal\Core\Entity\EntityInterface $value */ + $normalized[$value->getConfigDependencyName()] = $value; + } + } + return $normalized; + } + } diff --git a/core/lib/Drupal/Core/Config/ConfigManagerDependencyFinderInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerDependencyFinderInterface.php new file mode 100644 index 0000000..f6f49f2 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigManagerDependencyFinderInterface.php @@ -0,0 +1,51 @@ +uuid() != $this->uuid())) { throw new ConfigDuplicateUUIDException("Attempt to save a configuration entity '{$this->id()}' with UUID '{$this->uuid()}' when this entity already exists with UUID '{$original->uuid()}'"); } + + // Fix any dependencies. + $config_entities = $this->getConfigManager()->getConfigEntitiesToChange(ConfigManagerInterface::UPDATE, 'config', [$this], FALSE); + /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $dependent_entity */ + foreach ($config_entities['update'] as $dependent_entity) { + $dependent_entity->save(); + } } if (!$this->isSyncing()) { // Ensure the correct dependencies are present. If the configuration is @@ -498,6 +508,13 @@ public function onDependencyRemoval(array $dependencies) { /** * {@inheritdoc} + */ + public function onDependencyUpdating(array $dependencies) { + return FALSE; + } + + /** + * {@inheritdoc} * * Override to never invalidate the entity's cache tag; the config system * already invalidates it. @@ -628,4 +645,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..45a7bef --- /dev/null +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityDependencyResolverInterface.php @@ -0,0 +1,85 @@ +targetConfigId)) { + $storage = $this->entityTypeManager()->getStorage('config_test'); + if ($target = $storage->load($this->targetConfigId)) { + $this->addDependency($target->getConfigDependencyKey(), $target->getConfigDependencyName()); + } + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function onDependencyUpdating(array $dependencies) { + $changed = parent::onDependencyUpdating($dependencies); + + if (!empty($this->targetConfigId)) { + $storage = $this->entityTypeManager()->getStorage('config_test'); + if ($target = $storage->load($this->targetConfigId)) { + if (!empty($dependency = $dependencies[$target->getConfigDependencyKey()][$target->getConfigDependencyName()])) { + $this->set('label', "Config of '{$dependency->label()}'"); + $changed = TRUE; + } + } + } + + return $changed; + } + +} diff --git a/core/modules/config/tests/src/Kernel/ConfigDependencyTest.php b/core/modules/config/tests/src/Kernel/ConfigDependencyTest.php new file mode 100644 index 0000000..b9ebf9a --- /dev/null +++ b/core/modules/config/tests/src/Kernel/ConfigDependencyTest.php @@ -0,0 +1,60 @@ +container->get('entity_type.manager')->getStorage('config_test'); + + /** @var \Drupal\config_test\Entity\DependentConfigTest $dependency */ + $dependency = $storage->create(['id' => 'dependency', 'label' => 'Dependency']); + $dependency->save(); + + // Create a new 'dependent_config_test' config entity that has $dependency + // entity as dependency. + $config = DependentConfigTest::create([ + 'id' => 'config', + // The $config entity label reflects the dependency entity label. + 'label' => "Config of '{$dependency->label()}'", + 'targetConfigId' => 'dependency', + ]); + $config->save(); + + // Modify the dependency by setting a new label. + $dependency->set('label', 'Overridden')->save(); + + // Check that $config has automatically changed its label. + $this->assertSame("Config of 'Overridden'", DependentConfigTest::load('config')->label()); + + // Delete the dependency. + $dependency->delete(); + + // Check that $config has been deleted too. + $this->assertNull(DependentConfigTest::load('config')); + } + +} diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 8468caa..b2a62e4 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -517,30 +517,3 @@ function _editor_parse_file_uuids($text) { } return $uuids; } - -/** - * Implements hook_ENTITY_TYPE_presave(). - * - * Synchronizes the editor status to its paired text format status. - */ -function editor_filter_format_presave(FilterFormatInterface $format) { - // The text format being created cannot have a text editor yet. - if ($format->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..4777280 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 onDependencyUpdating(array $dependencies) { + $changed = parent::onDependencyUpdating($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..44b2d62 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,10 @@ 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); + $config_manager->method('getConfigEntitiesToChange') + ->will($this->returnValue(['update' => []]));; + $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..5a7df26 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,11 @@ 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->configManager->method('getConfigEntitiesToChange') + ->will($this->returnValue(['update' => []])); $this->cacheContextsManager = $this->getMockBuilder('Drupal\Core\Cache\Context\CacheContextsManager') ->disableOriginalConstructor()