Commit 4e451f65 authored by Dries's avatar Dries
Browse files

Issue #2262861 by alexpott: Add concept of collections to config storages.

parent 809b361f
......@@ -8,12 +8,12 @@
namespace Drupal\Core\Config;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheFactoryInterface;
/**
* Defines the cached storage.
*
* The class gets another storage and a cache backend injected. It reads from
* The class gets another storage and the cache factory injected. It reads from
* the cache and delegates the read to the storage on a cache miss. It also
* handles cache invalidation.
*/
......@@ -26,6 +26,13 @@ class CachedStorage implements StorageInterface, StorageCacheInterface {
*/
protected $storage;
/**
* The cache factory.
*
* @var \Drupal\Core\Cache\CacheFactoryInterface
*/
protected $cacheFactory;
/**
* The instantiated Cache backend.
*
......@@ -45,12 +52,20 @@ class CachedStorage implements StorageInterface, StorageCacheInterface {
*
* @param \Drupal\Core\Config\StorageInterface $storage
* A configuration storage to be cached.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* A cache backend instance to use for caching.
* @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory
* A cache factory used for getting cache backends.
*/
public function __construct(StorageInterface $storage, CacheBackendInterface $cache) {
public function __construct(StorageInterface $storage, CacheFactoryInterface $cache_factory) {
$this->storage = $storage;
$this->cache = $cache;
$this->cacheFactory = $cache_factory;
$collection = $this->getCollectionName();
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
$bin = 'config';
}
else {
$bin = 'config_' . str_replace('.', '_', $collection);
}
$this->cache = $this->cacheFactory->get($bin);
}
/**
......@@ -238,4 +253,29 @@ public function deleteAll($prefix = '') {
public function resetListCache() {
$this->findByPrefixCache = array();
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static(
$this->storage->createCollection($collection),
$this->cacheFactory
);
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
return $this->storage->getAllCollectionNames();
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->storage->getCollectionName();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigCollectionNamesEvent.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a configuration event for event listeners.
*/
class ConfigCollectionNamesEvent extends Event {
/**
* Configuration collection names.
*
* @var array
*/
protected $collections = array();
/**
* Adds names to the list of possible collections.
*
* @param array $collections
* Collection names to add.
*/
public function addCollectionNames(array $collections) {
$this->collections = array_merge($this->collections, $collections);
}
/**
* Adds a name to the list of possible collections.
*
* @param string $collection
* Collection name to add.
*/
public function addCollectionName($collection) {
$this->addCollectionNames(array($collection));
}
/**
* Gets the list of possible collection names.
*
* @return array
* The list of possible collection names.
*/
public function getCollectionNames($include_default = TRUE) {
sort($this->collections);
$collections = array_unique($this->collections);
if ($include_default) {
array_unshift($collections, StorageInterface::DEFAULT_COLLECTION);
}
return $collections;
}
}
......@@ -50,4 +50,11 @@ final class ConfigEvents {
*/
const IMPORT = 'config.importer.import';
/**
* Name of event fired to discover all the possible configuration collections.
*
* @see \Drupal\Core\Config\ConfigInstaller::installDefaultConfig()
*/
const COLLECTION_NAMES = 'config.collection_names';
}
......@@ -185,7 +185,9 @@ public function __construct(StorageComparerInterface $storage_comparer, EventDis
$this->moduleHandler = $module_handler;
$this->themeHandler = $theme_handler;
$this->stringTranslation = $string_translation;
$this->processedConfiguration = $this->storageComparer->getEmptyChangelist();
foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
$this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
}
$this->processedExtensions = $this->getEmptyExtensionsProcessedList();
}
......@@ -227,7 +229,9 @@ public function getStorageComparer() {
*/
public function reset() {
$this->storageComparer->reset();
$this->processedConfiguration = $this->storageComparer->getEmptyChangelist();
foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
$this->processedConfiguration[$collection] = $this->storageComparer->getEmptyChangelist();
}
$this->processedExtensions = $this->getEmptyExtensionsProcessedList();
$this->createExtensionChangelist();
$this->validated = FALSE;
......@@ -257,16 +261,13 @@ protected function getEmptyExtensionsProcessedList() {
/**
* Checks if there are any unprocessed configuration changes.
*
* @param array $ops
* The operations to check for changes. Defaults to all operations, i.e.
* array('delete', 'create', 'update', 'rename').
*
* @return bool
* TRUE if there are changes to process and FALSE if not.
*/
public function hasUnprocessedConfigurationChanges($ops = array('delete', 'create', 'rename', 'update')) {
foreach ($ops as $op) {
if (count($this->getUnprocessedConfiguration($op))) {
public function hasUnprocessedConfigurationChanges() {
foreach ($this->storageComparer->getAllCollectionNames() as $collection)
foreach (array('delete', 'create', 'rename', 'update') as $op) {
if (count($this->getUnprocessedConfiguration($op, $collection))) {
return TRUE;
}
}
......@@ -276,23 +277,29 @@ public function hasUnprocessedConfigurationChanges($ops = array('delete', 'creat
/**
* Gets list of processed changes.
*
* @param string $collection
* (optional) The configuration collection to get processed changes for.
* Defaults to the default collection.
*
* @return array
* An array containing a list of processed changes.
*/
public function getProcessedConfiguration() {
return $this->processedConfiguration;
public function getProcessedConfiguration($collection = StorageInterface::DEFAULT_COLLECTION) {
return $this->processedConfiguration[$collection];
}
/**
* Sets a change as processed.
*
* @param string $collection
* The configuration collection to set a change as processed for.
* @param string $op
* The change operation performed, either delete, create, rename, or update.
* @param string $name
* The name of the configuration processed.
*/
protected function setProcessedConfiguration($op, $name) {
$this->processedConfiguration[$op][] = $name;
protected function setProcessedConfiguration($collection, $op, $name) {
$this->processedConfiguration[$collection][$op][] = $name;
}
/**
......@@ -301,12 +308,15 @@ protected function setProcessedConfiguration($op, $name) {
* @param string $op
* The change operation to get the unprocessed list for, either delete,
* create, rename, or update.
* @param string $collection
* (optional) The configuration collection to get unprocessed changes for.
* Defaults to the default collection.
*
* @return array
* An array of configuration names.
*/
public function getUnprocessedConfiguration($op) {
return array_diff($this->storageComparer->getChangelist($op), $this->processedConfiguration[$op]);
public function getUnprocessedConfiguration($op, $collection = StorageInterface::DEFAULT_COLLECTION) {
return array_diff($this->storageComparer->getChangelist($op, $collection), $this->processedConfiguration[$collection][$op]);
}
/**
......@@ -582,19 +592,29 @@ public function processConfigurations(array &$context) {
// into account.
if ($this->totalConfigurationToProcess == 0) {
$this->storageComparer->reset();
foreach (array('delete', 'create', 'rename', 'update') as $op) {
foreach ($this->getUnprocessedConfiguration($op) as $name) {
$this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op));
foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
foreach (array('delete', 'create', 'rename', 'update') as $op) {
$this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op, $collection));
}
}
}
$operation = $this->getNextConfigurationOperation();
if (!empty($operation)) {
if ($this->checkOp($operation['op'], $operation['name'])) {
$this->processConfiguration($operation['op'], $operation['name']);
if ($this->checkOp($operation['collection'], $operation['op'], $operation['name'])) {
$this->processConfiguration($operation['collection'], $operation['op'], $operation['name']);
}
if ($operation['collection'] == StorageInterface::DEFAULT_COLLECTION) {
$context['message'] = $this->t('Synchronizing configuration: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name']));
}
else {
$context['message'] = $this->t('Synchronizing configuration: @op @name in @collection.', array('@op' => $operation['op'], '@name' => $operation['name'], '@collection' => $operation['collection']));
}
$processed_count = 0;
foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
foreach (array('delete', 'create', 'rename', 'update') as $op) {
$processed_count += count($this->processedConfiguration[$collection][$op]);
}
}
$context['message'] = t('Synchronizing configuration: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name']));
$processed_count = count($this->processedConfiguration['create']) + count($this->processedConfiguration['delete']) + count($this->processedConfiguration['update']);
$context['finished'] = $processed_count / $this->totalConfigurationToProcess;
}
else {
......@@ -658,13 +678,16 @@ protected function getNextExtensionOperation() {
protected function getNextConfigurationOperation() {
// The order configuration operations is processed is important. Deletes
// have to come first so that recreates can work.
foreach (array('delete', 'create', 'rename', 'update') as $op) {
$config_names = $this->getUnprocessedConfiguration($op);
if (!empty($config_names)) {
return array(
'op' => $op,
'name' => array_shift($config_names),
);
foreach ($this->storageComparer->getAllCollectionNames() as $collection) {
foreach (array('delete', 'create', 'rename', 'update') as $op) {
$config_names = $this->getUnprocessedConfiguration($op, $collection);
if (!empty($config_names)) {
return array(
'op' => $op,
'name' => array_shift($config_names),
'collection' => $collection,
);
}
}
}
return FALSE;
......@@ -708,6 +731,8 @@ public function validate() {
/**
* Processes a configuration change.
*
* @param string $collection
* The configuration collection to process changes for.
* @param string $op
* The change operation.
* @param string $name
......@@ -718,17 +743,21 @@ public function validate() {
* set, otherwise the exception message is logged and the configuration
* is skipped.
*/
protected function processConfiguration($op, $name) {
protected function processConfiguration($collection, $op, $name) {
try {
if (!$this->importInvokeOwner($op, $name)) {
$this->importConfig($op, $name);
$processed = FALSE;
if ($this->configManager->supportsConfigurationEntities($collection)) {
$processed = $this->importInvokeOwner($collection, $op, $name);
}
if (!$processed) {
$this->importConfig($collection, $op, $name);
}
}
catch (\Exception $e) {
$this->logError($this->t('Unexpected error during import with operation @op for @name: @message', array('@op' => $op, '@name' => $name, '@message' => $e->getMessage())));
// Error for that operation was logged, mark it as processed so that
// the import can continue.
$this->setProcessedConfiguration($op, $name);
$this->setProcessedConfiguration($collection, $op, $name);
}
}
......@@ -765,7 +794,7 @@ protected function processExtension($type, $op, $name) {
// the default or admin theme is change this will be picked up whilst
// processing configuration.
if ($op == 'disable' && $this->processedSystemTheme === FALSE) {
$this->importConfig('update', 'system.theme');
$this->importConfig(StorageInterface::DEFAULT_COLLECTION, 'update', 'system.theme');
$this->configManager->getConfigFactory()->reset('system.theme');
$this->processedSystemTheme = TRUE;
}
......@@ -785,6 +814,8 @@ protected function processExtension($type, $op, $name) {
* This method checks that the operation is still valid before processing a
* configuration change.
*
* @param string $collection
* The configuration collection.
* @param string $op
* The change operation.
* @param string $name
......@@ -795,10 +826,10 @@ protected function processExtension($type, $op, $name) {
* @return bool
* TRUE is to continue processing, FALSE otherwise.
*/
protected function checkOp($op, $name) {
protected function checkOp($collection, $op, $name) {
if ($op == 'rename') {
$names = $this->storageComparer->extractRenameNames($name);
$target_exists = $this->storageComparer->getTargetStorage()->exists($names['new_name']);
$target_exists = $this->storageComparer->getTargetStorage($collection)->exists($names['new_name']);
if ($target_exists) {
// If the target exists, the rename has already occurred as the
// result of a secondary configuration write. Change the operation
......@@ -810,13 +841,13 @@ protected function checkOp($op, $name) {
}
return TRUE;
}
$target_exists = $this->storageComparer->getTargetStorage()->exists($name);
$target_exists = $this->storageComparer->getTargetStorage($collection)->exists($name);
switch ($op) {
case 'delete':
if (!$target_exists) {
// The configuration has already been deleted. For example, a field
// is automatically deleted if all the instances are.
$this->setProcessedConfiguration($op, $name);
$this->setProcessedConfiguration($collection, $op, $name);
return FALSE;
}
break;
......@@ -833,7 +864,7 @@ protected function checkOp($op, $name) {
$this->logError($this->t('Deleted and replaced configuration entity "@name"', array('@name' => $name)));
}
else {
$this->storageComparer->getTargetStorage()->delete($name);
$this->storageComparer->getTargetStorage($collection)->delete($name);
$this->logError($this->t('Deleted and replaced configuration "@name"', array('@name' => $name)));
}
return TRUE;
......@@ -846,7 +877,7 @@ protected function checkOp($op, $name) {
// Mark as processed so that the synchronisation continues. Once the
// the current synchronisation is complete it will show up as a
// create.
$this->setProcessedConfiguration($op, $name);
$this->setProcessedConfiguration($collection, $op, $name);
return FALSE;
}
break;
......@@ -857,22 +888,24 @@ protected function checkOp($op, $name) {
/**
* Writes a configuration change from the source to the target storage.
*
* @param string $collection
* The configuration collection.
* @param string $op
* The change operation.
* @param string $name
* The name of the configuration to process.
*/
protected function importConfig($op, $name) {
$config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
protected function importConfig($collection, $op, $name) {
$config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
if ($op == 'delete') {
$config->delete();
}
else {
$data = $this->storageComparer->getSourceStorage()->read($name);
$data = $this->storageComparer->getSourceStorage($collection)->read($name);
$config->setData($data ? $data : array());
$config->save();
}
$this->setProcessedConfiguration($op, $name);
$this->setProcessedConfiguration($collection, $op, $name);
}
/**
......@@ -883,6 +916,8 @@ protected function importConfig($op, $name) {
*
* @todo Add support for other extension types; e.g., themes etc.
*
* @param string $collection
* The configuration collection.
* @param string $op
* The change operation to get the unprocessed list for, either delete,
* create, rename, or update.
......@@ -897,21 +932,21 @@ protected function importConfig($op, $name) {
* TRUE if the configuration was imported as a configuration entity. FALSE
* otherwise.
*/
protected function importInvokeOwner($op, $name) {
protected function importInvokeOwner($collection, $op, $name) {
// Renames are handled separately.
if ($op == 'rename') {
return $this->importInvokeRename($name);
return $this->importInvokeRename($collection, $name);
}
// Validate the configuration object name before importing it.
// Config::validateName($name);
if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
$old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($old_data = $this->storageComparer->getTargetStorage()->read($name)) {
$old_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($name)) {
$old_config->initWithData($old_data);
}
$data = $this->storageComparer->getSourceStorage()->read($name);
$new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
$data = $this->storageComparer->getSourceStorage($collection)->read($name);
$new_config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
if ($data !== FALSE) {
$new_config->setData($data);
}
......@@ -924,7 +959,7 @@ protected function importInvokeOwner($op, $name) {
throw new EntityStorageException(String::format('The entity storage "@storage" for the "@entity_type" entity type does not support imports', array('@storage' => get_class($entity_storage), '@entity_type' => $entity_type)));
}
$entity_storage->$method($name, $new_config, $old_config);
$this->setProcessedConfiguration($op, $name);
$this->setProcessedConfiguration($collection, $op, $name);
return TRUE;
}
return FALSE;
......@@ -933,6 +968,8 @@ protected function importInvokeOwner($op, $name) {
/**
* Imports a configuration entity rename.
*
* @param string $collection
* The configuration collection.
* @param string $rename_name
* The rename configuration name, as provided by
* \Drupal\Core\Config\StorageComparer::createRenameName().
......@@ -943,16 +980,16 @@ protected function importInvokeOwner($op, $name) {
*
* @see \Drupal\Core\Config\ConfigImporter::createRenameName()
*/
protected function importInvokeRename($rename_name) {
protected function importInvokeRename($collection, $rename_name) {
$names = $this->storageComparer->extractRenameNames($rename_name);
$entity_type_id = $this->configManager->getEntityTypeIdByName($names['old_name']);
$old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
if ($old_data = $this->storageComparer->getTargetStorage()->read($names['old_name'])) {
$old_config = new Config($names['old_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
if ($old_data = $this->storageComparer->getTargetStorage($collection)->read($names['old_name'])) {
$old_config->initWithData($old_data);
}
$data = $this->storageComparer->getSourceStorage()->read($names['new_name']);
$new_config = new Config($names['new_name'], $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager);
$data = $this->storageComparer->getSourceStorage($collection)->read($names['new_name']);
$new_config = new Config($names['new_name'], $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
if ($data !== FALSE) {
$new_config->setData($data);
}
......@@ -964,7 +1001,7 @@ protected function importInvokeRename($rename_name) {
throw new EntityStorageException(String::format('The entity storage "@storage" for the "@entity_type" entity type does not support imports', array('@storage' => get_class($entity_storage), '@entity_type' => $entity_type_id)));
}
$entity_storage->importRename($names['old_name'], $new_config, $old_config);
$this->setProcessedConfiguration('rename', $rename_name);
$this->setProcessedConfiguration($collection, 'rename', $rename_name);
return TRUE;
}
......
......@@ -88,10 +88,6 @@ public function __construct(ConfigFactoryInterface $config_factory, StorageInter
* {@inheritdoc}
*/
public function installDefaultConfig($type, $name) {
// Get all default configuration owned by this extension.
$source_storage = $this->getSourceStorage();
$config_to_install = $source_storage->listAll($name . '.');
$extension_path = drupal_get_path($type, $name);
// If the extension provides configuration schema clear the definitions.
if (is_dir($extension_path . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY)) {
......@@ -100,22 +96,61 @@ public function installDefaultConfig($type, $name) {
$this->typedConfig->clearCachedDefinitions();
}
// Gather all the supported collection names.
$event = new ConfigCollectionNamesEvent();
$this->eventDispatcher->dispatch(ConfigEvents::COLLECTION_NAMES, $event);
$collections = $event->getCollectionNames();
$old_state = $this->configFactory->getOverrideState();
$this->configFactory->setOverrideState(FALSE);
// Read enabled extensions directly from configuration to avoid circular
// dependencies with ModuleHandler and ThemeHandler.
$extension_config = $this->configFactory->get('core.extension');
$enabled_extensions = array_keys((array) $extension_config->get('module'));
$enabled_extensions += array_keys((array) $extension_config->get('theme'));
foreach ($collections as $collection) {
$config_to_install = $this->listDefaultConfigCollection($collection, $type, $name, $enabled_extensions);
if (!empty($config_to_install)) {
$this->createConfiguration($collection, $config_to_install);
}
}
$this->configFactory->setOverrideState($old_state);
// Reset all the static caches and list caches.
$this->configFactory->reset();
}
/**
* Installs default configuration for a particular collection.
*
* @param string $collection
* The configuration collection to install.
* @param string $type
* The extension type; e.g., 'module' or 'theme'.
* @param string $name
* The name of the module or theme to install default configuration for.
* @param array $enabled_extensions
* A list of all the currently enabled modules and themes.
*
* @return array
* The list of configuration objects to create.
*/
protected function listDefaultConfigCollection($collection, $type, $name, array $enabled_extensions) {
// Get all default configuration owned by this extension.
$source_storage = $this->getSourceStorage($collection);
$config_to_install = $source_storage->listAll($name . '.');
// If not installing the core base system default configuration, work out if
// this extension provides default configuration for any other enabled
// extensions.
$extension_path = drupal_get_path($type, $name);
if ($type !== 'core' && is_dir($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY)) {
$enabled_extensions = $other_module_config = array();
$default_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY);
$default_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection);
$other_module_config = array_filter($default_storage->listAll(), function ($value) use ($name) {
return !preg_match('/^' . $name . '\./', $value);
});
// Read enabled extensions directly from configuration to avoid circular
// dependencies with ModuleHandler and ThemeHandler.
$extension_config = $this->configFactory->get('core.extension');
$enabled_extensions += array_keys((array) $extension_config->get('module'));
$enabled_extensions += array_keys((array) $extension_config->get('theme'));