diff --git a/core/lib/Drupal/Core/Config/CachedStorage.php b/core/lib/Drupal/Core/Config/CachedStorage.php index a5d11fd..22ca9aa 100644 --- a/core/lib/Drupal/Core/Config/CachedStorage.php +++ b/core/lib/Drupal/Core/Config/CachedStorage.php @@ -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. */ @@ -27,6 +27,13 @@ class CachedStorage implements StorageInterface, StorageCacheInterface { protected $storage; /** + * The cache factory. + * + * @var \Drupal\Core\Cache\CacheFactoryInterface + */ + protected $cacheFactory; + + /** * The instantiated Cache backend. * * @var \Drupal\Core\Cache\CacheBackendInterface @@ -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 (empty($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(); + } + } diff --git a/core/lib/Drupal/Core/Config/ConfigFactory.php b/core/lib/Drupal/Core/Config/ConfigFactory.php index 5d3756e..f4e44d1 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactory.php +++ b/core/lib/Drupal/Core/Config/ConfigFactory.php @@ -312,4 +312,11 @@ public function addOverride(ConfigFactoryOverrideInterface $config_factory_overr $this->configFactoryOverrides[] = $config_factory_override; } + /** + * {@inheritdoc} + */ + public function getOverrides() { + return $this->configFactoryOverrides; + } + } diff --git a/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php b/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php index 55eb6f6..cafd3fd 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php @@ -134,4 +134,11 @@ public function listAll($prefix = ''); */ public function addOverride(ConfigFactoryOverrideInterface $config_factory_override); + /** + * Get all added configuration factory override instances. + * + * @return ConfigFactoryOverrideInterface[] + * The configuration factory override instances. + */ + public function getOverrides(); } diff --git a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php index 06e1f3f..96c8d48 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php @@ -32,4 +32,24 @@ public function loadOverrides($names); */ public function getCacheSuffix(); + /** + * Reacts to default configuration installation during extension install. + * + * @param string $type + * The type of extension being installed. Either 'module' or 'theme'. + * @param string $name + * The name of the extension. + */ + public function install($type, $name); + + /** + * Reacts to configuration removal during extension uninstallation. + * + * @param string $type + * The type of extension being uninstalled. Either 'module' or 'theme'. + * @param string $name + * The name of the extension. + */ + public function uninstall($type, $name); + } diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 6b36913..bc73819 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -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->storageComparer->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; } diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index a22907b..6a78cf2 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -87,25 +87,18 @@ public function __construct(ConfigFactoryInterface $config_factory, StorageInter /** * {@inheritdoc} */ - public function installDefaultConfig($type, $name) { + public function getConfigToInstall($type, $name, $collection = StorageInterface::DEFAULT_COLLECTION) { // Get all default configuration owned by this extension. - $source_storage = $this->getSourceStorage(); + $source_storage = $this->getSourceStorage($collection); $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)) { - // Refresh the schema cache if installing default configuration and the - // extension has a configuration schema directory. - $this->typedConfig->clearCachedDefinitions(); - } - // If not installing the core base system default configuration, work out if // this extension provides default configuration for any other enabled // extensions. 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); }); @@ -123,10 +116,26 @@ public function installDefaultConfig($type, $name) { $config_to_install = array_merge($config_to_install, $other_module_config); } + return $config_to_install; + } + + /** + * {@inheritdoc} + */ + public function installDefaultConfig($type, $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)) { + // Refresh the schema cache if installing default configuration and the + // extension has a configuration schema directory. + $this->typedConfig->clearCachedDefinitions(); + } + + $config_to_install = $this->getConfigToInstall($type, $name); if (!empty($config_to_install)) { // Order the configuration to install in the order of dependencies. - $data = $source_storage->readMultiple($config_to_install); + $data = $this->getSourceStorage()->readMultiple($config_to_install); $dependency_manager = new ConfigDependencyManager(); $sorted_config = $dependency_manager ->setData($data) @@ -138,12 +147,12 @@ public function installDefaultConfig($type, $name) { // 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); - if ($data[$name] !== FALSE) { - $new_config->setData($data[$name]); + foreach ($sorted_config as $config_name) { + $new_config = new Config($config_name, $this->activeStorage, $this->eventDispatcher, $this->typedConfig); + if ($data[$config_name] !== FALSE) { + $new_config->setData($data[$config_name]); } - if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { + if ($entity_type = $this->configManager->getEntityTypeIdByName($config_name)) { // If we are syncing do not create configuration entities. Pluggable // configuration entities can have dependencies on modules that are @@ -159,8 +168,8 @@ public function installDefaultConfig($type, $name) { ->getStorage($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()); + if ($this->activeStorage->exists($config_name)) { + $id = $entity_storage->getIDFromConfigName($config_name, $entity_storage->getEntityType()->getConfigPrefix()); $entity = $entity_storage->load($id); foreach ($new_config->get() as $property => $value) { $entity->set($property, $value); @@ -179,6 +188,12 @@ public function installDefaultConfig($type, $name) { } $this->configFactory->setOverrideState($old_state); } + + // Allow configuration factory overrides to respond to installation. + foreach ($this->configFactory->getOverrides() as $config_override) { + $config_override->install($type, $name); + } + // Reset all the static caches and list caches. $this->configFactory->reset(); } @@ -200,16 +215,16 @@ public function resetSourceStorage() { } /** - * Gets the configuration storage that provides the default configuration. - * - * @return \Drupal\Core\Config\StorageInterface - * The configuration storage that provides the default configuration. + * {@inheritdoc} */ - public function getSourceStorage() { + public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION) { if (!isset($this->sourceStorage)) { // Default to using the ExtensionInstallStorage which searches extension's // config directories for default configuration. - $this->sourceStorage = new ExtensionInstallStorage($this->activeStorage); + $this->sourceStorage = new ExtensionInstallStorage($this->activeStorage, ExtensionInstallStorage::CONFIG_INSTALL_DIRECTORY, $collection); + } + if ($this->sourceStorage->getCollectionName() != $collection) { + $this->sourceStorage = $this->sourceStorage->createCollection($collection); } return $this->sourceStorage; } diff --git a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php index efa3d54..93d331f 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigInstallerInterface.php @@ -13,6 +13,28 @@ interface ConfigInstallerInterface { /** + * Gets a list of configuration object names to install. + * + * It searches all the default configuration directories for all installed + * extensions to locate any configuration with its name prefix. Additionally, + * the default configuration directory for the extension being installed is + * searched to discover if it contains default configuration that is owned by + * other enabled extensions. + * + * @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 string $collection + * (optional) The configuration collection. Defaults to the default + * collection. + * + * @return array + * The list of configuration object names to install. + */ + public function getConfigToInstall($type, $name, $collection = StorageInterface::DEFAULT_COLLECTION); + + /** * Installs the default configuration of a given extension. * * When an extension is installed, it searches all the default configuration @@ -48,6 +70,17 @@ public function installDefaultConfig($type, $name); public function setSourceStorage(StorageInterface $storage); /** + * Gets the configuration storage that provides the default configuration. + * + * @param string $collection + * The configuration collection. + * + * @return \Drupal\Core\Config\StorageInterface + * The configuration storage that provides the default configuration. + */ + public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION); + + /** * Resets the configuration storage that provides the default configuration. * * @return self diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index 6faa23e..e6e9d85 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -100,7 +100,11 @@ public function getConfigFactory() { /** * {@inheritdoc} */ - public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL) { + public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) { + if ($collection != StorageInterface::DEFAULT_COLLECTION) { + $source_storage = $source_storage->createCollection($collection); + $target_storage = $target_storage->createCollection($collection); + } if (!isset($target_name)) { $target_name = $source_name; } @@ -156,6 +160,11 @@ public function uninstall($type, $name) { foreach ($config_names as $config_name) { $this->configFactory->get($config_name)->delete(); } + // Allow configuration factory overrides to respond to uninstallation. + foreach ($this->configFactory->getOverrides() as $config_override) { + $config_override->uninstall($type, $name); + } + $schema_dir = drupal_get_path($type, $name) . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY; if (is_dir($schema_dir)) { // Refresh the schema cache if uninstalling an extension that provides diff --git a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php index 1d35637..a28a23d 100644 --- a/core/lib/Drupal/Core/Config/ConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php @@ -51,13 +51,16 @@ public function getConfigFactory(); * @param string $target_name * (optional) The name of the configuration object in the target storage. * If omitted, the source name is used. + * @param string $collection + * (optional) The configuration collection name. Defaults to the default + * collection. * * @return core/lib/Drupal/Component/Diff * A formatted string showing the difference between the two storages. * * @todo Make renderer injectable */ - public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL); + public function diff(StorageInterface $source_storage, StorageInterface $target_storage, $source_name, $target_name = NULL, $collection = StorageInterface::DEFAULT_COLLECTION); /** * Creates a configuration snapshot following a successful import. diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 11ab691..55c4373 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -38,6 +38,13 @@ class DatabaseStorage implements StorageInterface { protected $options = array(); /** + * The storage collection. + * + * @var string + */ + protected $collection = ''; + + /** * Constructs a new DatabaseStorage. * * @param \Drupal\Core\Database\Connection $connection @@ -46,11 +53,14 @@ class DatabaseStorage implements StorageInterface { * A database table name to store configuration data in. * @param array $options * (optional) Any additional database connection options to use in queries. + * @param string $collection + * (optional) The collection to store configuration in. */ - public function __construct(Connection $connection, $table, array $options = array()) { + public function __construct(Connection $connection, $table, array $options = array(), $collection = '') { $this->connection = $connection; $this->table = $table; $this->options = $options; + $this->collection = $collection; } /** @@ -58,7 +68,8 @@ public function __construct(Connection $connection, $table, array $options = arr */ public function exists($name) { try { - return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', 0, 1, array( + return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name = :name', 0, 1, array( + ':collection' => $this->collection, ':name' => $name, ), $this->options)->fetchField(); } @@ -75,7 +86,7 @@ public function exists($name) { public function read($name) { $data = FALSE; try { - $raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', array(':name' => $name), $this->options)->fetchField(); + $raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name = :name', array(':collection' => $this->collection, ':name' => $name), $this->options)->fetchField(); if ($raw !== FALSE) { $data = $this->decode($raw); } @@ -93,7 +104,7 @@ public function read($name) { public function readMultiple(array $names) { $list = array(); try { - $list = $this->connection->query('SELECT name, data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name IN (:names)', array(':names' => $names), $this->options)->fetchAllKeyed(); + $list = $this->connection->query('SELECT name, data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name IN (:names)', array(':collection' => $this->collection, ':names' => $names), $this->options)->fetchAllKeyed(); foreach ($list as &$data) { $data = $this->decode($data); } @@ -136,7 +147,7 @@ public function write($name, array $data) { protected function doWrite($name, $data) { $options = array('return' => Database::RETURN_AFFECTED) + $this->options; return (bool) $this->connection->merge($this->table, $options) - ->key('name', $name) + ->keys(array('collection', 'name'), array($this->collection, $name)) ->fields(array('data' => $data)) ->execute(); } @@ -176,8 +187,15 @@ protected static function schemaDefinition() { $schema = array( 'description' => 'The base table for configuration data.', 'fields' => array( + 'collection' => array( + 'description' => 'Primary Key: Config object collection.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), 'name' => array( - 'description' => 'Primary Key: Unique config object name.', + 'description' => 'Primary Key: Config object name.', 'type' => 'varchar', 'length' => 255, 'not null' => TRUE, @@ -190,7 +208,7 @@ protected static function schemaDefinition() { 'size' => 'big', ), ), - 'primary key' => array('name'), + 'primary key' => array('collection', 'name'), ); return $schema; } @@ -205,6 +223,7 @@ protected static function schemaDefinition() { public function delete($name) { $options = array('return' => Database::RETURN_AFFECTED) + $this->options; return (bool) $this->connection->delete($this->table, $options) + ->condition('collection', $this->collection) ->condition('name', $name) ->execute(); } @@ -220,6 +239,7 @@ public function rename($name, $new_name) { return (bool) $this->connection->update($this->table, $options) ->fields(array('name' => $new_name)) ->condition('name', $name) + ->condition('collection', $this->collection) ->execute(); } @@ -246,7 +266,8 @@ public function decode($raw) { */ public function listAll($prefix = '') { try { - return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name', array( + return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection = :collection AND name LIKE :name', array( + ':collection' => $this->collection, ':name' => $this->connection->escapeLike($prefix) . '%', ), $this->options)->fetchCol(); } @@ -263,10 +284,39 @@ public function deleteAll($prefix = '') { $options = array('return' => Database::RETURN_AFFECTED) + $this->options; return (bool) $this->connection->delete($this->table, $options) ->condition('name', $prefix . '%', 'LIKE') + ->condition('collection', $this->collection) ->execute(); } catch (\Exception $e) { return FALSE; } } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + return new static( + $this->connection, + $this->table, + $this->options, + $collection + ); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName() { + return $this->collection; + } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames() { + return $this->connection->query('SELECT DISTINCT collection FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection <> \'\' ORDER by collection')->fetchCol(); + } + + } diff --git a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php index 813efeb..5d771c7 100644 --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -32,9 +32,10 @@ class ExtensionInstallStorage extends InstallStorage { * The directory to scan in each extension to scan for files. Defaults to * 'config'. */ - public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY) { + public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = self::DEFAULT_COLLECTION) { $this->configStorage = $config_storage; $this->directory = $directory; + $this->collection = $collection; } /** @@ -73,5 +74,17 @@ protected function getAllFolders() { } return $this->folders; } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + return new static( + $this->configStorage, + $this->directory, + $collection + ); + } + } diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index 1dac0f4..9f4b7ac 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -17,6 +17,13 @@ class FileStorage implements StorageInterface { /** + * The storage collection. + * + * @var string + */ + protected $collection; + + /** * The filesystem path for configuration objects. * * @var string @@ -28,9 +35,12 @@ class FileStorage implements StorageInterface { * * @param string $directory * A directory path to use for reading and writing of configuration files. + * @param string $collection + * (optional) The collection to store configuration in. */ - public function __construct($directory) { + public function __construct($directory, $collection = '') { $this->directory = $directory; + $this->collection = $collection; } /** @@ -40,7 +50,7 @@ public function __construct($directory) { * The path to the configuration file. */ public function getFilePath($name) { - return $this->directory . '/' . $name . '.' . static::getFileExtension(); + return $this->getCollectionDirectory() . '/' . $name . '.' . static::getFileExtension(); } /** @@ -57,10 +67,14 @@ public static function getFileExtension() { * Check if the directory exists and create it if not. */ protected function ensureStorage() { - $success = file_prepare_directory($this->directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); - $success = $success && file_save_htaccess($this->directory, TRUE, TRUE); + $dir = $this->getCollectionDirectory(); + $success = file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + // Only create .htaccess file in root directory. + if ($dir == $this->directory) { + $success = $success && file_save_htaccess($this->directory, TRUE, TRUE); + } if (!$success) { - throw new StorageException("Failed to create config directory {$this->directory}"); + throw new StorageException('Failed to create config directory ' . $dir); } return $this; } @@ -142,12 +156,22 @@ public function write($name, array $data) { */ public function delete($name) { if (!$this->exists($name)) { - if (!file_exists($this->directory)) { - throw new StorageException($this->directory . '/ not found.'); + $dir = $this->getCollectionDirectory(); + if (!file_exists($dir)) { + throw new StorageException($dir . '/ not found.'); } return FALSE; } - return drupal_unlink($this->getFilePath($name)); + $success = drupal_unlink($this->getFilePath($name)); + + // If a collection is now empty remove the directory. + if ($success && !empty($this->collection)) { + $names = $this->listAll(); + if (empty($names)) { + drupal_rmdir($this->getCollectionDirectory()); + } + } + return $success; } /** @@ -186,12 +210,13 @@ public function decode($raw) { public function listAll($prefix = '') { // glob() silently ignores the error of a non-existing search directory, // even with the GLOB_ERR flag. - if (!file_exists($this->directory)) { + $dir = $this->getCollectionDirectory(); + if (!file_exists($dir)) { return array(); } $extension = '.' . static::getFileExtension(); // \GlobIterator on Windows requires an absolute path. - $files = new \GlobIterator(realpath($this->directory) . '/' . $prefix . '*' . $extension); + $files = new \GlobIterator(realpath($dir) . '/' . $prefix . '*' . $extension); $names = array(); foreach ($files as $file) { @@ -215,4 +240,99 @@ public function deleteAll($prefix = '') { return $success; } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + return new static( + $this->directory, + $collection + ); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName() { + return $this->collection; + } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames($directory = '') { + $collections = $this->getAllCollectionNamesHelper($this->directory); + sort($collections); + return $collections; + } + + /** + * Helper function for getAllCollectionNames(). + * + * If the file storage has the following subdirectory structure: + * ./another_collection/one + * ./another_collection/two + * ./collection/sub/one + * ./collection/sub/two + * this function will return: + * @code + * array( + * 'another_collection.one', + * 'another_collection.two', + * 'collection.sub.one', + * 'collection.sub.two', + * ); + * @endcode + * + * @param string $directory + * The directory to check for sub directories. This allows this + * function to be used recursively to discover all the collections in the + * storage. + * + * @return array + * A list of collection names contained within the provided directory. + */ + protected function getAllCollectionNamesHelper($directory) { + $collections = array(); + foreach (new \DirectoryIterator($directory) as $fileinfo) { + if ($fileinfo->isDir() && !$fileinfo->isDot()) { + $collection = $fileinfo->getFilename(); + // Recursively call getAllCollectionNamesHelper() to discover if there + // are subdirectories. Subdirectories represent a dotted collection + // name. + $sub_collections = $this->getAllCollectionNamesHelper($directory . '/' . $collection); + if (!empty($sub_collections)) { + // Build up the collection name by concatenating the subdirectory + // names with the current directory name. + foreach ($sub_collections as $sub_collection) { + $collections[] = $collection . '.' . $sub_collection; + } + } + else { + // The directory has no subdirectories. Therefore add each directory + // to list of collections to be returned by the helper. + $collections[] = $collection; + } + } + } + return $collections; + } + + /** + * Gets the directory for the collection. + * + * @return string + * The directory for the collection. + */ + protected function getCollectionDirectory() { + if (empty($this->collection)) { + $dir = $this->directory; + } + else { + $dir = $this->directory . '/' . str_replace('.', '/', $this->collection); + } + return $dir; + } + } diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index 2db0944..028fa9c 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -53,8 +53,9 @@ class InstallStorage extends FileStorage { * The directory to scan in each extension to scan for files. Defaults to * 'config'. */ - public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY) { + public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = self::DEFAULT_COLLECTION) { $this->directory = $directory; + $this->collection = $collection; } /** @@ -198,7 +199,7 @@ public function getComponentNames($type, array $list) { * The configuration folder name for this component. */ protected function getComponentFolder($type, $name) { - return drupal_get_path($type, $name) . '/' . $this->directory; + return drupal_get_path($type, $name) . '/' . $this->getCollectionDirectory(); } /** diff --git a/core/lib/Drupal/Core/Config/NullStorage.php b/core/lib/Drupal/Core/Config/NullStorage.php index c66f718..5995da7 100644 --- a/core/lib/Drupal/Core/Config/NullStorage.php +++ b/core/lib/Drupal/Core/Config/NullStorage.php @@ -92,4 +92,26 @@ public function listAll($prefix = '') { public function deleteAll($prefix = '') { return FALSE; } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + // No op. + } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames() { + return array(); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName() { + return ''; + } + } diff --git a/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php index 8a6b9f1..0353206 100644 --- a/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -23,6 +23,13 @@ class StorageComparer implements StorageComparerInterface { protected $sourceStorage; /** + * The source storages keyed by collection. + * + * @var \Drupal\Core\Config\StorageInterface[] + */ + protected $sourceStorages; + + /** * The target storage used to write configuration changes. * * @var \Drupal\Core\Config\StorageInterface @@ -30,8 +37,17 @@ class StorageComparer implements StorageComparerInterface { protected $targetStorage; /** + * The target storages keyed by collection. + * + * @var \Drupal\Core\Config\StorageInterface[] + */ + protected $targetStorages; + + /** * List of changes to between the source storage and the target storage. * + * The list is keyed by storage collection name. + * * @var array */ protected $changelist; @@ -39,6 +55,8 @@ class StorageComparer implements StorageComparerInterface { /** * Sorted list of all the configuration object names in the source storage. * + * The list is keyed by storage collection name. + * * @var array */ protected $sourceNames = array(); @@ -46,6 +64,8 @@ class StorageComparer implements StorageComparerInterface { /** * Sorted list of all the configuration object names in the target storage. * + * The list is keyed by storage collection name. + * * @var array */ protected $targetNames = array(); @@ -53,6 +73,8 @@ class StorageComparer implements StorageComparerInterface { /** * The source configuration data keyed by name. * + * The data is keyed by storage collection name. + * * @var array */ protected $sourceData = array(); @@ -60,6 +82,8 @@ class StorageComparer implements StorageComparerInterface { /** * The target configuration data keyed by name. * + * The data is keyed by storage collection name. + * * @var array */ protected $targetData = array(); @@ -75,21 +99,37 @@ class StorageComparer implements StorageComparerInterface { public function __construct(StorageInterface $source_storage, StorageInterface $target_storage) { $this->sourceStorage = $source_storage; $this->targetStorage = $target_storage; - $this->changelist = $this->getEmptyChangelist(); + $this->changelist[StorageInterface::DEFAULT_COLLECTION] = $this->getEmptyChangelist(); } /** * {@inheritdoc} */ - public function getSourceStorage() { - return $this->sourceStorage; + public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION) { + if (!isset($this->sourceStorages[$collection])) { + if ($collection == StorageInterface::DEFAULT_COLLECTION) { + $this->sourceStorages[$collection] = $this->sourceStorage; + } + else { + $this->sourceStorages[$collection] = $this->sourceStorage->createCollection($collection); + } + } + return $this->sourceStorages[$collection]; } /** * {@inheritdoc} */ - public function getTargetStorage() { - return $this->targetStorage; + public function getTargetStorage($collection = StorageInterface::DEFAULT_COLLECTION) { + if (!isset($this->targetStorages[$collection])) { + if ($collection == StorageInterface::DEFAULT_COLLECTION) { + $this->targetStorages[$collection] = $this->targetStorage; + } + else { + $this->targetStorages[$collection] = $this->targetStorage->createCollection($collection); + } + } + return $this->targetStorages[$collection]; } /** @@ -107,16 +147,18 @@ public function getEmptyChangelist() { /** * {@inheritdoc} */ - public function getChangelist($op = NULL) { + public function getChangelist($op = NULL, $collection = StorageInterface::DEFAULT_COLLECTION) { if ($op) { - return $this->changelist[$op]; + return $this->changelist[$collection][$op]; } - return $this->changelist; + return $this->changelist[$collection]; } /** * Adds changes to the changelist. * + * @param string $collection + * The storage collection to add changes for. * @param string $op * The change operation performed. Either delete, create, rename, or update. * @param array $changes @@ -125,16 +167,16 @@ public function getChangelist($op = NULL) { * Array to sort that can be used to sort the changelist. This array must * contain all the items that are in the change list. */ - protected function addChangeList($op, array $changes, array $sort_order = NULL) { + protected function addChangeList($collection, $op, array $changes, array $sort_order = NULL) { // Only add changes that aren't already listed. - $changes = array_diff($changes, $this->changelist[$op]); - $this->changelist[$op] = array_merge($this->changelist[$op], $changes); + $changes = array_diff($changes, $this->changelist[$collection][$op]); + $this->changelist[$collection][$op] = array_merge($this->changelist[$collection][$op], $changes); if (isset($sort_order)) { - $count = count($this->changelist[$op]); + $count = count($this->changelist[$collection][$op]); // Sort the changlist in the same order as the $sort_order array and // ensure the array is keyed from 0. - $this->changelist[$op] = array_values(array_intersect($sort_order, $this->changelist[$op])); - if ($count != count($this->changelist[$op])) { + $this->changelist[$collection][$op] = array_values(array_intersect($sort_order, $this->changelist[$collection][$op])); + if ($count != count($this->changelist[$collection][$op])) { throw new \InvalidArgumentException(String::format('Sorting the @op changelist should not change its length.', array('@op' => $op))); } } @@ -144,13 +186,20 @@ protected function addChangeList($op, array $changes, array $sort_order = NULL) * {@inheritdoc} */ public function createChangelist() { - $this->getAndSortConfigData(); - $this->addChangelistCreate(); - $this->addChangelistUpdate(); - $this->addChangelistDelete(); - $this->addChangelistRename(); - $this->sourceData = NULL; - $this->targetData = NULL; + foreach ($this->getAllCollectionNames() as $collection) { + $this->changelist[$collection] = $this->getEmptyChangelist(); + $this->getAndSortConfigData($collection); + $this->addChangelistCreate($collection); + $this->addChangelistUpdate($collection); + $this->addChangelistDelete($collection); + // Only collections that support configuration entities can have renames. + if ($this->supportsConfigurationEntities($collection)) { + $this->addChangelistRename($collection); + } + // Only need data whilst calculating changelists. Free up the memory. + $this->sourceData = NULL; + $this->targetData = NULL; + } return $this; } @@ -160,10 +209,13 @@ public function createChangelist() { * The list of deletes is sorted so that dependencies are deleted after * configuration entities that depend on them. For example, field instances * should be deleted after fields. + * + * @param string $collection + * The storage collection to operate on. */ - protected function addChangelistDelete() { - $deletes = array_diff(array_reverse($this->targetNames), $this->sourceNames); - $this->addChangeList('delete', $deletes); + protected function addChangelistDelete($collection) { + $deletes = array_diff(array_reverse($this->targetNames[$collection]), $this->sourceNames[$collection]); + $this->addChangeList($collection, 'delete', $deletes); } /** @@ -172,10 +224,13 @@ protected function addChangelistDelete() { * The list of creates is sorted so that dependencies are created before * configuration entities that depend on them. For example, fields * should be created before field instances. + * + * @param string $collection + * The storage collection to operate on. */ - protected function addChangelistCreate() { - $creates = array_diff($this->sourceNames, $this->targetNames); - $this->addChangeList('create', $creates); + protected function addChangelistCreate($collection) { + $creates = array_diff($this->sourceNames[$collection], $this->targetNames[$collection]); + $this->addChangeList($collection, 'create', $creates); } /** @@ -184,19 +239,22 @@ protected function addChangelistCreate() { * The list of updates is sorted so that dependencies are created before * configuration entities that depend on them. For example, fields * should be updated before field instances. + * + * @param string $collection + * The storage collection to operate on. */ - protected function addChangelistUpdate() { + protected function addChangelistUpdate($collection) { $recreates = array(); - foreach (array_intersect($this->sourceNames, $this->targetNames) as $name) { - if ($this->sourceData[$name] !== $this->targetData[$name]) { - if (isset($this->sourceData[$name]['uuid']) && $this->sourceData[$name]['uuid'] != $this->targetData[$name]['uuid']) { + foreach (array_intersect($this->sourceNames[$collection], $this->targetNames[$collection]) as $name) { + if ($this->sourceData[$collection][$name] !== $this->targetData[$collection][$name]) { + if (isset($this->sourceData[$collection][$name]['uuid']) && $this->sourceData[$collection][$name]['uuid'] != $this->targetData[$collection][$name]['uuid']) { // The entity has the same file as an existing entity but the UUIDs do // not match. This means that the entity has been recreated so config // synchronisation should do the same. $recreates[] = $name; } else { - $this->addChangeList('update', array($name)); + $this->addChangeList($collection, 'update', array($name)); } } } @@ -204,8 +262,8 @@ protected function addChangelistUpdate() { if (!empty($recreates)) { // Recreates should become deletes and creates. Deletes should be ordered // so that dependencies are deleted first. - $this->addChangeList('create', $recreates, $this->sourceNames); - $this->addChangeList('delete', $recreates, array_reverse($this->targetNames)); + $this->addChangeList($collection, 'create', $recreates, $this->sourceNames[$collection]); + $this->addChangeList($collection, 'delete', $recreates, array_reverse($this->targetNames[$collection])); } } @@ -216,17 +274,20 @@ protected function addChangelistUpdate() { * The list of renames is created from the different source and target names * with same UUID. These changes will be removed from the create and delete * lists. + * + * @param string $collection + * The storage collection to operate on. */ - protected function addChangelistRename() { + protected function addChangelistRename($collection) { // Renames will be present in both the create and delete lists. - $create_list = $this->getChangelist('create'); - $delete_list = $this->getChangelist('delete'); + $create_list = $this->getChangelist('create', $collection); + $delete_list = $this->getChangelist('delete', $collection); if (empty($create_list) || empty($delete_list)) { return; } $create_uuids = array(); - foreach ($this->sourceData as $id => $data) { + foreach ($this->sourceData[$collection] as $id => $data) { if (isset($data['uuid']) && in_array($id, $create_list)) { $create_uuids[$data['uuid']] = $id; } @@ -245,50 +306,52 @@ protected function addChangelistRename() { // Node type is a good example of a configuration entity that renames other // configuration when it is renamed. // @see \Drupal\node\Entity\NodeType::postSave() - foreach ($this->targetNames as $name) { - $data = $this->targetData[$name]; + foreach ($this->targetNames[$collection] as $name) { + $data = $this->targetData[$collection][$name]; if (isset($data['uuid']) && isset($create_uuids[$data['uuid']])) { // Remove the item from the create list. - $this->removeFromChangelist('create', $create_uuids[$data['uuid']]); + $this->removeFromChangelist($collection, 'create', $create_uuids[$data['uuid']]); // Remove the item from the delete list. - $this->removeFromChangelist('delete', $name); + $this->removeFromChangelist($collection, 'delete', $name); // Create the rename name. $renames[] = $this->createRenameName($name, $create_uuids[$data['uuid']]); } } - $this->addChangeList('rename', $renames); + $this->addChangeList($collection, 'rename', $renames); } /** * Removes the entry from the given operation changelist for the given name. * + * @param string $collection + * The storage collection to operate on. * @param string $op * The changelist to act on. Either delete, create, rename or update. * @param string $name * The name of the configuration to remove. */ - protected function removeFromChangelist($op, $name) { - $key = array_search($name, $this->changelist[$op]); + protected function removeFromChangelist($collection, $op, $name) { + $key = array_search($name, $this->changelist[$collection][$op]); if ($key !== FALSE) { - unset($this->changelist[$op][$key]); + unset($this->changelist[$collection][$op][$key]); } } /** * {@inheritdoc} */ - public function moveRenameToUpdate($rename) { + public function moveRenameToUpdate($rename, $collection = StorageInterface::DEFAULT_COLLECTION) { $names = $this->extractRenameNames($rename); - $this->removeFromChangelist('rename', $rename); - $this->addChangeList('update', array($names['new_name']), $this->sourceNames); + $this->removeFromChangelist($collection, 'rename', $rename); + $this->addChangeList($collection, 'update', array($names['new_name']), $this->sourceNames[$collection]); } /** * {@inheritdoc} */ public function reset() { - $this->changelist = $this->getEmptyChangelist(); + $this->changelist = array(StorageInterface::DEFAULT_COLLECTION => $this->getEmptyChangelist()); $this->sourceNames = $this->targetNames = array(); return $this->createChangelist(); } @@ -296,10 +359,12 @@ public function reset() { /** * {@inheritdoc} */ - public function hasChanges($ops = array('delete', 'create', 'update', 'rename')) { - foreach ($ops as $op) { - if (!empty($this->changelist[$op])) { - return TRUE; + public function hasChanges() { + foreach ($this->getAllCollectionNames() as $collection) { + foreach (array('delete', 'create', 'update', 'rename') as $op) { + if (!empty($this->changelist[$collection][$op])) { + return TRUE; + } } } return FALSE; @@ -317,12 +382,22 @@ public function validateSiteUuid() { /** * Gets and sorts configuration data from the source and target storages. */ - protected function getAndSortConfigData() { - $this->targetData = $this->targetStorage->readMultiple($this->targetStorage->listAll()); - $this->sourceData = $this->sourceStorage->readMultiple($this->sourceStorage->listAll()); - $dependency_manager = new ConfigDependencyManager(); - $this->targetNames = $dependency_manager->setData($this->targetData)->sortAll(); - $this->sourceNames = $dependency_manager->setData($this->sourceData)->sortAll(); + protected function getAndSortConfigData($collection) { + $source_storage = $this->getSourceStorage($collection); + $target_storage = $this->getTargetStorage($collection); + $this->targetData[$collection] = $target_storage->readMultiple($target_storage->listAll()); + $this->sourceData[$collection] = $source_storage->readMultiple($source_storage->listAll()); + // Collections only support simple configuration therefore do not use + // configuration dependencies. + if ($this->supportsConfigurationEntities($collection)) { + $dependency_manager = new ConfigDependencyManager(); + $this->targetNames[$collection] = $dependency_manager->setData($this->targetData[$collection])->sortAll(); + $this->sourceNames[$collection] = $dependency_manager->setData($this->sourceData[$collection])->sortAll(); + } + else { + $this->targetNames[$collection] = $target_storage->listAll(); + $this->sourceNames[$collection] = $source_storage->listAll(); + } } /** @@ -352,4 +427,22 @@ public function extractRenameNames($name) { 'new_name' => $names[1], ); } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames($include_default = TRUE) { + $collections = array_unique(array_merge($this->sourceStorage->getAllCollectionNames(), $this->targetStorage->getAllCollectionNames())); + if ($include_default) { + array_unshift($collections, StorageInterface::DEFAULT_COLLECTION); + } + return $collections; + } + + /** + * {@inheritdoc} + */ + public function supportsConfigurationEntities($collection) { + return $collection == StorageInterface::DEFAULT_COLLECTION; + } } diff --git a/core/lib/Drupal/Core/Config/StorageComparerInterface.php b/core/lib/Drupal/Core/Config/StorageComparerInterface.php index f85a8aa..f74d7a9 100644 --- a/core/lib/Drupal/Core/Config/StorageComparerInterface.php +++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php @@ -15,18 +15,26 @@ /** * Gets the configuration source storage. * + * @param string $collection + * (optional) The storage collection to use. Defaults to the + * default collection. + * * @return \Drupal\Core\Config\StorageInterface * Storage object used to read configuration. */ - public function getSourceStorage(); + public function getSourceStorage($collection = StorageInterface::DEFAULT_COLLECTION); /** * Gets the configuration target storage. * + * @param string $collection + * (optional) The storage collection to use. Defaults to the + * default collection. + * * @return \Drupal\Core\Config\StorageInterface * Storage object used to write configuration. */ - public function getTargetStorage(); + public function getTargetStorage($collection = StorageInterface::DEFAULT_COLLECTION); /** * Gets an empty changelist. @@ -42,11 +50,14 @@ public function getEmptyChangelist(); * @param string $op * (optional) A change operation. Either delete, create or update. If * supplied the returned list will be limited to this operation. + * @param string $collection + * (optional) The collection to get the changelist for. Defaults to the + * default collection. * * @return array * An array of config changes that are yet to be imported. */ - public function getChangelist($op = NULL); + public function getChangelist($op = NULL, $collection = StorageInterface::DEFAULT_COLLECTION); /** * Recalculates the differences. @@ -63,14 +74,10 @@ public function reset(); * * @see \Drupal\Core\Config\StorageComparerInterface::createChangelist(). * - * @param array $ops - * The operations to check for changes. Defaults to all operations, i.e. - * array('delete', 'create', 'update'). - * * @return bool * TRUE if there are changes to process and FALSE if not. */ - public function hasChanges($ops = array('delete', 'create', 'update')); + public function hasChanges(); /** * Validates that the system.site::uuid in the source and target match. @@ -85,10 +92,13 @@ public function validateSiteUuid(); * * @param string $rename * The rename name, as provided by ConfigImporter::createRenameName(). + * @param string $collection + * (optional) The collection where the configuration is stored. Defaults to + * the default collection. * * @see \Drupal\Core\Config\ConfigImporter::createRenameName() */ - public function moveRenameToUpdate($rename); + public function moveRenameToUpdate($rename, $collection = StorageInterface::DEFAULT_COLLECTION); /** * Extracts old and new configuration names from a configuration change name. @@ -106,4 +116,26 @@ public function moveRenameToUpdate($rename); */ public function extractRenameNames($name); + /** + * Gets the existing collections from both the target and source storage. + * + * @param bool $include_default + * (optional) Include the default unnamed collection. Defaults to TRUE. + * + * @return array + * An array of existing collection names. + */ + public function getAllCollectionNames($include_default = TRUE); + + /** + * Determines if the provided collection supports configuration entities. + * + * @param string $collection + * The collection to check. + * + * @return bool + * TRUE if the collection support configuration entities, FALSE if not. + */ + public function supportsConfigurationEntities($collection); + } diff --git a/core/lib/Drupal/Core/Config/StorageInterface.php b/core/lib/Drupal/Core/Config/StorageInterface.php index 8b99784..a5ffb02 100644 --- a/core/lib/Drupal/Core/Config/StorageInterface.php +++ b/core/lib/Drupal/Core/Config/StorageInterface.php @@ -16,6 +16,11 @@ interface StorageInterface { /** + * The default collection name. + */ + const DEFAULT_COLLECTION = ''; + + /** * Returns whether a configuration object exists. * * @param string $name @@ -156,4 +161,52 @@ public function listAll($prefix = ''); */ public function deleteAll($prefix = ''); + /** + * Creates a collection on the storage. + * + * A configuration storage can contain multiple sets of configuration objects + * in partitioned collections. The collection name identifies the current + * collection used. + * + * Implementations of this method must provide a new instance to avoid side + * effects caused by the fact that Config objects have their storage injected. + * + * @param string $collection + * The collection name. Valid collection names conform to the following + * regex [a-zA-Z_.]. A storage does not need to have a collection set. + * However, if a collection is set, then storage should use it to store + * configuration in a way that allows retrieval of configuration for a + * particular collection. Collections can be nested, for example + * 'language.de'. If this is a file system then we could create a language + * folder with a subfolder named de. If this is a database table then there + * could be a collection column which will store the value 'language.de'. + * Storage nested levels should be consistent. If a collection with the name + * 'language.de' exists then a collections with the names 'language' or + * 'language.en.gb' should not be used. + * + * @return \Drupal\Core\Config\StorageInterface + * An new instance of the storage backend with the collection set. + */ + public function createCollection($collection); + + /** + * Gets the existing collections. + * + * A configuration storage can contain multiple sets of configuration objects + * in partitioned collections. The collection key name identifies the current + * collection used. + * + * @return array + * An array of existing collection names. + */ + public function getAllCollectionNames(); + + /** + * Gets the name of the current collection the storage is using. + * + * @return string + * The current collection name. + */ + public function getCollectionName(); + } diff --git a/core/modules/config/config.routing.yml b/core/modules/config/config.routing.yml index f2a944a..0fdb30b 100644 --- a/core/modules/config/config.routing.yml +++ b/core/modules/config/config.routing.yml @@ -14,6 +14,14 @@ config.diff: requirements: _permission: 'synchronize configuration' +config.diff_collection: + path: '/admin/config/development/configuration/sync/diff_collection/{collection}/{source_name}/{target_name}' + defaults: + _content: '\Drupal\config\Controller\ConfigController::diff' + target_name: NULL + requirements: + _permission: 'synchronize configuration' + config.export_download: path: '/admin/config/development/configuration/full/export-download' defaults: diff --git a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php index 88cdeab..ff91637 100644 --- a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php +++ b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php @@ -85,9 +85,15 @@ public function downloadExport() { file_unmanaged_delete(file_directory_temp() . '/config.tar.gz'); $archiver = new ArchiveTar(file_directory_temp() . '/config.tar.gz', 'gz'); - foreach (\Drupal::service('config.storage')->listAll() as $name) { + foreach ($this->targetStorage->listAll() as $name) { $archiver->addString("$name.yml", Yaml::encode(\Drupal::config($name)->get())); } + foreach ($this->targetStorage->getAllCollectionNames() as $collection) { + $collection_storage = $this->targetStorage->createCollection($collection); + foreach ($collection_storage->listAll() as $name) { + $archiver->addString(str_replace('.', '/', $collection) . "/$name.yml", Yaml::encode($collection_storage->read($name))); + } + } $request = new Request(array('file' => 'config.tar.gz')); return $this->fileDownloadController->download($request, 'temporary'); @@ -96,15 +102,23 @@ public function downloadExport() { /** * Shows diff of specificed configuration file. * - * @param string $config_file + * @param string $source_name * The name of the configuration file. + * @param string $target_name + * (optional) The name of the target configuration file if different from + * the $source_name. + * @param string $collection + * (optional) The configuration collection name. Defaults to the default + * collection. * * @return string * Table showing a two-way diff between the active and staged configuration. */ - public function diff($source_name, $target_name = NULL) { - - $diff = $this->configManager->diff($this->targetStorage, $this->sourceStorage, $source_name, $target_name); + public function diff($source_name, $target_name = NULL, $collection = NULL) { + if (!isset($collection)) { + $collection = StorageInterface::DEFAULT_COLLECTION; + } + $diff = $this->configManager->diff($this->targetStorage, $this->sourceStorage, $source_name, $target_name, $collection); $formatter = new \DrupalDiffFormatter(); $formatter->show_header = FALSE; diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index a9eaac7..3a19912 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -184,71 +184,86 @@ public function buildForm(array $form, array &$form_state) { // Add the AJAX library to the form for dialog support. $form['#attached']['library'][] = 'core/drupal.ajax'; - foreach ($storage_comparer->getChangelist() as $config_change_type => $config_names) { - if (empty($config_names)) { - continue; - } - - // @todo A table caption would be more appropriate, but does not have the - // visual importance of a heading. - $form[$config_change_type]['heading'] = array( - '#type' => 'html_tag', - '#tag' => 'h3', - ); - switch ($config_change_type) { - case 'create': - $form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count new', '@count new'); - break; - - case 'update': - $form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count changed', '@count changed'); - break; - - case 'delete': - $form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count removed', '@count removed'); - break; - - case 'rename': - $form[$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count renamed', '@count renamed'); - break; + foreach ($storage_comparer->getAllCollectionNames() as $collection) { + if ($collection != StorageInterface::DEFAULT_COLLECTION) { + $form[$collection]['collection_heading'] = array( + '#type' => 'html_tag', + '#tag' => 'h2', + '#value' => $this->t('!collection configuration collection', array('!collection' => $collection)), + ); } - $form[$config_change_type]['list'] = array( - '#type' => 'table', - '#header' => array('Name', 'Operations'), - ); - - foreach ($config_names as $config_name) { - if ($config_change_type == 'rename') { - $names = $storage_comparer->extractRenameNames($config_name); - $href = $this->urlGenerator->getPathFromRoute('config.diff', array('source_name' => $names['old_name'], 'target_name' => $names['new_name'])); - $config_name = $this->t('!source_name to !target_name', array('!source_name' => $names['old_name'], '!target_name' => $names['new_name'])); + foreach ($storage_comparer->getChangelist(NULL, $collection) as $config_change_type => $config_names) { + if (empty($config_names)) { + continue; } - else { - $href = $this->urlGenerator->getPathFromRoute('config.diff', array('source_name' => $config_name)); + + // @todo A table caption would be more appropriate, but does not have the + // visual importance of a heading. + $form[$collection][$config_change_type]['heading'] = array( + '#type' => 'html_tag', + '#tag' => 'h3', + ); + switch ($config_change_type) { + case 'create': + $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count new', '@count new'); + break; + + case 'update': + $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count changed', '@count changed'); + break; + + case 'delete': + $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count removed', '@count removed'); + break; + + case 'rename': + $form[$collection][$config_change_type]['heading']['#value'] = format_plural(count($config_names), '@count renamed', '@count renamed'); + break; } - $links['view_diff'] = array( - 'title' => $this->t('View differences'), - 'href' => $href, - 'attributes' => array( - 'class' => array('use-ajax'), - 'data-accepts' => 'application/vnd.drupal-modal', - 'data-dialog-options' => json_encode(array( - 'width' => 700 - )), - ), + $form[$collection][$config_change_type]['list'] = array( + '#type' => 'table', + '#header' => array('Name', 'Operations'), ); - $form[$config_change_type]['list']['#rows'][] = array( - 'name' => $config_name, - 'operations' => array( - 'data' => array( - '#type' => 'operations', - '#links' => $links, + + foreach ($config_names as $config_name) { + if ($config_change_type == 'rename') { + $names = $storage_comparer->extractRenameNames($config_name); + $route_options = array('source_name' => $names['old_name'], 'target_name' => $names['new_name']); + $config_name = $this->t('!source_name to !target_name', array('!source_name' => $names['old_name'], '!target_name' => $names['new_name'])); + } + else { + $route_options = array('source_name' => $config_name); + } + if ($collection != StorageInterface::DEFAULT_COLLECTION) { + $route_options['collection'] = $collection; + $href = $this->urlGenerator->getPathFromRoute('config.diff_collection', $route_options); + } + else { + $href = $this->urlGenerator->getPathFromRoute('config.diff', $route_options); + } + $links['view_diff'] = array( + 'title' => $this->t('View differences'), + 'href' => $href, + 'attributes' => array( + 'class' => array('use-ajax'), + 'data-accepts' => 'application/vnd.drupal-modal', + 'data-dialog-options' => json_encode(array( + 'width' => 700 + )), ), - ), - ); + ); + $form[$collection][$config_change_type]['list']['#rows'][] = array( + 'name' => $config_name, + 'operations' => array( + 'data' => array( + '#type' => 'operations', + '#links' => $links, + ), + ), + ); + } } } - return $form; } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php index 6f3b3de..138b861 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigDiffTest.php @@ -113,4 +113,36 @@ function testDiff() { $this->assertEqual(count($diff->edits), 2, 'There are two items in the diff.'); } + /** + * Tests calculating the difference between two sets of config collections. + */ + function testCollectionDiff() { + /** @var \Drupal\Core\Config\StorageInterface $active */ + $active = $this->container->get('config.storage'); + /** @var \Drupal\Core\Config\StorageInterface $staging */ + $staging = $this->container->get('config.storage.staging'); + $active_test_collection = $active->createCollection('test'); + $staging_test_collection = $staging->createCollection('test'); + + $config_name = 'config_test.test'; + $data = array('foo' => 'bar'); + + $active->write($config_name, $data); + $staging->write($config_name, $data); + $active_test_collection->write($config_name, $data); + $staging_test_collection->write($config_name, array('foo' => 'baz')); + + // Test the fields match in the default collection diff. + $diff = \Drupal::service('config.manager')->diff($active, $staging, $config_name); + $this->assertEqual($diff->edits[0]->type, 'copy', 'The first item in the diff is a copy.'); + $this->assertEqual(count($diff->edits), 1, 'There is one item in the diff'); + + // Test that the differences are detected when diffing the collection. + $diff = \Drupal::service('config.manager')->diff($active, $staging, $config_name, NULL, 'test'); + $this->assertEqual($diff->edits[0]->type, 'change', 'The second item in the diff is a copy.'); + $this->assertEqual($diff->edits[0]->orig, array('foo: bar')); + $this->assertEqual($diff->edits[0]->closing, array('foo: baz')); + $this->assertEqual($diff->edits[1]->type, 'copy', 'The second item in the diff is a copy.'); + } + } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php index 0a01bd1..d9bc321 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php @@ -7,6 +7,7 @@ namespace Drupal\config\Tests; +use Drupal\Core\Archiver\ArchiveTar; use Drupal\simpletest\WebTestBase; /** @@ -130,4 +131,84 @@ public function testExportImport() { $this->drupalGet('node/add'); $this->assertFieldByName("{$this->field->name}[0][value]", '', 'Widget is displayed'); } + + /** + * Tests an export and import of collections. + */ + public function testExportImportCollections() { + + /** @var \Drupal\Core\Config\StorageInterface $active_storage */ + $active_storage = \Drupal::service('config.storage'); + $test1_storage = $active_storage->createCollection('collection.test1'); + $test1_storage->write('config_test.create', array('foo' => 'bar')); + $test1_storage->write('config_test.update', array('foo' => 'bar')); + $test2_storage = $active_storage->createCollection('collection.test2'); + $test2_storage->write('config_test.another_create', array('foo' => 'bar')); + $test2_storage->write('config_test.another_update', array('foo' => 'bar')); + + // Export the configuration. + $this->drupalPostForm('admin/config/development/configuration/full/export', array(), 'Export'); + $this->tarball = $this->drupalGetContent(); + $filename = file_directory_temp() .'/' . $this->randomName(); + file_put_contents($filename, $this->tarball); + + // Set up the active storage collections to test import. + $test1_storage->delete('config_test.create'); + $test1_storage->write('config_test.update', array('foo' => 'baz')); + $test1_storage->write('config_test.delete', array('foo' => 'bar')); + $test2_storage->delete('config_test.another_create'); + $test2_storage->write('config_test.another_update', array('foo' => 'baz')); + $test2_storage->write('config_test.another_delete', array('foo' => 'bar')); + + // Create the tar contains the expected contect for the collections. + $tar = new ArchiveTar($filename, 'gz'); + $content_list = $tar->listContent(); + // Convert the list of files into something easy to search. + $files = array(); + foreach ($content_list as $file) { + $files[] = $file['filename']; + } + $this->assertTrue(in_array('collection/test1/config_test.create.yml', $files), 'Config export contains collection/test1/config_test.create.yml.'); + $this->assertTrue(in_array('collection/test2/config_test.another_create.yml', $files), 'Config export contains collection/test2/config_test.another_create.yml.'); + $this->assertTrue(in_array('collection/test1/config_test.update.yml', $files), 'Config export contains collection/test1/config_test.update.yml.'); + $this->assertTrue(in_array('collection/test2/config_test.another_update.yml', $files), 'Config export contains collection/test2/config_test.another_update.yml.'); + $this->assertFalse(in_array('collection/test1/config_test.delete.yml', $files), 'Config export does not contain collection/test1/config_test.delete.yml.'); + $this->assertFalse(in_array('collection/test2/config_test.another_delete.yml', $files), 'Config export does not contain collection/test2/config_test.another_delete.yml.'); + + $this->drupalPostForm('admin/config/development/configuration/full/import', array('files[import_tarball]' => $filename), 'Upload'); + // Verify that there are configuration differences to import. + $this->drupalGet('admin/config/development/configuration'); + $this->assertNoText(t('There are no configuration changes.')); + $this->assertText(t('!collection configuration collection', array('!collection' => 'collection.test1'))); + $this->assertText(t('!collection configuration collection', array('!collection' => 'collection.test2'))); + $this->assertText('config_test.create'); + $this->assertLinkByHref('admin/config/development/configuration/sync/diff_collection/collection.test1/config_test.create'); + $this->assertText('config_test.update'); + $this->assertLinkByHref('admin/config/development/configuration/sync/diff_collection/collection.test1/config_test.update'); + $this->assertText('config_test.delete'); + $this->assertLinkByHref('admin/config/development/configuration/sync/diff_collection/collection.test1/config_test.delete'); + $this->assertText('config_test.another_create'); + $this->assertLinkByHref('admin/config/development/configuration/sync/diff_collection/collection.test2/config_test.another_create'); + $this->assertText('config_test.another_update'); + $this->assertLinkByHref('admin/config/development/configuration/sync/diff_collection/collection.test2/config_test.another_update'); + $this->assertText('config_test.another_delete'); + $this->assertLinkByHref('admin/config/development/configuration/sync/diff_collection/collection.test2/config_test.another_delete'); + + $this->drupalPostForm(NULL, array(), 'Import all'); + $this->assertText(t('There are no configuration changes.')); + + // Test data in collections. + $data = $test1_storage->read('config_test.create'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.create in collection.test1 has been created.'); + $data = $test1_storage->read('config_test.update'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.update in collection.test1 has been updated.'); + $this->assertFalse($test1_storage->read('config_test.delete'), 'The config_test.delete in collection.test1 has been updated.'); + + $data = $test2_storage->read('config_test.another_create'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.another_create in collection.test2 has been created.'); + $data = $test2_storage->read('config_test.another_update'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.another_update in collection.test2 has been updated.'); + $this->assertFalse($test2_storage->read('config_test.another_delete'), 'The config_test.another_delete in collection.test2 has been updated.'); + } + } diff --git a/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php b/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php index df1b335..e90a058 100644 --- a/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php @@ -51,9 +51,9 @@ public function testDefaultConfig() { $default_config_storage = new TestInstallStorage(); foreach ($default_config_storage->listAll() as $config_name) { - // @todo: remove once migration (https://drupal.org/node/2183957) and - // translation (https://drupal.org/node/2168609) schemas are in. - if (strpos($config_name, 'migrate.migration') === 0 || strpos($config_name, 'language.config') === 0) { + // @todo: remove once migration (https://drupal.org/node/2183957) schemas + // are in. + if (strpos($config_name, 'migrate.migration') === 0) { continue; } diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/CachedStorageTest.php b/core/modules/config/lib/Drupal/config/Tests/Storage/CachedStorageTest.php index cb00a1e..766d013 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/CachedStorageTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/CachedStorageTest.php @@ -42,8 +42,8 @@ public static function getInfo() { function setUp() { parent::setUp(); $this->filestorage = new FileStorage($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]); + $this->storage = new CachedStorage($this->filestorage, \Drupal::service('cache_factory')); $this->cache = \Drupal::service('cache_factory')->get('config'); - $this->storage = new CachedStorage($this->filestorage, $this->cache); // ::listAll() verifications require other configuration data to exist. $this->storage->write('system.performance', array()); } diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php index c112ccd..79d6311 100644 --- a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php +++ b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php @@ -181,6 +181,65 @@ function testDataTypes() { $this->assertIdentical($read_data, $data); } + /** + * Tests that the storage supports collections. + */ + public function testCollection() { + $name = 'config_test.storage'; + $data = array('foo' => 'bar'); + $result = $this->storage->write($name, $data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($data, $this->storage->read($name)); + + // Create configuration in a new collection. + $new_storage = $this->storage->createCollection('collection.sub.new'); + $this->assertFalse($new_storage->exists($name)); + $this->assertEqual(array(), $new_storage->listAll()); + $new_storage->write($name, $data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($data, $new_storage->read($name)); + $this->assertEqual(array($name), $new_storage->listAll()); + $this->assertTrue($new_storage->exists($name)); + $new_data = array('foo' => 'baz'); + $new_storage->write($name, $new_data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($new_data, $new_storage->read($name)); + + // Create configuration in another collection. + $another_storage = $this->storage->createCollection('collection.sub.another'); + $this->assertFalse($another_storage->exists($name)); + $this->assertEqual(array(), $another_storage->listAll()); + $another_storage->write($name, $new_data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($new_data, $another_storage->read($name)); + $this->assertEqual(array($name), $another_storage->listAll()); + $this->assertTrue($another_storage->exists($name)); + + // Create configuration in yet another collection. + $alt_storage = $this->storage->createCollection('alternate'); + $alt_storage->write($name, $new_data); + $this->assertIdentical($result, TRUE); + $this->assertIdentical($new_data, $alt_storage->read($name)); + + // Switch back to the collection-less mode and check the data still exists + // add has not been touched. + $this->assertIdentical($data, $this->storage->read($name)); + + // Check that the getAllCollectionNames() method works. + $this->assertIdentical(array('alternate', 'collection.sub.another', 'collection.sub.new'), $this->storage->getAllCollectionNames()); + + // Check that the collections are removed when they are empty. + $alt_storage->delete($name); + $this->assertIdentical(array('collection.sub.another', 'collection.sub.new'), $this->storage->getAllCollectionNames()); + + // Check that the having an empty collection-less storage does not break + // anything. Before deleting check that the previous delete did not affect + // data in another collection. + $this->assertIdentical($data, $this->storage->read($name)); + $this->storage->delete($name); + $this->assertIdentical(array('collection.sub.another', 'collection.sub.new'), $this->storage->getAllCollectionNames()); + } + abstract protected function read($name); abstract protected function insert($name, $data); @@ -188,4 +247,5 @@ function testDataTypes() { abstract protected function update($name, $data); abstract protected function delete($name); + } diff --git a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php index 106df3d..355869f 100644 --- a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php +++ b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php @@ -40,5 +40,17 @@ public function getCacheSuffix() { return 'ConfigOverrider'; } + /** + * {@inheritdoc} + */ + public function install($type, $name) { + } + + /** + * {@inheritdoc} + */ + public function uninstall($type, $name) { + } + } diff --git a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php index a9a435e..19ea21e 100644 --- a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php +++ b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php @@ -41,5 +41,17 @@ public function getCacheSuffix() { return 'ConfigOverriderLowPriority'; } + /** + * {@inheritdoc} + */ + public function install($type, $name) { + } + + /** + * {@inheritdoc} + */ + public function uninstall($type, $name) { + } + } diff --git a/core/modules/config/tests/config_test/config/install/language.config.de.config_test.system.yml b/core/modules/config/tests/config_test/config/install/language/de/config_test.system.yml similarity index 100% rename from core/modules/config/tests/config_test/config/install/language.config.de.config_test.system.yml rename to core/modules/config/tests/config_test/config/install/language/de/config_test.system.yml diff --git a/core/modules/config/tests/config_test/config/install/language.config.en.config_test.system.yml b/core/modules/config/tests/config_test/config/install/language/en/config_test.system.yml similarity index 100% rename from core/modules/config/tests/config_test/config/install/language.config.en.config_test.system.yml rename to core/modules/config/tests/config_test/config/install/language/en/config_test.system.yml diff --git a/core/modules/config/tests/config_test/config/install/language.config.fr.config_test.system.yml b/core/modules/config/tests/config_test/config/install/language/fr/config_test.system.yml similarity index 100% rename from core/modules/config/tests/config_test/config/install/language.config.fr.config_test.system.yml rename to core/modules/config/tests/config_test/config/install/language/fr/config_test.system.yml diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php index fb3ba88..13311d9 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php @@ -15,6 +15,7 @@ use Drupal\Core\Form\BaseFormIdInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Language\Language; +use Drupal\language\Config\LanguageConfigOverride; use Drupal\language\ConfigurableLanguageManagerInterface; use Drupal\locale\StringStorageInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -352,7 +353,7 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d * Set the configuration in this language. * @param \Drupal\Core\Config\Config $base_config * Base configuration values, in the source language. - * @param \Drupal\Core\Config\Config $config_translation + * @param \Drupal\language\Config\LanguageConfigOverride $config_translation * Translation configuration override data. * @param array $config_values * A simple one dimensional or recursive array: @@ -372,7 +373,7 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d * @return array * Translation configuration override data. */ - protected function setConfig(Language $language, Config $base_config, Config $config_translation, array $config_values, $shipped_config = FALSE) { + protected function setConfig(Language $language, Config $base_config, LanguageConfigOverride $config_translation, array $config_values, $shipped_config = FALSE) { foreach ($config_values as $key => $value) { if (is_array($value) && !isset($value['translation'])) { // Traverse into this level in the configuration. diff --git a/core/modules/language/language.module b/core/modules/language/language.module index f35da4d..6a71fc7 100644 --- a/core/modules/language/language.module +++ b/core/modules/language/language.module @@ -441,6 +441,8 @@ function language_save($language) { $language_entity->save(); $t_args = array('%language' => $language->name, '%langcode' => $language->id); if ($language->is_new) { + // Install any available language configuration overrides for the language. + \Drupal::service('language.config_factory_override')->installLanguageOverrides($language->getId()); watchdog('language', 'The %language (%langcode) language has been created.', $t_args); } else { diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php index bfadec0..76a384a 100644 --- a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php @@ -7,7 +7,8 @@ namespace Drupal\language\Config; -use Drupal\Core\Config\Config; +use Drupal\Component\Utility\Unicode; +use Drupal\Core\Config\ExtensionInstallStorage; use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Language\Language; @@ -22,9 +23,19 @@ class LanguageConfigFactoryOverride implements LanguageConfigFactoryOverrideInte /** * The configuration storage. * + * Do not access this directly. Should be accessed through self::getStorage() + * so that the cache of storages per langcode is used. + * * @var \Drupal\Core\Config\StorageInterface */ - protected $storage; + protected $baseStorage; + + /** + * An array of configuration storages keyed by langcode. + * + * @var \Drupal\Core\Config\StorageInterface[] + */ + protected $storages; /** * The typed config manager. @@ -58,7 +69,7 @@ class LanguageConfigFactoryOverride implements LanguageConfigFactoryOverrideInte * The typed configuration manager. */ public function __construct(StorageInterface $storage, EventDispatcherInterface $event_dispatcher, TypedConfigManagerInterface $typed_config) { - $this->storage = $storage; + $this->baseStorage = $storage; $this->eventDispatcher = $event_dispatcher; $this->typedConfigManager = $typed_config; } @@ -67,72 +78,34 @@ public function __construct(StorageInterface $storage, EventDispatcherInterface * {@inheritdoc} */ public function loadOverrides($names) { - $data = array(); - $language_names = $this->getLanguageConfigNames($names); - if ($language_names) { - $data = $this->storage->readMultiple(array_values($language_names)); - // Re-key the data array to use configuration names rather than override - // names. - $prefix_length = strlen(static::LANGUAGE_CONFIG_PREFIX . '.' . $this->language->id) + 1; - foreach ($data as $key => $value) { - unset($data[$key]); - $key = substr($key, $prefix_length); - $data[$key] = $value; - } + if ($this->language) { + $storage = $this->getStorage($this->language->getId()); + return $storage->readMultiple($names); } - return $data; + return array(); } /** * {@inheritdoc} */ public function getOverride($langcode, $name) { - $override_name = $this->getLanguageConfigName($langcode, $name); - $overrides = $this->storage->read($override_name); - $config = new Config($override_name, $this->storage, $this->eventDispatcher, $this->typedConfigManager); - if (!empty($overrides)) { - $config->initWithData($overrides); + $storage = $this->getStorage($langcode); + $data = $storage->read($name); + $override = new LanguageConfigOverride($name, $storage, $this->typedConfigManager); + if (!empty($data)) { + $override->initWithData($data); } - return $config; + return $override; } /** - * Generate a list of configuration names based on base names. - * - * @param array $names - * List of configuration names. - * - * @return array - * List of configuration names for language override files if applicable. - */ - protected function getLanguageConfigNames(array $names) { - $language_names = array(); - if (isset($this->language)) { - foreach ($names as $name) { - if ($language_name = $this->getLanguageConfigName($this->language->id, $name)) { - $language_names[$name] = $language_name; - } - } - } - return $language_names; - } - - /** - * Get language override name for given language and configuration name. - * - * @param string $langcode - * Language code. - * @param string $name - * Configuration name. - * - * @return bool|string - * Configuration name or FALSE if not applicable. + * {@inheritdoc} */ - protected function getLanguageConfigName($langcode, $name) { - if (strpos($name, static::LANGUAGE_CONFIG_PREFIX) === 0) { - return FALSE; + public function getStorage($langcode) { + if (!isset($this->storages[$langcode])) { + $this->storages[$langcode] = $this->baseStorage->createCollection($this->createConfigCollectionName($langcode)); } - return static::LANGUAGE_CONFIG_PREFIX . '.' . $langcode . '.' . $name; + return $this->storages[$langcode]; } /** @@ -165,4 +138,72 @@ public function setLanguageFromDefault(LanguageDefault $language_default = NULL) return $this; } + /** + * {@inheritdoc} + */ + public function install($type, $name) { + $config_installer = \Drupal::service('config.installer'); + // Work out if this extension provides default language overrides. + foreach (\Drupal::languageManager()->getLanguages() as $language) { + $collection = $this->createConfigCollectionName($language->getId()); + $config_to_install = $config_installer->getConfigToInstall($type, $name, $collection); + $storage = $this->getStorage($language->getId()); + // Remove configuration that already exists in the active storage. + $config_to_install = array_diff($config_to_install, $storage->listAll()); + $data = $config_installer->getSourceStorage($collection)->readMultiple($config_to_install); + foreach ($data as $config_name => $config_data) { + $config = new LanguageConfigOverride($config_name, $storage, $this->typedConfigManager); + $config->setData($config_data)->save(); + } + } + } + + /** + * {@inheritdoc} + */ + public function installLanguageOverrides($langcode) { + $enabled_extensions = array(); + $extension_config = $this->baseStorage->read('core.extension'); + $enabled_extensions += array_keys($extension_config['module']); + $enabled_extensions += array_keys($extension_config['theme']); + + $install_storage = new ExtensionInstallStorage($this->baseStorage, ExtensionInstallStorage::CONFIG_INSTALL_DIRECTORY, $this->createConfigCollectionName($langcode)); + $storage = $this->getStorage($langcode); + + $config_to_install = $install_storage->listAll(); + // Only install language overrides for enable modules. + $config_to_install = array_filter($config_to_install, function ($config_name) use ($enabled_extensions) { + $provider = Unicode::substr($config_name, 0, strpos($config_name, '.')); + return in_array($provider, $enabled_extensions); + }); + foreach ($config_to_install as $config_name) { + $data = $install_storage->read($config_name); + $config = new LanguageConfigOverride($config_name, $storage, $this->typedConfigManager); + $config->setData($data)->save(); + } + } + + /** + * {@inheritdoc} + */ + public function uninstall($type, $name) { + foreach (\Drupal::languageManager()->getLanguages() as $language) { + $storage = $this->getStorage($language->getId()); + $storage->deleteAll($name . '.'); + } + } + + /** + * Creates a configuration collection name based on a langcode. + * + * @param $langcode + * The langcode. + * + * @return string + * The configuration collection name for a langcode. + */ + protected function createConfigCollectionName($langcode) { + return 'language.' . $langcode; + } + } diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php index 1c3de55..1d3a43b 100644 --- a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php @@ -17,11 +17,6 @@ interface LanguageConfigFactoryOverrideInterface extends ConfigFactoryOverrideInterface { /** - * Prefix for all language configuration files. - */ - const LANGUAGE_CONFIG_PREFIX = 'language.config'; - - /** * Gets the language object used to override configuration data. * * @return \Drupal\Core\Language\Language @@ -62,4 +57,23 @@ public function setLanguageFromDefault(LanguageDefault $language_default = NULL) */ public function getOverride($langcode, $name); + /** + * Returns the storage instance for a particular langcode. + * + * @param string $langcode + * Language code. + * + * @return \Drupal\language\Config\LanguageOverrideStorageInterface + * The language override storage object. + */ + public function getStorage($langcode); + + /** + * Installs available language configuration overrides for a given langcode. + * + * @param string $langcode + * Language code. + */ + public function installLanguageOverrides($langcode); + } diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageConfigOverride.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigOverride.php new file mode 100644 index 0000000..6ed2b11 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigOverride.php @@ -0,0 +1,71 @@ +name = $name; + $this->storage = $storage; + $this->typedConfigManager = $typed_config; + } + + /** + * {@inheritdoc} + */ + public function save() { + // Validate the configuration object name before saving. + static::validateName($this->name); + + // If there is a schema for this configuration object, cast all values to + // conform to the schema. + if ($this->typedConfigManager->hasConfigSchema($this->name)) { + // Ensure that the schema wrapper has the latest data. + $this->schemaWrapper = NULL; + foreach ($this->data as $key => $value) { + $this->data[$key] = $this->castValue($key, $value); + } + } + + $this->storage->write($this->name, $this->data); + $this->isNew = FALSE; + $this->originalData = $this->data; + return $this; + } + + /** + * {@inheritdoc} + */ + public function delete() { + // @todo Consider to remove the pruning of data for Config::delete(). + $this->data = array(); + $this->storage->delete($this->name); + $this->isNew = TRUE; + $this->originalData = $this->data; + return $this; + } + +} diff --git a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php index 101449c..b28e02b 100644 --- a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php +++ b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManager.php @@ -410,6 +410,13 @@ public function getLanguageConfigOverride($langcode, $name) { /** * {@inheritdoc} */ + public function getLanguageConfigOverrideStorage($langcode) { + return $this->configFactoryOverride->getStorage($langcode); + } + + /** + * {@inheritdoc} + */ public function getStandardLanguageListWithoutConfigured() { $languages = $this->getLanguages(); $predefined = $this->getStandardLanguageList(); diff --git a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php index 9bbf2ce..0007524 100644 --- a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php +++ b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php @@ -91,12 +91,24 @@ public function updateLockedLanguageWeights(); * @param string $name * The language configuration object name. * - * @return \Drupal\Core\Config\Config + * @return \Drupal\language\Config\LanguageConfigOverride * The language config override object. */ public function getLanguageConfigOverride($langcode, $name); /** + * Gets a language config override storage object. + * + * @param string $langcode + * The language code for the override. + * + * @param \Drupal\Core\Config\StorageInterface $storage + * A storage object to use for reading and writing the + * configuration override. + */ + public function getLanguageConfigOverrideStorage($langcode); + + /** * Returns the standard language list excluding already configured languages. * * @return array diff --git a/core/modules/language/lib/Drupal/language/Tests/LanguageConfigOverrideImportTest.php b/core/modules/language/lib/Drupal/language/Tests/LanguageConfigOverrideImportTest.php new file mode 100644 index 0000000..9df51e3 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageConfigOverrideImportTest.php @@ -0,0 +1,67 @@ + 'Language config override synchronize', + 'description' => 'Ensures the language config overrides can be synchronized.', + 'group' => 'Language', + ); + } + + /** + * + */ + public function testConfigOverrideImport() { + language_save(new Language(array( + 'name' => 'French', + 'id' => 'fr', + ))); + /* @var \Drupal\Core\Config\StorageInterface $staging */ + $staging = \Drupal::service('config.storage.staging'); + $this->copyConfig(\Drupal::service('config.storage'), $staging); + + \Drupal::moduleHandler()->uninstall(array('language')); + // Ensure that the current site has no overrides registered to the + // ConfigFactory. + $this->rebuildContainer(); + + /* @var \Drupal\Core\Config\StorageInterface $override_staging */ + $override_staging = new FileStorage(config_get_config_directory(CONFIG_STAGING_DIRECTORY) . '/language/fr'); + // Create some overrides in staging. + $override_staging->write('system.site', array('name' => 'FR default site name')); + $override_staging->write('system.maintenance', array('message' => 'FR message: @site is currently under maintenance. We should be back shortly. Thank you for your patience')); + + $this->configImporter()->import(); + $this->rebuildContainer(); + \Drupal::service('router.builder')->rebuild(); + + $override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'system.site'); + $this->assertEqual('FR default site name', $override->get('name')); + $this->drupalGet('fr'); + $this->assertText('FR default site name'); + + $this->drupalLogin($this->root_user); + $this->drupalGet('admin/config/development/maintenance/translate/fr/edit'); + $this->assertText('FR message: @site is currently under maintenance. We should be back shortly. Thank you for your patience'); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php b/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php index 3c0dc57..f731b13 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php @@ -230,9 +230,9 @@ public function getStringNames(array $lids) { * Language code to delete. */ public function deleteLanguageTranslations($langcode) { - $locale_name = LanguageConfigFactoryOverrideInterface::LANGUAGE_CONFIG_PREFIX . '.' . $langcode . '.'; - foreach ($this->configStorage->listAll($locale_name) as $name) { - $this->configStorage->delete($name); + $storage = $this->languageManager->getLanguageConfigOverrideStorage($langcode); + foreach ($storage->listAll() as $name) { + $this->languageManager->getLanguageConfigOverride($langcode, $name)->delete(); } } diff --git a/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigManagerTest.php b/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigManagerTest.php index bdde87f..50cbc7d 100644 --- a/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigManagerTest.php +++ b/core/modules/locale/lib/Drupal/locale/Tests/LocaleConfigManagerTest.php @@ -42,7 +42,7 @@ public function testHasTranslation() { $this->installConfig(array('locale_test')); $locale_config_manager = \Drupal::service('locale.config.typed'); - $language = new Language(array('id' => 'de')); + $language = language_save(new Language(array('id' => 'de'))); $result = $locale_config_manager->hasTranslation('locale_test.no_translation', $language); $this->assertFalse($result, 'There is no translation for locale_test.no_translation configuration.'); diff --git a/core/modules/locale/tests/modules/locale_test/config/install/language.config.de.locale_test.translation.yml b/core/modules/locale/tests/modules/locale_test/config/install/language/de/locale_test.translation.yml similarity index 100% rename from core/modules/locale/tests/modules/locale_test/config/install/language.config.de.locale_test.translation.yml rename to core/modules/locale/tests/modules/locale_test/config/install/language/de/locale_test.translation.yml diff --git a/core/modules/system/tests/modules/menu_test/config/install/language.config.nl.menu_test.menu_item.yml b/core/modules/system/tests/modules/menu_test/config/install/language/nl/menu_test.menu_item.yml similarity index 100% rename from core/modules/system/tests/modules/menu_test/config/install/language.config.nl.menu_test.menu_item.yml rename to core/modules/system/tests/modules/menu_test/config/install/language/nl/menu_test.menu_item.yml diff --git a/core/modules/tour/tests/tour_test/config/install/language.config.it.tour.tour.tour-test.yml b/core/modules/tour/tests/tour_test/config/install/language/it/tour.tour.tour-test.yml similarity index 100% rename from core/modules/tour/tests/tour_test/config/install/language.config.it.tour.tour.tour-test.yml rename to core/modules/tour/tests/tour_test/config/install/language/it/tour.tour.tour-test.yml diff --git a/core/tests/Drupal/Tests/Core/Config/CachedStorageTest.php b/core/tests/Drupal/Tests/Core/Config/CachedStorageTest.php index 6c60af4..a66d55b 100644 --- a/core/tests/Drupal/Tests/Core/Config/CachedStorageTest.php +++ b/core/tests/Drupal/Tests/Core/Config/CachedStorageTest.php @@ -6,7 +6,6 @@ use Drupal\Core\Config\CachedStorage; use Drupal\Core\Cache\MemoryBackend; use Drupal\Core\Cache\NullBackend; -use Drupal\Core\Cache\CacheBackendInterface; /** * Tests the interaction of cache and file storage in CachedStorage. @@ -15,6 +14,11 @@ */ class CachedStorageTest extends UnitTestCase { + /** + * @var \Drupal\Core\Cache\CacheFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $cacheFactory; + public static function getInfo() { return array( 'name' => 'Config cached storage test', @@ -23,6 +27,10 @@ public static function getInfo() { ); } + public function setUp() { + $this->cacheFactory = $this->getMock('Drupal\Core\Cache\CacheFactoryInterface'); + } + /** * Test listAll static cache. */ @@ -37,7 +45,11 @@ public function testListAllStaticCache() { ->will($this->returnValue($response)); $cache = new NullBackend(__FUNCTION__); - $cachedStorage = new CachedStorage($storage, $cache); + $this->cacheFactory->expects($this->once()) + ->method('get') + ->with('config') + ->will($this->returnValue($cache)); + $cachedStorage = new CachedStorage($storage, $this->cacheFactory); $this->assertEquals($response, $cachedStorage->listAll($prefix)); $this->assertEquals($response, $cachedStorage->listAll($prefix)); } @@ -53,7 +65,11 @@ public function testListAllPrimedPersistentCache() { $response = array("$prefix." . $this->randomName(), "$prefix." . $this->randomName()); $cache = new MemoryBackend(__FUNCTION__); $cache->set('find:' . $prefix, $response); - $cachedStorage = new CachedStorage($storage, $cache); + $this->cacheFactory->expects($this->once()) + ->method('get') + ->with('config') + ->will($this->returnValue($cache)); + $cachedStorage = new CachedStorage($storage, $this->cacheFactory); $this->assertEquals($response, $cachedStorage->listAll($prefix)); } @@ -79,7 +95,11 @@ public function testGetMultipleOnPrimedCache() { foreach ($configCacheValues as $key => $value) { $cache->set($key, $value); } - $cachedStorage = new CachedStorage($storage, $cache); + $this->cacheFactory->expects($this->once()) + ->method('get') + ->with('config') + ->will($this->returnValue($cache)); + $cachedStorage = new CachedStorage($storage, $this->cacheFactory); $this->assertEquals($configCacheValues, $cachedStorage->readMultiple($configNames)); } @@ -119,7 +139,11 @@ public function testGetMultipleOnPartiallyPrimedCache() { ->with(array(2 => $configNames[2], 4 => $configNames[4])) ->will($this->returnValue($response)); - $cachedStorage = new CachedStorage($storage, $cache); + $this->cacheFactory->expects($this->once()) + ->method('get') + ->with('config') + ->will($this->returnValue($cache)); + $cachedStorage = new CachedStorage($storage, $this->cacheFactory); $expected_data = $configCacheValues + array($configNames[2] => $config_exists_not_cached_data); $this->assertEquals($expected_data, $cachedStorage->readMultiple($configNames)); @@ -143,7 +167,11 @@ public function testReadNonExistentFileCacheMiss() { ->method('read') ->with($name) ->will($this->returnValue(FALSE)); - $cachedStorage = new CachedStorage($storage, $cache); + $this->cacheFactory->expects($this->once()) + ->method('get') + ->with('config') + ->will($this->returnValue($cache)); + $cachedStorage = new CachedStorage($storage, $this->cacheFactory); $this->assertFalse($cachedStorage->read($name)); @@ -163,7 +191,11 @@ public function testReadNonExistentFileCached() { $storage = $this->getMock('Drupal\Core\Config\StorageInterface'); $storage->expects($this->never()) ->method('read'); - $cachedStorage = new CachedStorage($storage, $cache); + $this->cacheFactory->expects($this->once()) + ->method('get') + ->with('config') + ->will($this->returnValue($cache)); + $cachedStorage = new CachedStorage($storage, $this->cacheFactory); $this->assertFalse($cachedStorage->read($name)); } diff --git a/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php b/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php index 500c06f..893a56e 100644 --- a/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php +++ b/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php @@ -123,6 +123,12 @@ public function testCreateChangelistNoChange() { $this->targetStorage->expects($this->once()) ->method('readMultiple') ->will($this->returnValue($config_data)); + $this->sourceStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); + $this->targetStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); $this->storageComparer->createChangelist(); $this->assertEmpty($this->storageComparer->getChangelist('create')); @@ -151,6 +157,12 @@ public function testCreateChangelistCreate() { $this->targetStorage->expects($this->once()) ->method('readMultiple') ->will($this->returnValue($target_data)); + $this->sourceStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); + $this->targetStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); $this->storageComparer->createChangelist(); $expected = array( @@ -184,6 +196,12 @@ public function testCreateChangelistDelete() { $this->targetStorage->expects($this->once()) ->method('readMultiple') ->will($this->returnValue($target_data)); + $this->sourceStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); + $this->targetStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); $this->storageComparer->createChangelist(); $expected = array( @@ -217,6 +235,12 @@ public function testCreateChangelistUpdate() { $this->targetStorage->expects($this->once()) ->method('readMultiple') ->will($this->returnValue($target_data)); + $this->sourceStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); + $this->targetStorage->expects($this->once()) + ->method('getAllCollectionNames') + ->will($this->returnValue(array())); $this->storageComparer->createChangelist(); $expected = array(