diff -u b/core/lib/Drupal/Core/Config/CachedStorage.php b/core/lib/Drupal/Core/Config/CachedStorage.php --- b/core/lib/Drupal/Core/Config/CachedStorage.php +++ b/core/lib/Drupal/Core/Config/CachedStorage.php @@ -59,7 +59,7 @@ $this->storage = $storage; $this->cacheFactory = $cache_factory; $collection = $this->getCollectionName(); - if (empty($collection)) { + if ($collection == StorageInterface::DEFAULT_COLLECTION) { $bin = 'config'; } else { diff -u b/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php --- b/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -746,7 +746,7 @@ protected function processConfiguration($collection, $op, $name) { try { $processed = FALSE; - if ($this->storageComparer->supportsConfigurationEntities($collection)) { + if ($this->configManager->supportsConfigurationEntities($collection)) { $processed = $this->importInvokeOwner($collection, $op, $name); } if (!$processed) { diff -u b/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php --- b/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -135,10 +135,23 @@ * {@inheritdoc} */ public function createSnapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) { + // Empty the snapshot of all configuration. $snapshot_storage->deleteAll(); + foreach ($snapshot_storage->getAllCollectionNames() as $collection) { + $snapshot_collection = $snapshot_storage->createCollection($collection); + $snapshot_collection->deleteAll(); + } foreach ($source_storage->listAll() as $name) { $snapshot_storage->write($name, $source_storage->read($name)); } + // Copy collections as well. + foreach ($source_storage->getAllCollectionNames() as $collection) { + $source_collection = $source_storage->createCollection($collection); + $snapshot_collection = $snapshot_storage->createCollection($collection); + foreach ($source_collection->listAll() as $name) { + $snapshot_collection->write($name, $source_collection->read($name)); + } + } } /** @@ -222,2 +235,8 @@ + /** + * {@inheritdoc} + */ + public function supportsConfigurationEntities($collection) { + return $collection == StorageInterface::DEFAULT_COLLECTION; + } } diff -u b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php --- b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigManagerInterface.php @@ -113,4 +113,14 @@ public function findConfigEntityDependentsAsEntities($type, array $names); + /** + * 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 -u b/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php --- b/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -42,7 +42,7 @@ * * @var string */ - protected $collection = ''; + protected $collection = StorageInterface::DEFAULT_COLLECTION; /** * Constructs a new DatabaseStorage. @@ -54,9 +54,10 @@ * @param array $options * (optional) Any additional database connection options to use in queries. * @param string $collection - * (optional) The collection to store configuration in. + * (optional) The collection to store configuration in. Defaults to the + * default collection. */ - public function __construct(Connection $connection, $table, array $options = array(), $collection = '') { + public function __construct(Connection $connection, $table, array $options = array(), $collection = StorageInterface::DEFAULT_COLLECTION) { $this->connection = $connection; $this->table = $table; $this->options = $options; @@ -315,7 +316,14 @@ * {@inheritdoc} */ public function getAllCollectionNames() { - return $this->connection->query('SELECT DISTINCT collection FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection <> \'\' ORDER by collection')->fetchCol(); + try { + return $this->connection->query('SELECT DISTINCT collection FROM {' . $this->connection->escapeTable($this->table) . '} WHERE collection <> :collection ORDER by collection', array( + ':collection' => StorageInterface::DEFAULT_COLLECTION) + )->fetchCol(); + } + catch (\Exception $e) { + return array(); + } } diff -u b/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php --- b/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -36,9 +36,10 @@ * @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. + * (optional) The collection to store configuration in. Defaults to the + * default collection. */ - public function __construct($directory, $collection = '') { + public function __construct($directory, $collection = StorageInterface::DEFAULT_COLLECTION) { $this->directory = $directory; $this->collection = $collection; } @@ -165,7 +166,7 @@ $success = drupal_unlink($this->getFilePath($name)); // If a collection is now empty remove the directory. - if ($success && !empty($this->collection)) { + if ($success && $this->collection != StorageInterface::DEFAULT_COLLECTION) { $names = $this->listAll(); if (empty($names)) { drupal_rmdir($this->getCollectionDirectory()); @@ -261,7 +262,7 @@ /** * {@inheritdoc} */ - public function getAllCollectionNames($directory = '') { + public function getAllCollectionNames() { $collections = $this->getAllCollectionNamesHelper($this->directory); sort($collections); return $collections; @@ -326,7 +327,7 @@ * The directory for the collection. */ protected function getCollectionDirectory() { - if (empty($this->collection)) { + if ($this->collection == StorageInterface::DEFAULT_COLLECTION) { $dir = $this->directory; } else { diff -u b/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php --- b/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -9,11 +9,12 @@ use Drupal\Component\Utility\String; use Drupal\Core\Config\Entity\ConfigDependencyManager; +use Drupal\Core\DependencyInjection\DependencySerialization; /** * Defines a config storage comparer. */ -class StorageComparer implements StorageComparerInterface { +class StorageComparer extends DependencySerialization implements StorageComparerInterface { /** * The source storage used to discover configuration changes. @@ -44,6 +45,13 @@ protected $targetStorages; /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface + */ + protected $configManager; + + /** * List of changes to between the source storage and the target storage. * * The list is keyed by storage collection name. @@ -95,10 +103,13 @@ * Storage object used to read configuration. * @param \Drupal\Core\Config\StorageInterface $target_storage * Storage object used to write configuration. + * @param \Drupal\Core\Config\ConfigManagerInterface $config_manager + * The configuration manager. */ - public function __construct(StorageInterface $source_storage, StorageInterface $target_storage) { + public function __construct(StorageInterface $source_storage, StorageInterface $target_storage, ConfigManagerInterface $config_manager) { $this->sourceStorage = $source_storage; $this->targetStorage = $target_storage; + $this->configManager = $config_manager; $this->changelist[StorageInterface::DEFAULT_COLLECTION] = $this->getEmptyChangelist(); } @@ -193,7 +204,7 @@ $this->addChangelistUpdate($collection); $this->addChangelistDelete($collection); // Only collections that support configuration entities can have renames. - if ($this->supportsConfigurationEntities($collection)) { + if ($this->configManager->supportsConfigurationEntities($collection)) { $this->addChangelistRename($collection); } // Only need data whilst calculating changelists. Free up the memory. @@ -385,18 +396,20 @@ 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 + $target_names = $target_storage->listAll(); + $source_names = $source_storage->listAll(); + $this->targetData[$collection] = $target_storage->readMultiple($target_names); + $this->sourceData[$collection] = $source_storage->readMultiple($source_names); + // If the collection only supports simple configuration do not use // configuration dependencies. - if ($this->supportsConfigurationEntities($collection)) { + if ($this->configManager->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(); + $this->targetNames[$collection] = $target_names; + $this->sourceNames[$collection] = $source_names; } } @@ -441,8 +454,2 @@ - /** - * {@inheritdoc} - */ - public function supportsConfigurationEntities($collection) { - return $collection == StorageInterface::DEFAULT_COLLECTION; - } } diff -u b/core/lib/Drupal/Core/Config/StorageComparerInterface.php b/core/lib/Drupal/Core/Config/StorageComparerInterface.php --- b/core/lib/Drupal/Core/Config/StorageComparerInterface.php +++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php @@ -122,20 +122,9 @@ * @param bool $include_default - * (optional) Include the default unnamed collection. Defaults to TRUE. + * (optional) Include the default 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 -u b/core/lib/Drupal/Core/Config/StorageInterface.php b/core/lib/Drupal/Core/Config/StorageInterface.php --- b/core/lib/Drupal/Core/Config/StorageInterface.php +++ b/core/lib/Drupal/Core/Config/StorageInterface.php @@ -185,7 +185,7 @@ * 'language.en.gb' should not be used. * * @return \Drupal\Core\Config\StorageInterface - * An new instance of the storage backend with the collection set. + * A new instance of the storage backend with the collection set. */ public function createCollection($collection); diff -u b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php --- b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -160,7 +160,7 @@ ); $source_list = $this->sourceStorage->listAll(); - $storage_comparer = new StorageComparer($this->sourceStorage, $this->targetStorage); + $storage_comparer = new StorageComparer($this->sourceStorage, $this->targetStorage, $this->configManager); if (empty($source_list) || !$storage_comparer->createChangelist()->hasChanges()) { $form['no_changes'] = array( '#type' => 'table', diff -u b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php --- b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigExportImportUITest.php @@ -160,6 +160,24 @@ $test2_storage->write('config_test.another_update', array('foo' => 'baz')); $test2_storage->write('config_test.another_delete', array('foo' => 'bar')); + // Create a snapshot. + $snapshot_storage = \Drupal::service('config.storage.snapshot'); + \Drupal::service('config.manager')->createSnapshot($active_storage, $snapshot_storage); + + // Ensure that the snapshot has the expected collection data before import. + $test1_snapshot = $snapshot_storage->createCollection('collection.test1'); + $data = $test1_snapshot->read('config_test.delete'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.delete in collection.test1 exists in the snapshot storage.'); + $data = $test1_snapshot->read('config_test.update'); + $this->assertEqual($data, array('foo' => 'baz'), 'The config_test.update in collection.test1 exists in the snapshot storage.'); + $this->assertFalse($test1_snapshot->read('config_test.create'), 'The config_test.create in collection.test1 does not exist in the snapshot storage.'); + $test2_snapshot = $snapshot_storage->createCollection('collection.test2'); + $data = $test2_snapshot->read('config_test.another_delete'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.another_delete in collection.test2 exists in the snapshot storage.'); + $data = $test2_snapshot->read('config_test.another_update'); + $this->assertEqual($data, array('foo' => 'baz'), 'The config_test.another_update in collection.test2 exists in the snapshot storage.'); + $this->assertFalse($test2_snapshot->read('config_test.another_create'), 'The config_test.another_create in collection.test2 does not exist in the snapshot storage.'); + // Create the tar contains the expected contect for the collections. $tar = new ArchiveTar($filename, 'gz'); $content_list = $tar->listContent(); @@ -202,13 +220,28 @@ $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.'); + $this->assertFalse($test1_storage->read('config_test.delete'), 'The config_test.delete in collection.test1 has been deleted.'); $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.'); + $this->assertFalse($test2_storage->read('config_test.another_delete'), 'The config_test.another_delete in collection.test2 has been deleted.'); + + // Ensure that the snapshot has been updated with the collection data. + $snapshot_storage = \Drupal::service('config.storage.snapshot'); + $test1_snapshot = $snapshot_storage->createCollection('collection.test1'); + $data = $test1_snapshot->read('config_test.create'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.create in collection.test1 has been created in the snapshot storage.'); + $data = $test1_snapshot->read('config_test.update'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.update in collection.test1 has been updated in the snapshot storage.'); + $this->assertFalse($test1_snapshot->read('config_test.delete'), 'The config_test.delete in collection.test1 does not exist in the snapshot storage.'); + $test2_snapshot = $snapshot_storage->createCollection('collection.test2'); + $data = $test2_snapshot->read('config_test.another_create'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.another_create in collection.test2 has been created in the snapshot storage.'); + $data = $test2_snapshot->read('config_test.another_update'); + $this->assertEqual($data, array('foo' => 'bar'), 'The config_test.another_update in collection.test2 has been updated in the snapshot storage.'); + $this->assertFalse($test2_snapshot->read('config_test.another_delete'), 'The config_test.another_delete in collection.test2 does not exist in the snapshot storage.'); } } diff -u b/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php b/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php --- b/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php +++ b/core/tests/Drupal/Tests/Core/Config/StorageComparerTest.php @@ -30,6 +30,11 @@ protected $targetStorage; /** + * @var \Drupal\Core\Config\ConfigManager|\PHPUnit_Framework_MockObject_MockObject + */ + protected $configManager; + + /** * The storage comparer to test. * * @var \Drupal\Core\Config\StorageComparer @@ -54,7 +59,8 @@ public function setUp() { $this->sourceStorage = $this->getMock('Drupal\Core\Config\StorageInterface'); $this->targetStorage = $this->getMock('Drupal\Core\Config\StorageInterface'); - $this->storageComparer = new StorageComparer($this->sourceStorage, $this->targetStorage); + $this->configManager = $this->getMock('Drupal\Core\Config\ConfigManagerInterface'); + $this->storageComparer = new StorageComparer($this->sourceStorage, $this->targetStorage, $this->configManager); } protected function getConfigData() { @@ -129,6 +135,9 @@ $this->targetStorage->expects($this->once()) ->method('getAllCollectionNames') ->will($this->returnValue(array())); + $this->configManager->expects($this->any()) + ->method('supportsConfigurationEntities') + ->will($this->returnValue(TRUE)); $this->storageComparer->createChangelist(); $this->assertEmpty($this->storageComparer->getChangelist('create')); @@ -163,6 +172,9 @@ $this->targetStorage->expects($this->once()) ->method('getAllCollectionNames') ->will($this->returnValue(array())); + $this->configManager->expects($this->any()) + ->method('supportsConfigurationEntities') + ->will($this->returnValue(TRUE)); $this->storageComparer->createChangelist(); $expected = array( @@ -202,6 +214,9 @@ $this->targetStorage->expects($this->once()) ->method('getAllCollectionNames') ->will($this->returnValue(array())); + $this->configManager->expects($this->any()) + ->method('supportsConfigurationEntities') + ->will($this->returnValue(TRUE)); $this->storageComparer->createChangelist(); $expected = array( @@ -241,6 +256,9 @@ $this->targetStorage->expects($this->once()) ->method('getAllCollectionNames') ->will($this->returnValue(array())); + $this->configManager->expects($this->any()) + ->method('supportsConfigurationEntities') + ->will($this->returnValue(TRUE)); $this->storageComparer->createChangelist(); $expected = array( only in patch2: unchanged: --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigCollectionNamesEvent.php @@ -0,0 +1,59 @@ +collections = array_merge($this->collections, $collections); + } + + /** + * Adds a name to the list of possible collections. + * + * @param string $collection + * Collection name to add. + */ + public function addCollectionName($collection) { + $this->addCollectionNames(array($collection)); + } + + /** + * Gets the list of possible collection names. + * + * @return array + * The list of possible collection names. + */ + public function getCollectionNames($include_default = TRUE) { + sort($this->collections); + $collections = array_unique($this->collections); + if ($include_default) { + array_unshift($collections, StorageInterface::DEFAULT_COLLECTION); + } + return $collections; + } + +} only in patch2: unchanged: --- a/core/lib/Drupal/Core/Config/ConfigEvents.php +++ b/core/lib/Drupal/Core/Config/ConfigEvents.php @@ -50,4 +50,11 @@ */ const IMPORT = 'config.importer.import'; + /** + * Name of event fired to discover all the possible configuration collections. + * + * @see \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() + */ + const COLLECTION_NAMES = 'config.collection_names'; + } only in patch2: unchanged: --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -88,10 +88,6 @@ public function __construct(ConfigFactoryInterface $config_factory, StorageInter * {@inheritdoc} */ public function installDefaultConfig($type, $name) { - // Get all default configuration owned by this extension. - $source_storage = $this->getSourceStorage(); - $config_to_install = $source_storage->listAll($name . '.'); - $extension_path = drupal_get_path($type, $name); // If the extension provides configuration schema clear the definitions. if (is_dir($extension_path . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY)) { @@ -100,22 +96,61 @@ public function installDefaultConfig($type, $name) { $this->typedConfig->clearCachedDefinitions(); } + // Gather all the supported collection names. + $event = new ConfigCollectionNamesEvent(); + $this->eventDispatcher->dispatch(ConfigEvents::COLLECTION_NAMES, $event); + $collections = $event->getCollectionNames(); + + $old_state = $this->configFactory->getOverrideState(); + $this->configFactory->setOverrideState(FALSE); + + // Read enabled extensions directly from configuration to avoid circular + // dependencies with ModuleHandler and ThemeHandler. + $extension_config = $this->configFactory->get('core.extension'); + $enabled_extensions = array_keys((array) $extension_config->get('module')); + $enabled_extensions += array_keys((array) $extension_config->get('theme')); + + foreach ($collections as $collection) { + $config_to_install = $this->listDefaultConfigCollection($collection, $type, $name, $enabled_extensions); + if (!empty($config_to_install)) { + $this->createConfiguration($collection, $config_to_install); + } + } + $this->configFactory->setOverrideState($old_state); + // Reset all the static caches and list caches. + $this->configFactory->reset(); + } + + /** + * Installs default configuration for a particular collection. + * + * @param string $collection + * The configuration collection to install. + * @param string $type + * The extension type; e.g., 'module' or 'theme'. + * @param string $name + * The name of the module or theme to install default configuration for. + * @param array $enabled_extensions + * A list of all the currently enabled modules and themes. + * + * @return array + * The list of configuration objects to create. + */ + protected function listDefaultConfigCollection($collection, $type, $name, array $enabled_extensions) { + // Get all default configuration owned by this extension. + $source_storage = $this->getSourceStorage($collection); + $config_to_install = $source_storage->listAll($name . '.'); + // If not installing the core base system default configuration, work out if // this extension provides default configuration for any other enabled // extensions. + $extension_path = drupal_get_path($type, $name); if ($type !== 'core' && is_dir($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY)) { - $enabled_extensions = $other_module_config = array(); - $default_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY); + $default_storage = new FileStorage($extension_path . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection); $other_module_config = array_filter($default_storage->listAll(), function ($value) use ($name) { return !preg_match('/^' . $name . '\./', $value); }); - // Read enabled extensions directly from configuration to avoid circular - // dependencies with ModuleHandler and ThemeHandler. - $extension_config = $this->configFactory->get('core.extension'); - $enabled_extensions += array_keys((array) $extension_config->get('module')); - $enabled_extensions += array_keys((array) $extension_config->get('theme')); - $other_module_config = array_filter($other_module_config, function ($config_name) use ($enabled_extensions) { $provider = Unicode::substr($config_name, 0, strpos($config_name, '.')); return in_array($provider, $enabled_extensions); @@ -124,63 +159,100 @@ public function installDefaultConfig($type, $name) { $config_to_install = array_merge($config_to_install, $other_module_config); } - if (!empty($config_to_install)) { - // Order the configuration to install in the order of dependencies. - $data = $source_storage->readMultiple($config_to_install); + return $config_to_install; + } + + /** + * Creates configuration in a collection based on the provided list. + * + * @param string $collection + * The configuration collection. + * @param array $config_to_install + * A list of configuration object names to create. + */ + protected function createConfiguration($collection, array $config_to_install) { + // Order the configuration to install in the order of dependencies. + $data = $this->getSourceStorage($collection)->readMultiple($config_to_install); + $config_entity_support = $this->configManager->supportsConfigurationEntities($collection); + if ($config_entity_support) { $dependency_manager = new ConfigDependencyManager(); - $sorted_config = $dependency_manager + $config_to_install = $dependency_manager ->setData($data) ->sortAll(); + } - $old_state = $this->configFactory->getOverrideState(); - $this->configFactory->setOverrideState(FALSE); + // Remove configuration that already exists in the active storage. + $config_to_install = array_diff($config_to_install, $this->getActiveStorage($collection)->listAll()); - // Remove configuration that already exists in the active storage. - $sorted_config = array_diff($sorted_config, $this->activeStorage->listAll()); + foreach ($config_to_install as $name) { + $new_config = new Config($name, $this->getActiveStorage($collection), $this->eventDispatcher, $this->typedConfig); + if ($data[$name] !== FALSE) { + $new_config->setData($data[$name]); + } + if ($config_entity_support && $entity_type = $this->configManager->getEntityTypeIdByName($name)) { - 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]); + // If we are syncing do not create configuration entities. Pluggable + // configuration entities can have dependencies on modules that are + // not yet enabled. This approach means that any code that expects + // default configuration entities to exist will be unstable after the + // module has been enabled and before the config entity has been + // imported. + if ($this->isSyncing) { + continue; } - if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { - - // If we are syncing do not create configuration entities. Pluggable - // configuration entities can have dependencies on modules that are - // not yet enabled. This approach means that any code that expects - // default configuration entities to exist will be unstable after the - // module has been enabled and before the config entity has been - // imported. - if ($this->isSyncing) { - continue; - } - $entity_storage = $this->configManager - ->getEntityManager() - ->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()); - $entity = $entity_storage->load($id); - foreach ($new_config->get() as $property => $value) { - $entity->set($property, $value); - } - $entity->save(); - } - else { - $entity_storage - ->create($new_config->get()) - ->save(); + $entity_storage = $this->configManager + ->getEntityManager() + ->getStorage($entity_type); + // It is possible that secondary writes can occur during configuration + // creation. Updates of such configuration are allowed. + if ($this->getActiveStorage($collection)->exists($name)) { + $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix()); + $entity = $entity_storage->load($id); + foreach ($new_config->get() as $property => $value) { + $entity->set($property, $value); } + $entity->save(); } else { - $new_config->save(); + $entity_storage + ->create($new_config->get()) + ->save(); } } + else { + $new_config->save(); + } + } + } + + /** + * Installs all default configuration in a particular collection. + * + * The function is useful if the site needs to respond to an event that has + * just created another collection and we need to check all the installed + * extensions for any matching configuration. For example, if a language has + * just been created. + * + * @param string $collection + * The configuration collection. + */ + public function installCollectionDefaultConfig($collection) { + $config_to_install = $this->getSourceStorage($collection)->listAll(); + $extension_config = $this->configFactory->get('core.extension'); + $enabled_extensions = array_keys((array) $extension_config->get('module')); + $enabled_extensions += array_keys((array) $extension_config->get('theme')); + $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); + }); + if (!empty($config_to_install)) { + $old_state = $this->configFactory->getOverrideState(); + $this->configFactory->setOverrideState(FALSE); + $this->createConfiguration($collection, $config_to_install); $this->configFactory->setOverrideState($old_state); + // Reset all the static caches and list caches. + $this->configFactory->reset(); } - // Reset all the static caches and list caches. - $this->configFactory->reset(); } /** @@ -202,19 +274,43 @@ public function resetSourceStorage() { /** * Gets the configuration storage that provides the default configuration. * + * @param string $collection + * (optional) The configuration collection. Defaults to the default + * collection. + * * @return \Drupal\Core\Config\StorageInterface * The configuration storage that provides the default configuration. */ - 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, InstallStorage::CONFIG_INSTALL_DIRECTORY, $collection); + } + if ($this->sourceStorage->getCollectionName() != $collection) { + $this->sourceStorage = $this->sourceStorage->createCollection($collection); } return $this->sourceStorage; } /** + * Gets the configuration storage that provides the active configuration. + * + * @param string $collection + * (optional) The configuration collection. Defaults to the default + * collection. + * + * @return \Drupal\Core\Config\StorageInterface + * The configuration storage that provides the default configuration. + */ + protected function getActiveStorage($collection = StorageInterface::DEFAULT_COLLECTION) { + if ($this->activeStorage->getCollectionName() != $collection) { + $this->activeStorage = $this->activeStorage->createCollection($collection); + } + return $this->activeStorage; + } + + /** * {@inheritdoc} */ public function setSyncing($status) { only in patch2: unchanged: --- a/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php +++ b/core/lib/Drupal/Core/Config/ExtensionInstallStorage.php @@ -30,11 +30,26 @@ class ExtensionInstallStorage extends InstallStorage { * themes is stored. * @param string $directory * The directory to scan in each extension to scan for files. Defaults to - * 'config'. + * 'config/install'. + * @param string $collection + * (optional) The collection to store configuration in. Defaults to the + * default collection. */ - public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY) { + public function __construct(StorageInterface $config_storage, $directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) { $this->configStorage = $config_storage; $this->directory = $directory; + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + return new static( + $this->configStorage, + $this->directory, + $collection + ); } /** only in patch2: unchanged: --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -51,10 +51,14 @@ class InstallStorage extends FileStorage { * * @param string $directory * The directory to scan in each extension to scan for files. Defaults to - * 'config'. + * 'config/install'. + * @param string $collection + * (optional) The collection to store configuration in. Defaults to the + * default collection. */ - public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY) { + public function __construct($directory = self::CONFIG_INSTALL_DIRECTORY, $collection = StorageInterface::DEFAULT_COLLECTION) { $this->directory = $directory; + $this->collection = $collection; } /** @@ -198,7 +202,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(); } /** only in patch2: unchanged: --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php @@ -111,7 +111,8 @@ public function testInstallUninstall() { // Ensure that we have no configuration changes to import. $storage_comparer = new StorageComparer( $this->container->get('config.storage.staging'), - $this->container->get('config.storage') + $this->container->get('config.storage'), + $this->container->get('config.manager') ); $this->assertIdentical($storage_comparer->createChangelist()->getChangelist(), $storage_comparer->getEmptyChangelist()); } only in patch2: unchanged: --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php @@ -49,7 +49,8 @@ public function setUp() { // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( $this->container->get('config.storage.staging'), - $this->container->get('config.storage') + $this->container->get('config.storage'), + $this->container->get('config.manager') ); $this->configImporter = new ConfigImporter( $storage_comparer->createChangelist(), only in patch2: unchanged: --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameValidationTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRenameValidationTest.php @@ -56,7 +56,8 @@ public function setUp() { // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( $this->container->get('config.storage.staging'), - $this->container->get('config.storage') + $this->container->get('config.storage'), + $this->container->get('config.manager') ); $this->configImporter = new ConfigImporter( $storage_comparer->createChangelist(), only in patch2: unchanged: --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -54,7 +54,8 @@ function setUp() { // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( $this->container->get('config.storage.staging'), - $this->container->get('config.storage') + $this->container->get('config.storage'), + $this->container->get('config.manager') ); $this->configImporter = new ConfigImporter( $storage_comparer->createChangelist(), only in patch2: unchanged: --- a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php @@ -11,6 +11,8 @@ /** * Tests installation of configuration objects in installation functionality. + * + * @see \Drupal\Core\Config\ConfigInstaller */ class ConfigInstallTest extends DrupalUnitTestBase { public static function getInfo() { @@ -78,6 +80,92 @@ function testModuleInstallation() { \Drupal::config('core.extension')->set('module', array())->save(); \Drupal::service('config.manager')->uninstall('module', 'config_test'); $this->assertFalse(\Drupal::service('config.typed')->hasConfigSchema('config_schema_test.schema_in_install'), 'Configuration schema for config_schema_test.schema_in_install does not exist.'); + } + /** + * Tests that collections are ignored if the event does not return anything. + */ + public function testCollectionInstallationNoCollections() { + // Install the test module. + $this->enableModules(array('config_collection_install_test')); + $this->installConfig(array('config_collection_install_test')); + /** @var \Drupal\Core\Config\StorageInterface $active_storage */ + $active_storage = \Drupal::service('config.storage'); + $this->assertEqual(array(), $active_storage->getAllCollectionNames()); } + + /** + * Tests config objects in collections are installed as expected. + */ + public function testCollectionInstallationCollections() { + $collections = array( + 'another_collection', + 'collection.test1', + 'collection.test2', + ); + // Set the event listener to return three possible collections. + // @see \Drupal\config_collection_install_test\EventSubscriber + \Drupal::state()->set('config_collection_install_test.collection_names', $collections); + // Install the test module. + $this->enableModules(array('config_collection_install_test')); + $this->installConfig(array('config_collection_install_test')); + /** @var \Drupal\Core\Config\StorageInterface $active_storage */ + $active_storage = \Drupal::service('config.storage'); + $this->assertEqual($collections, $active_storage->getAllCollectionNames()); + foreach ($collections as $collection) { + $collection_storage = $active_storage->createCollection($collection); + $data = $collection_storage->read('config_collection_install_test.test'); + $this->assertEqual($collection, $data['collection']); + } + + // Test that the we can use the config installer to install all the + // available default configuration in a particular collection for enabled + // extensions. + \Drupal::service('config.installer')->installCollectionDefaultConfig('entity'); + // The 'entity' collection will not exist because the 'config_test' module + // is not enabled. + $this->assertEqual($collections, $active_storage->getAllCollectionNames()); + // Enable the 'config_test' module and try again. + $this->enableModules(array('config_test')); + \Drupal::service('config.installer')->installCollectionDefaultConfig('entity'); + $collections[] = 'entity'; + $this->assertEqual($collections, $active_storage->getAllCollectionNames()); + $collection_storage = $active_storage->createCollection('entity'); + $data = $collection_storage->read('config_test.dynamic.dotted.default'); + $this->assertIdentical(array('label' => 'entity'), $data); + } + + /** + * Tests collections which do not support config entities install correctly. + * + * Config entity detection during config installation is done by matching + * config name prefixes. If a collection provides a configuration with a + * matching name but does not support config entities it should be created + * using simple configuration. + */ + public function testCollectionInstallationCollectionConfigEntity() { + $collections = array( + 'entity', + ); + \Drupal::state()->set('config_collection_install_test.collection_names', $collections); + // Install the test module. + $this->enableModules(array('config_test', 'config_collection_install_test')); + $this->installConfig(array('config_test')); + /** @var \Drupal\Core\Config\StorageInterface $active_storage */ + $active_storage = \Drupal::service('config.storage'); + $this->assertEqual($collections, $active_storage->getAllCollectionNames()); + $collection_storage = $active_storage->createCollection('entity'); + + // The config_test.dynamic.dotted.default configuraton object saved in the + // active store should be a configuration entity complete with UUID. Because + // the entity collection does not support configuration entities the + // configuration object stored there with the same name should only contain + // a label. + $name = 'config_test.dynamic.dotted.default'; + $data = $active_storage->read($name); + $this->assertTrue(isset($data['uuid'])); + $data = $collection_storage->read($name); + $this->assertIdentical(array('label' => 'entity'), $data); + } + } only in patch2: unchanged: --- a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php @@ -45,12 +45,13 @@ function testSnapshot() { $active = $this->container->get('config.storage'); $staging = $this->container->get('config.storage.staging'); $snapshot = $this->container->get('config.storage.snapshot'); + $config_manager = $this->container->get('config.manager'); $config_name = 'config_test.system'; $config_key = 'foo'; $new_data = 'foobar'; - $active_snapshot_comparer = new StorageComparer($active, $snapshot); - $staging_snapshot_comparer = new StorageComparer($staging, $snapshot); + $active_snapshot_comparer = new StorageComparer($active, $snapshot, $config_manager); + $staging_snapshot_comparer = new StorageComparer($staging, $snapshot, $config_manager); // Verify that we have an initial snapshot that matches the active // configuration. This has to be true as no config should be installed. only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/tests/config_collection_install_test/config/install/another_collection/config_collection_install_test.test.yml @@ -0,0 +1 @@ +collection: another_collection \ No newline at end of file only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/tests/config_collection_install_test/config/install/collection/test1/config_collection_install_test.test.yml @@ -0,0 +1 @@ +collection: collection.test1 \ No newline at end of file only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/tests/config_collection_install_test/config/install/collection/test2/config_collection_install_test.test.yml @@ -0,0 +1 @@ +collection: collection.test2 \ No newline at end of file only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/tests/config_collection_install_test/config/install/entity/config_test.dynamic.dotted.default.yml @@ -0,0 +1 @@ +label: entity only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/tests/config_collection_install_test/config_collection_install_test.info.yml @@ -0,0 +1,5 @@ +name: 'Configuration events test' +type: module +package: Testing +version: VERSION +core: 8.x only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/tests/config_collection_install_test/config_collection_install_test.services.yml @@ -0,0 +1,6 @@ +services: + config_events_test.event_subscriber: + class: Drupal\config_collection_install_test\EventSubscriber + arguments: ['@state'] + tags: + - { name: event_subscriber } only in patch2: unchanged: --- /dev/null +++ b/core/modules/config/tests/config_collection_install_test/lib/Drupal/config_collection_install_test/EventSubscriber.php @@ -0,0 +1,52 @@ +state = $state; + } + + /** + * Reacts to the ConfigEvents::COLLECTION_NAMES event. + * + * @param \Drupal\Core\Config\ConfigCollectionNamesEvent $event + * The configuration collection names event. + */ + public function addCollectionNames(ConfigCollectionNamesEvent $event) { + $event->addCollectionNames($this->state->get('config_collection_install_test.collection_names', array())); + } + + /** + * {@inheritdoc} + */ + static function getSubscribedEvents() { + $events[ConfigEvents::COLLECTION_NAMES][] = array('addCollectionNames'); + return $events; + } + +} only in patch2: unchanged: --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1481,7 +1481,8 @@ public function configImporter() { // Set up the ConfigImporter object for testing. $storage_comparer = new StorageComparer( $this->container->get('config.storage.staging'), - $this->container->get('config.storage') + $this->container->get('config.storage'), + $this->container->get('config.manager') ); $this->configImporter = new ConfigImporter( $storage_comparer,