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: ...@@ -69,7 +69,7 @@ services:
factory_method: getActive factory_method: getActive
config.manager: config.manager:
class: Drupal\Core\Config\ConfigManager 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: config.storage:
class: Drupal\Core\Config\CachedStorage class: Drupal\Core\Config\CachedStorage
arguments: ['@config.cachedstorage.storage', '@cache.config'] arguments: ['@config.cachedstorage.storage', '@cache.config']
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Unicode;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class ConfigInstaller implements ConfigInstallerInterface { class ConfigInstaller implements ConfigInstallerInterface {
...@@ -105,25 +106,43 @@ public function installDefaultConfig($type, $name) { ...@@ -105,25 +106,43 @@ public function installDefaultConfig($type, $name) {
} }
if (!empty($config_to_install)) { 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(); $old_state = $this->configFactory->getOverrideState();
$this->configFactory->setOverrideState(FALSE); $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); $new_config = new Config($name, $this->activeStorage, $this->eventDispatcher, $this->typedConfig);
$data = $source_storage->read($name); if ($data[$name] !== FALSE) {
if ($data !== FALSE) { $new_config->setData($data[$name]);
$new_config->setData($data);
} }
if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
$this->configManager $entity_storage = $this->configManager
->getEntityManager() ->getEntityManager()
->getStorageController($entity_type) ->getStorageController($entity_type);
->create($new_config->get()) // It is possible that secondary writes can occur during configuration
->save(); // 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 { else {
$new_config->save(); $new_config->save();
...@@ -131,6 +150,8 @@ public function installDefaultConfig($type, $name) { ...@@ -131,6 +150,8 @@ public function installDefaultConfig($type, $name) {
} }
$this->configFactory->setOverrideState($old_state); $this->configFactory->setOverrideState($old_state);
} }
// Reset all the static caches and list caches.
$this->configFactory->reset();
} }
} }
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\TranslationManager; use Drupal\Core\StringTranslation\TranslationManager;
...@@ -45,6 +46,13 @@ class ConfigManager implements ConfigManagerInterface { ...@@ -45,6 +46,13 @@ class ConfigManager implements ConfigManagerInterface {
*/ */
protected $stringTranslation; protected $stringTranslation;
/**
* The active configuration storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $activeStorage;
/** /**
* Creates ConfigManager objects. * Creates ConfigManager objects.
* *
...@@ -57,11 +65,12 @@ class ConfigManager implements ConfigManagerInterface { ...@@ -57,11 +65,12 @@ class ConfigManager implements ConfigManagerInterface {
* @param \Drupal\Core\StringTranslation\TranslationManager $string_translation * @param \Drupal\Core\StringTranslation\TranslationManager $string_translation
* The string translation service. * 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->entityManager = $entity_manager;
$this->configFactory = $config_factory; $this->configFactory = $config_factory;
$this->typedConfigManager = $typed_config_manager; $this->typedConfigManager = $typed_config_manager;
$this->stringTranslation = $string_translation; $this->stringTranslation = $string_translation;
$this->activeStorage = $active_storage;
} }
/** /**
...@@ -125,6 +134,17 @@ public function createSnapshot(StorageInterface $source_storage, StorageInterfac ...@@ -125,6 +134,17 @@ public function createSnapshot(StorageInterface $source_storage, StorageInterfac
* {@inheritdoc} * {@inheritdoc}
*/ */
public function uninstall($type, $name) { 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 . '.'); $config_names = $this->configFactory->listAll($name . '.');
foreach ($config_names as $config_name) { foreach ($config_names as $config_name) {
$this->configFactory->get($config_name)->delete(); $this->configFactory->get($config_name)->delete();
...@@ -137,4 +157,56 @@ public function uninstall($type, $name) { ...@@ -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 ...@@ -68,4 +68,35 @@ public function createSnapshot(StorageInterface $source_storage, StorageInterfac
*/ */
public function uninstall($type, $name); 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 ...@@ -63,6 +63,20 @@ abstract class ConfigEntityBase extends Entity implements ConfigEntityInterface
*/ */
private $isSyncing = FALSE; 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(). * Overrides Entity::__construct().
*/ */
...@@ -170,6 +184,20 @@ public function isSyncing() { ...@@ -170,6 +184,20 @@ public function isSyncing() {
return $this->isSyncing; return $this->isSyncing;
} }
/**
* {@inheritdoc}
*/
public function setUninstalling($uninstalling) {
$this->isUninstalling = $uninstalling;
}
/**
* {@inheritdoc}
*/
public function isUninstalling() {
return $this->isUninstalling;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
...@@ -207,6 +235,8 @@ public function toArray() { ...@@ -207,6 +235,8 @@ public function toArray() {
$name = $property->getName(); $name = $property->getName();
$properties[$name] = $this->get($name); $properties[$name] = $this->get($name);
} }
// Add protected dependencies property.
$properties['dependencies'] = $this->dependencies;
return $properties; return $properties;
} }
...@@ -221,7 +251,8 @@ public function preSave(EntityStorageControllerInterface $storage_controller) { ...@@ -221,7 +251,8 @@ public function preSave(EntityStorageControllerInterface $storage_controller) {
if ($this instanceof EntityWithPluginBagInterface) { if ($this instanceof EntityWithPluginBagInterface) {
// Any changes to the plugin configuration must be saved to the entity's // Any changes to the plugin configuration must be saved to the entity's
// copy as well. // 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 // Ensure this entity's UUID does not exist with a different ID, regardless
...@@ -241,6 +272,33 @@ public function preSave(EntityStorageControllerInterface $storage_controller) { ...@@ -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()))); 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()) { ...@@ -264,4 +322,48 @@ public function url($rel = 'edit-form', $options = array()) {
return parent::url($rel, $options); 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) {