Commit 8cbab149 authored by catch's avatar catch

Issue #2080823 by alexpott, swentel, Wim Leers: Create API to discover content...

Issue #2080823 by alexpott, swentel, Wim Leers: Create API to discover content or config entities' soft dependencies and use this to present a confirm form on module uninstall.
parent 5f61e266
......@@ -69,7 +69,7 @@ services:
factory_method: getActive
config.manager:
class: Drupal\Core\Config\ConfigManager
arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation']
arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage']
config.storage:
class: Drupal\Core\Config\CachedStorage
arguments: ['@config.cachedstorage.storage', '@cache.config']
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ConfigInstaller implements ConfigInstallerInterface {
......@@ -105,32 +106,52 @@ public function installDefaultConfig($type, $name) {
}
if (!empty($config_to_install)) {
// Order the configuration to install in the order of dependencies.
$data = $source_storage->readMultiple($config_to_install);
$dependency_manager = new ConfigDependencyManager();
$sorted_config = $dependency_manager
->setData($data)
->sortAll();
$old_state = $this->configFactory->getOverrideState();
$this->configFactory->setOverrideState(FALSE);
foreach ($config_to_install as $name) {
// Only import new config.
if ($this->activeStorage->exists($name)) {
continue;
}
// Remove configuration that already exists in the active storage.
$sorted_config = array_diff($sorted_config, $this->activeStorage->listAll());
foreach ($sorted_config as $name) {
$new_config = new Config($name, $this->activeStorage, $this->eventDispatcher, $this->typedConfig);
$data = $source_storage->read($name);
if ($data !== FALSE) {
$new_config->setData($data);
if ($data[$name] !== FALSE) {
$new_config->setData($data[$name]);
}
if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
$this->configManager
$entity_storage = $this->configManager
->getEntityManager()
->getStorageController($entity_type)
->getStorageController($entity_type);
// It is possible that secondary writes can occur during configuration
// creation. Updates of such configuration are allowed.
if ($this->activeStorage->exists($name)) {
$id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix());
$entity = $entity_storage->load($id);
foreach ($new_config->get() as $property => $value) {
$entity->set($property, $value);
}
$entity->save();
}
else {
$entity_storage
->create($new_config->get())
->save();
}
}
else {
$new_config->save();
}
}
$this->configFactory->setOverrideState($old_state);
}
// Reset all the static caches and list caches.
$this->configFactory->reset();
}
}
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\TranslationManager;
......@@ -45,6 +46,13 @@ class ConfigManager implements ConfigManagerInterface {
*/
protected $stringTranslation;
/**
* The active configuration storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $activeStorage;
/**
* Creates ConfigManager objects.
*
......@@ -57,11 +65,12 @@ class ConfigManager implements ConfigManagerInterface {
* @param \Drupal\Core\StringTranslation\TranslationManager $string_translation
* The string translation service.
*/
public function __construct(EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, TypedConfigManager $typed_config_manager, TranslationManager $string_translation) {
public function __construct(EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, TypedConfigManager $typed_config_manager, TranslationManager $string_translation, StorageInterface $active_storage) {
$this->entityManager = $entity_manager;
$this->configFactory = $config_factory;
$this->typedConfigManager = $typed_config_manager;
$this->stringTranslation = $string_translation;
$this->activeStorage = $active_storage;
}
/**
......@@ -125,6 +134,17 @@ public function createSnapshot(StorageInterface $source_storage, StorageInterfac
* {@inheritdoc}
*/
public function uninstall($type, $name) {
// Remove all dependent configuration entities.
$dependent_entities = $this->findConfigEntityDependentsAsEntities($type, array($name));
// Reverse the array to that entities are removed in the correct order of
// dependence. For example, this ensures that field instances are removed
// before fields.
foreach (array_reverse($dependent_entities) as $entity) {
$entity->setUninstalling(TRUE);
$entity->delete();
}
$config_names = $this->configFactory->listAll($name . '.');
foreach ($config_names as $config_name) {
$this->configFactory->get($config_name)->delete();
......@@ -137,4 +157,56 @@ public function uninstall($type, $name) {
}
}
/**
* {@inheritdoc}
*/
public function findConfigEntityDependents($type, array $names) {
$dependency_manager = new ConfigDependencyManager();
// This uses the configuration storage directly to avoid blowing the static
// caches in the configuration factory and the configuration entity system.
// Additionally this ensures that configuration entity dependency discovery
// has no dependencies on the config entity classes. Assume data with UUID
// is a config entity. Only configuration entities can be depended on so we
// can ignore everything else.
$data = array_filter($this->activeStorage->readMultiple($this->activeStorage->listAll()), function($config) {
return isset($config['uuid']);
});
$dependency_manager->setData($data);
$dependencies = array();
foreach ($names as $name) {
$dependencies = array_merge($dependencies, $dependency_manager->getDependentEntities($type, $name));
}
return $dependencies;
}
/**
* {@inheritdoc}
*/
public function findConfigEntityDependentsAsEntities($type, array $names) {
$dependencies = $this->findConfigEntityDependents($type, $names);
$entities = array();
$definitions = $this->entityManager->getDefinitions();
foreach ($dependencies as $config_name => $dependency) {
// Group by entity type to efficient load entities using
// \Drupal\Core\Entity\EntityStorageControllerInterface::loadMultiple().
$entity_type_id = $this->getEntityTypeIdByName($config_name);
// It is possible that a non-configuration entity will be returned if a
// simple configuration object has a UUID key. This would occur if the
// dependents of the system module are calculated since system.site has
// a UUID key.
if ($entity_type_id) {
$id = substr($config_name, strlen($definitions[$entity_type_id]->getConfigPrefix()) + 1);
$entities[$entity_type_id][] = $id;
}
}
$entities_to_return = array();
foreach ($entities as $entity_type_id => $entities_to_load) {
$storage_controller = $this->entityManager->getStorageController($entity_type_id);
// Remove the keys since there are potential ID clashes from different
// configuration entity types.
$entities_to_return = array_merge($entities_to_return, array_values($storage_controller->loadMultiple($entities_to_load)));
}
return $entities_to_return;
}
}
......@@ -68,4 +68,35 @@ public function createSnapshot(StorageInterface $source_storage, StorageInterfac
*/
public function uninstall($type, $name);
/**
* Finds config entities that are dependent on extensions or entities.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'entity'.
* @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.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityDependency[]
* An array of configuration entity dependency objects.
*/
public function findConfigEntityDependents($type, array $names);
/**
* Finds config entities that are dependent on extensions or entities.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'entity'.
* @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.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityInterface[]
* An array of dependencies as configuration entities.
*/
public function findConfigEntityDependentsAsEntities($type, array $names);
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigDependencyManager.
*/
namespace Drupal\Core\Config\Entity;
use Drupal\Component\Graph\Graph;
use Drupal\Component\Utility\SortArray;
/**
* Provides a class to discover configuration entity dependencies.
*
* Configuration entities can depend on modules, themes and other configuration
* entities. The dependency system is used during configuration installation to
* ensure that configuration entities are imported in the correct order. For
* example, node types are created before their fields and the fields are
* created before their field instances.
*
* Dependencies are stored to the configuration entity's configuration object so
* that they can be checked without the module that provides the configuration
* entity class being installed. This is important for configuration
* synchronization which needs to be able to validate configuration in the
* staging directory before the synchronization has occurred.
*
* Configuration entities determine their dependencies by implementing
* \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies().
* This method should be called from the configuration entity's implementation
* of \Drupal\Core\Entity\EntityInterface::preSave(). Implementations should use
* the helper method
* \Drupal\Core\Config\Entity\ConfigEntityBase::addDependency() to add
* dependencies. All the implementations in core call the parent method
* \Drupal\Core\Config\Entity\ConfigEntityBase::calculateDependencies() which
* resets the dependencies and provides an implementation to determine the
* plugin providers for configuration entities that implement
* \Drupal\Core\Config\Entity\EntityWithPluginBagInterface.
*
* The configuration manager service provides methods to find dependencies for
* a specified module, theme or configuration entity.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::getConfigDependencyName()
* @see \Drupal\Core\Config\Entity\ConfigEntityBase::addDependency()
* @see \Drupal\Core\Config\ConfigInstaller::installDefaultConfig()
* @see \Drupal\Core\Config\Entity\ConfigEntityDependency
*/
class ConfigDependencyManager {
/**
* The config entity data.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityDependency[]
*/
protected $data = array();
/**
* The directed acyclic graph.
*
* @var array
*/
protected $graph;
/**
* Gets dependencies.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'entity'.
* @param string $name
* The specific name to check. If $type equals 'module' or 'theme' then it
* should be a module name or theme name. In the case of entity it should be
* the full configuration object name.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityDependency[]
* An array of config entity dependency objects that are dependent.
*/
public function getDependentEntities($type, $name) {
$dependent_entities = array();
$entities_to_check = array();
if ($type == 'entity') {
$entities_to_check[] = $name;
}
else {
if ($type == 'module' || $type == 'theme') {
$dependent_entities = array_filter($this->data, function (ConfigEntityDependency $entity) use ($type, $name) {
return $entity->hasDependency($type, $name);
});
}
// If checking module or theme dependencies then discover which entities
// are dependent on the entities that have a direct dependency.
foreach ($dependent_entities as $entity) {
$entities_to_check[] = $entity->getConfigDependencyName();
}
}
return array_merge($dependent_entities, $this->createGraphConfigEntityDependencies($entities_to_check));
}
/**
* Sorts the dependencies in order of most dependent last.
*
* @return array
* The list of entities in order of most dependent last, otherwise
* alphabetical.
*/
public function sortAll() {
$graph = $this->getGraph();
// 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);
}
/**
* Sorts the dependency graph by reverse weight and alphabetically.
*
* @param array $a
* First item for comparison. The compared items should be associative
* arrays that include a 'weight' and a 'component' key.
* @param array $b
* Second item for comparison.
*
* @return int
* The comparison result for uasort().
*/
public function sortGraph(array $a, array $b) {
$weight_cmp = SortArray::sortByKeyInt($a, $b, 'weight') * -1;
if ($weight_cmp === 0) {
return SortArray::sortByKeyString($a, $b, 'component');
}
return $weight_cmp;
}
/**
* Creates a graph of config entity dependencies.
*
* @param array $entities_to_check
* The configuration entity full configuration names to determine the
* dependencies for.
*
* @return \Drupal\Core\Config\Entity\ConfigEntityDependency[]
* A graph of config entity dependency objects that are dependent on the
* supplied entities to check.
*/
protected function createGraphConfigEntityDependencies($entities_to_check) {
$dependent_entities = array();
$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];
}
}
}
return $dependent_entities;
}
/**
* Gets the dependency graph of all the config entities.
*
* @return array
* The dependency graph of all the config entities.
*/
protected function getGraph() {
if (!isset($this->graph)) {
$graph = array();
foreach ($this->data as $entity) {
$graph_key = $entity->getConfigDependencyName();
$graph[$graph_key]['edges'] = array();
$dependencies = $entity->getDependencies('entity');
if (!empty($dependencies)) {
foreach ($dependencies as $dependency) {
$graph[$graph_key]['edges'][$dependency] = TRUE;
}
}
}
$graph_object = new Graph($graph);
$this->graph = $graph_object->searchAndSort();
}
return $this->graph;
}
/**
* Sets data to calculate dependencies for.
*
* The data is converted into lightweight ConfigEntityDependency objects.
*
* @param array $data
* Configuration data keyed by configuration object name. Typically the
* output of \Drupal\Core\Config\StorageInterface::loadMultiple().
*
* @return $this
*/
public function setData(array $data) {
array_walk($data, function (&$config, $name) {
$config = new ConfigEntityDependency($name, $config);
});
$this->data = $data;
$this->graph = NULL;
return $this;
}
}
......@@ -63,6 +63,20 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
*/
private $isSyncing = FALSE;
/**
* Whether the config is being deleted by the uninstall process.
*
* @var bool
*/
private $isUninstalling = FALSE;
/**
* The configuration entity's dependencies.
*
* @var array
*/
protected $dependencies = array();
/**
* Overrides Entity::__construct().
*/
......@@ -170,6 +184,20 @@ public function isSyncing() {
return $this->isSyncing;
}
/**
* {@inheritdoc}
*/
public function setUninstalling($uninstalling) {
$this->isUninstalling = $uninstalling;
}
/**
* {@inheritdoc}
*/
public function isUninstalling() {
return $this->isUninstalling;
}
/**
* {@inheritdoc}
*/
......@@ -207,6 +235,8 @@ public function toArray() {
$name = $property->getName();
$properties[$name] = $this->get($name);
}
// Add protected dependencies property.
$properties['dependencies'] = $this->dependencies;
return $properties;
}
......@@ -221,7 +251,8 @@ public function preSave(EntityStorageControllerInterface $storage_controller) {
if ($this instanceof EntityWithPluginBagInterface) {
// Any changes to the plugin configuration must be saved to the entity's
// copy as well.
$this->set($this->pluginConfigKey, $this->getPluginBag()->getConfiguration());
$plugin_bag = $this->getPluginBag();
$this->set($this->pluginConfigKey, $plugin_bag->getConfiguration());
}
// Ensure this entity's UUID does not exist with a different ID, regardless
......@@ -241,6 +272,33 @@ public function preSave(EntityStorageControllerInterface $storage_controller) {
throw new ConfigDuplicateUUIDException(format_string('Attempt to save a configuration entity %id with UUID %uuid when this entity already exists with UUID %original_uuid', array('%id' => $this->id(), '%uuid' => $this->uuid(), '%original_uuid' => $original->uuid())));
}
}
if (!$this->isSyncing()) {
// Ensure the correct dependencies are present. If the configuration is
// being written during a configuration synchronisation then there is no
// need to recalculate the dependencies.
$this->calculateDependencies();
}
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
// Dependencies should be recalculated on every save. This ensures stale
// dependencies are never saved.
$this->dependencies = array();
// @todo When \Drupal\Core\Config\Entity\EntityWithPluginBagInterface moves
// to a trait, switch to class_uses() instead.
if ($this instanceof EntityWithPluginBagInterface) {
// Configuration entities need to depend on the providers of any plugins
// that they store the configuration for.
$plugin_bag = $this->getPluginBag();
foreach($plugin_bag as $instance) {
$definition = $instance->getPluginDefinition();
$this->addDependency('module', $definition['provider']);
}
}
return $this->dependencies;
}
/**
......@@ -264,4 +322,48 @@ public function url($rel = 'edit-form', $options = array()) {
return parent::url($rel, $options);
}
/**
* Creates a dependency.
*
* @param string $type
* The type of dependency being checked. Either 'module', 'theme', 'entity'.
* @param string $name
* If $type equals 'module' or 'theme' then it should be the name of the
* module or theme. In the case of entity it should be the full
* configuration object name.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::getConfigDependencyName()
*
* @return $this
*/
protected function addDependency($type, $name) {
// A config entity is always dependent on its provider. There is no need to
// explicitly declare the dependency. An explicit dependency on Core, which
// provides some plugins, is also not needed.
// @see \Drupal\Core\Config\Entity\ConfigEntityDependency::hasDependency()
if ($type == 'module' && ($name == $this->getEntityType()->getProvider() || $name == 'Core')) {
return $this;
}
if (empty($this->dependencies[$type])) {
$this->dependencies[$type] = array($name);
if (count($this->dependencies) > 1) {
// Ensure a consistent order of type keys.
ksort($this->dependencies);
}
}
elseif (!in_array($name, $this->dependencies[$type])) {
$this->dependencies[$type][] = $name;
// Ensure a consistent order of dependency names.
sort($this->dependencies[$type], SORT_FLAG_CASE);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getConfigDependencyName() {
return $this->getEntityType()->getConfigPrefix() . '.' . $this->id();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\Entity\ConfigEntityDependency.
*/
namespace Drupal\Core\Config\Entity;
/**
* Provides a value object to discover configuration dependencies.
*
* @see \Drupal\Core\Config\Entity\ConfigDependencyManager
*/
class ConfigEntityDependency {
/**
* The configuration entity's configuration object name.
*
* @var string
*/
protected $name;
/**
* The configuration entity's dependencies.
*
* @var array
*/
protected $dependencies;
/**
* Constructs the configuration entity dependency from the entity values.
*
* @param string $name
* The configuration entity's configuration object name.
* @param array $values
* The configuration entity's values.
*/
public function __construct($name, $values) {
$this->name = $name;
if (isset($values['dependencies'])) {
$this->dependencies = $values['dependencies'];
}
else {
$this->dependencies = array();
}
}
/**
* Gets the configuration entity's dependencies of the supplied type.
*
* @param string $type
* The type of dependency to return. Either 'module', 'theme', 'entity'.
*