Commit 4e451f65 authored by Dries's avatar Dries

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

parent 809b361f
......@@ -8,12 +8,12 @@
namespace Drupal\Core\Config;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheFactoryInterface;
/**
* Defines the cached storage.
*
* The class gets another storage and a cache backend injected. It reads from
* The class gets another storage and the cache factory injected. It reads from
* the cache and delegates the read to the storage on a cache miss. It also
* handles cache invalidation.
*/
......@@ -26,6 +26,13 @@ class CachedStorage implements StorageInterface, StorageCacheInterface {
*/
protected $storage;
/**
* The cache factory.
*
* @var \Drupal\Core\Cache\CacheFactoryInterface
*/
protected $cacheFactory;
/**
* The instantiated Cache backend.
*
......@@ -45,12 +52,20 @@ class CachedStorage implements StorageInterface, StorageCacheInterface {
*
* @param \Drupal\Core\Config\StorageInterface $storage
* A configuration storage to be cached.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* A cache backend instance to use for caching.
* @param \Drupal\Core\Cache\CacheFactoryInterface $cache_factory
* A cache factory used for getting cache backends.
*/
public function __construct(StorageInterface $storage, CacheBackendInterface $cache) {
public function __construct(StorageInterface $storage, CacheFactoryInterface $cache_factory) {
$this->storage = $storage;
$this->cache = $cache;
$this->cacheFactory = $cache_factory;
$collection = $this->getCollectionName();
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
$bin = 'config';
}
else {
$bin = 'config_' . str_replace('.', '_', $collection);
}
$this->cache = $this->cacheFactory->get($bin);
}
/**
......@@ -238,4 +253,29 @@ public function deleteAll($prefix = '') {
public function resetListCache() {
$this->findByPrefixCache = array();
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static(
$this->storage->createCollection($collection),
$this->cacheFactory
);
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
return $this->storage->getAllCollectionNames();
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->storage->getCollectionName();
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigCollectionNamesEvent.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a configuration event for event listeners.
*/
class ConfigCollectionNamesEvent extends Event {
/**
* Configuration collection names.
*
* @var array
*/
protected $collections = array();
/**
* Adds names to the list of possible collections.
*
* @param array $collections
* Collection names to add.
*/
public function addCollectionNames(array $collections) {
$this->collections = array_merge($this->collections, $collections);
}
/**
* Adds a name to the list of possible collections.
*
* @param string $collection
* Collection name to add.
*/
public function addCollectionName($collection) {
$this->addCollectionNames(array($collection));
}
/**
* Gets the list of possible collection names.
*
* @return array
* The list of possible collection names.
*/
public function getCollectionNames($include_default = TRUE) {
sort($this->collections);
$collections = array_unique($this->collections);
if ($include_default) {
array_unshift($collections, StorageInterface::DEFAULT_COLLECTION);
}
return $collections;
}
}
......@@ -50,4 +50,11 @@ final class ConfigEvents {
*/
const IMPORT = 'config.importer.import';
/**
* Name of event fired to discover all the possible configuration collections.
*
* @see \Drupal\Core\Config\ConfigInstaller::installDefaultConfig()
*/
const COLLECTION_NAMES = 'config.collection_names';
}
......@@ -37,6 +37,19 @@ interface ConfigInstallerInterface {
*/
public function installDefaultConfig($type, $name);
/**
* Installs all default configuration in the specified 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);
/**
* Sets the configuration storage that provides the default configuration.
*
......
......@@ -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;
}
......@@ -131,10 +135,23 @@ public function diff(StorageInterface $source_storage, StorageInterface $target_
* {@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));
}
}
}
/**
......@@ -156,6 +173,13 @@ public function uninstall($type, $name) {
foreach ($config_names as $config_name) {
$this->configFactory->get($config_name)->delete();
}
// Remove any matching configuration from collections.
foreach ($this->activeStorage->getAllCollectionNames() as $collection) {
$collection_storage = $this->activeStorage->createCollection($collection);
$collection_storage->deleteAll($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
......@@ -216,4 +240,10 @@ public function findConfigEntityDependentsAsEntities($type, array $names) {
return $entities_to_return;
}
/**
* {@inheritdoc}
*/
public function supportsConfigurationEntities($collection) {
return $collection == StorageInterface::DEFAULT_COLLECTION;
}
}
......@@ -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.
......@@ -109,5 +112,15 @@ public function findConfigEntityDependents($type, array $names);
*/
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);
}
......@@ -37,6 +37,13 @@ class DatabaseStorage implements StorageInterface {
*/
protected $options = array();
/**
* The storage collection.
*
* @var string
*/
protected $collection = StorageInterface::DEFAULT_COLLECTION;
/**
* Constructs a new DatabaseStorage.
*
......@@ -46,11 +53,15 @@ 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. Defaults to the
* default collection.
*/
public function __construct(Connection $connection, $table, array $options = array()) {
public function __construct(Connection $connection, $table, array $options = array(), $collection = StorageInterface::DEFAULT_COLLECTION) {
$this->connection = $connection;
$this->table = $table;
$this->options = $options;
$this->collection = $collection;
}
/**
......@@ -58,7 +69,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 +87,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 +105,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 +148,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 +188,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 +209,7 @@ protected static function schemaDefinition() {
'size' => 'big',
),
),
'primary key' => array('name'),
'primary key' => array('collection', 'name'),
);
return $schema;
}
......@@ -205,6 +224,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 +240,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 +267,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 +285,46 @@ 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() {
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();
}
}
}
......@@ -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
);
}
/**
......
......@@ -16,6 +16,13 @@
*/
class FileStorage implements StorageInterface {
/**
* The storage collection.
*
* @var string
*/
protected $collection;
/**
* The filesystem path for configuration objects.
*
......@@ -28,9 +35,13 @@ 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. Defaults to the
* default collection.
*/
public function __construct($directory) {
public function __construct($directory, $collection = StorageInterface::DEFAULT_COLLECTION) {
$this->directory = $directory;
$this->collection = $collection;
}
/**
......@@ -40,7 +51,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 +68,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,8 +157,9 @@ 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;
}
......@@ -186,12 +202,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) {
......@@ -212,7 +229,110 @@ public function deleteAll($prefix = '') {
$success = FALSE;
}
}
if ($success && $this->collection != StorageInterface::DEFAULT_COLLECTION) {
// Remove empty directories.
if (!(new \FilesystemIterator($this->getCollectionDirectory()))->valid()) {
drupal_rmdir($this->getCollectionDirectory());
}
}
return $success;
}
/**
* {@inheritdoc}
*/
public function createCollection($collection) {
return new static(
$this->directory,
$collection
);
}
/**
* {@inheritdoc}
*/
public function getCollectionName() {
return $this->collection;
}
/**
* {@inheritdoc}
*/
public function getAllCollectionNames() {
$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;
}
}
// Check that the collection is valid by searching if for configuration
// objects. A directory without any configuration objects is not a valid
// collection.
// \GlobIterator on Windows requires an absolute path.
$files = new \GlobIterator(realpath($directory . '/' . $collection) . '/*.' . $this->getFileExtension());
if (count($files)) {
$collections[] = $collection;
}
}
}
return $collections;
}
/**
* Gets the directory for the collection.
*
* @return string
* The directory for the collection.
*/
protected function getCollectionDirectory() {
if ($this->collection == StorageInterface::DEFAULT_COLLECTION) {
$dir = $this->directory;
}
else {
$dir = $this->directory . '/' . str_replace('.', '/', $this->collection);
}
return $dir;
}
}
......@@ -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();
}
/**
......
......@@ -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}