Commit 4b5fad54 authored by catch's avatar catch

Issue #1702080 by sun, chx: Added Introduce canonical FileStorage + (regular) cache.

parent 4f37cf02
......@@ -2413,47 +2413,62 @@ function drupal_get_bootstrap_phase() {
*
* @see Drupal\Core\DrupalKernel
*
* @param $reset
* A new container instance to reset the Drupal container to.
* @param Symfony\Component\DependencyInjection\Container $new_container
* A new container instance to replace the current.
* @param bool $rebuild
* (optional) Internal use only. Whether to enforce a rebuild of the container.
* Used by the testing framework to inject a fresh container for unit tests.
*
* @return Symfony\Component\DependencyInjection\Container
* The instance of the Container used to set up and maintain object
* instances.
*/
function drupal_container(Container $reset = NULL) {
function drupal_container(Container $new_container = NULL, $rebuild = FALSE) {
// We do not use drupal_static() here because we do not have a mechanism by
// which to reinitialize the stored objects, so a drupal_static_reset() call
// would leave Drupal in a nonfunctional state.
static $container = NULL;
if (isset($reset)) {
$container = $reset;
if ($rebuild) {
$container = NULL;
}
if (isset($new_container)) {
$container = $new_container;
}
elseif (!isset($container)) {
if (!isset($container)) {
// Return a ContainerBuilder instance with the bare essentials needed for any
// full bootstrap regardless of whether there will be a DrupalKernel involved.
// This will get merged with the full Kernel-built Container on normal page
// requests.
$container = new ContainerBuilder();
// Register configuration storage class and options.
// @todo The active store and its options need to be configurable.
// Use either global $conf (recursion warning) or global $config, or a
// bootstrap configuration *file* to allow to set/override this very
// lowest of low level configuration.
$container->setParameter('config.storage.options', array(
'connection' => 'default',
'target' => 'default',
));
$container->register('config.storage', 'Drupal\Core\Config\DatabaseStorage')
->addArgument('%config.storage.options%');
// Register active configuration storage.
$container
->register('config.cachedstorage.storage', 'Drupal\Core\Config\FileStorage')
->addArgument(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
// @todo Replace this with a cache.factory service plus 'config' argument.
$container
->register('cache.config')
->setFactoryClass('Drupal\Core\Cache\CacheFactory')
->setFactoryMethod('get')
->addArgument('config');
$container
->register('config.storage', 'Drupal\Core\Config\CachedStorage')
->addArgument(new Reference('config.cachedstorage.storage'))
->addArgument(new Reference('cache.config'));
// Register configuration object factory.
$container->register('config.subscriber.globalconf', 'Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber');
$container->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher')
->addMethodCall('addSubscriber', array(new Reference('config.subscriber.globalconf')));
// Register configuration object factory.
$container->register('config.factory', 'Drupal\Core\Config\ConfigFactory')
->addArgument(new Reference('config.storage'))
->addArgument(new Reference('dispatcher'));
// Register staging configuration storage.
$container
->register('config.storage.staging', 'Drupal\Core\Config\FileStorage')
->addArgument(config_get_config_directory(CONFIG_STAGING_DIRECTORY));
}
return $container;
}
......
......@@ -5,6 +5,8 @@
* Functions and interfaces for cache handling.
*/
use Drupal\Core\Cache\CacheFactory;
/**
* Instantiates and statically caches the correct class for a cache bin.
*
......@@ -32,9 +34,7 @@ function cache($bin = 'cache') {
// storage of a cache bin mid-request.
static $cache_objects;
if (!isset($cache_objects[$bin])) {
$cache_backends = cache_get_backends();
$class = isset($cache_backends[$bin]) ? $cache_backends[$bin] : $cache_backends['cache'];
$cache_objects[$bin] = new $class($bin);
$cache_objects[$bin] = CacheFactory::get($bin);
}
return $cache_objects[$bin];
}
......@@ -53,17 +53,7 @@ function cache($bin = 'cache') {
* The list of tags to invalidate cache items for.
*/
function cache_invalidate(array $tags) {
foreach (cache_get_backends() as $bin => $class) {
foreach (CacheFactory::getBackends() as $bin => $class) {
cache($bin)->invalidateTags($tags);
}
}
/**
* Returns a list of cache backends for this site.
*
* @return
* An associative array with cache bins as keys, and backend classes as value.
*/
function cache_get_backends() {
return variable_get('cache_classes', array('cache' => 'Drupal\Core\Cache\DatabaseBackend'));
}
......@@ -17,14 +17,11 @@
* The extension type; e.g., 'module' or 'theme'.
* @param string $name
* The name of the module or theme to install default configuration for.
*
* @todo Make this acknowledge other storage engines rather than having
* SQL be hardcoded.
*/
function config_install_default_config($type, $name) {
$config_dir = drupal_get_path($type, $name) . '/config';
if (is_dir($config_dir)) {
$source_storage = new FileStorage(array('directory' => $config_dir));
$source_storage = new FileStorage($config_dir);
$target_storage = drupal_container()->get('config.storage');
$null_storage = new NullStorage();
......@@ -43,7 +40,9 @@ function config_install_default_config($type, $name) {
}
/**
* @todo Modules need a way to access the active store, whatever it is.
* Gets configuration object names starting with a given prefix.
*
* @see Drupal\Core\Config\StorageInterface::listAll()
*/
function config_get_storage_names_with_prefix($prefix = '') {
return drupal_container()->get('config.storage')->listAll($prefix);
......@@ -137,7 +136,7 @@ function config_sync_changes(array $config_changes, StorageInterface $source_sto
*/
function config_import() {
// Retrieve a list of differences between staging and the active store.
$source_storage = new FileStorage(array('directory' => config_get_config_directory(CONFIG_STAGING_DIRECTORY)));
$source_storage = drupal_container()->get('config.storage.staging');
$target_storage = drupal_container()->get('config.storage');
$config_changes = config_sync_get_changes($source_storage, $target_storage);
......@@ -219,7 +218,7 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou
function config_export() {
// Retrieve a list of differences between the active store and staging.
$source_storage = drupal_container()->get('config.storage');
$target_storage = new FileStorage(array('directory' => config_get_config_directory(CONFIG_STAGING_DIRECTORY)));
$target_storage = drupal_container()->get('config.storage.staging');
$config_changes = config_sync_get_changes($source_storage, $target_storage);
if (empty($config_changes)) {
......
......@@ -5,6 +5,8 @@
use Drupal\Core\Database\Install\TaskException;
use Drupal\Core\Language\Language;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
......@@ -272,6 +274,46 @@ function install_begin_request(&$install_state) {
include_once DRUPAL_ROOT . '/core/includes/module.inc';
include_once DRUPAL_ROOT . '/core/includes/session.inc';
// Determine whether the configuration system is ready to operate.
$install_state['config_verified'] = install_verify_config_directory(CONFIG_ACTIVE_DIRECTORY) && install_verify_config_directory(CONFIG_STAGING_DIRECTORY);
// If it is not, replace the configuration storage with the InstallStorage
// implementation, for the following reasons:
// - The first call into drupal_container() will try to set up the regular
// runtime configuration storage, using the CachedStorage by default. It
// calls config_get_config_directory() to retrieve the config directory to
// use, but that throws an exception, since $config_directories is not
// defined since there is no settings.php yet. If there is a prepared
// settings.php already, then the returned directory still cannot be used,
// because it does not necessarily exist. The installer ensures that it
// exists and is writeable in a later step.
// - The installer outputs maintenance theme pages and performs many other
// operations, which try to load configuration. Since there is no active
// configuration yet, and because the configuration system does not have a
// notion of default values at runtime, data is missing in many places. The
// lack of data does not trigger errors, but results in a broken user
// interface (e.g., missing page title, etc).
// - The actual configuration data to read during installation is essentially
// the default configuration provided by the installation profile and
// modules (most notably System module). The InstallStorage therefore reads
// from the default configuration directories of extensions.
// This override is reverted as soon as the config directory has been set up
// successfully.
// @see drupal_install_config_directories()
if (!$install_state['config_verified']) {
// @todo Move into a proper Drupal\Core\DependencyInjection\InstallContainerBuilder.
$container = new ContainerBuilder();
$container->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher');
$container->register('config.storage', 'Drupal\Core\Config\InstallStorage');
$container->register('config.factory', 'Drupal\Core\Config\ConfigFactory')
->addArgument(new Reference('config.storage'))
->addArgument(new Reference('dispatcher'));
drupal_container($container);
}
// Set up $language, so t() caller functions will still work.
drupal_language_initialize();
......@@ -316,7 +358,6 @@ function install_begin_request(&$install_state) {
// Check existing settings.php.
$install_state['database_verified'] = install_verify_database_settings();
$install_state['config_verified'] = install_verify_config_directory(CONFIG_ACTIVE_DIRECTORY) && install_verify_config_directory(CONFIG_STAGING_DIRECTORY);
$install_state['settings_verified'] = $install_state['config_verified'] && $install_state['database_verified'];
if ($install_state['database_verified']) {
......@@ -1054,6 +1095,11 @@ function install_settings_form_submit($form, &$form_state) {
// Add the config directories to settings.php.
drupal_install_config_directories();
// We have valid configuration directories in settings.php.
// Reset the service container, so the config.storage service will use the
// actual active storage for installing configuration.
drupal_container(NULL, TRUE);
// Indicate that the settings file has been verified, and check the database
// for the last completed task, now that we have a valid connection. This
// last step is important since we want to trigger an error if the new
......
......@@ -275,7 +275,7 @@ function drupal_install_config_directories() {
drupal_rewrite_settings($settings);
}
// Ensure that the config directory exists or can be created, and is writable.
// Ensure the config directories exist or can be created, and are writable.
foreach (array(CONFIG_ACTIVE_DIRECTORY, CONFIG_STAGING_DIRECTORY) as $config_type) {
// This should never fail, since if the config directory was specified in
// settings.php it will have already been created and verified earlier, and
......@@ -296,7 +296,7 @@ function drupal_install_config_directories() {
* Checks whether a config directory name is defined, and if so, whether it
* exists and is writable.
*
* This partically duplicates install_ensure_config_directory(), but is required
* This partially duplicates install_ensure_config_directory(), but is required
* since the installer would create the config directory too early in the
* installation process otherwise (e.g., when only visiting install.php when
* there is a settings.php already, but not actually executing the installation).
......@@ -313,9 +313,16 @@ function install_verify_config_directory($type) {
if (!isset($config_directories[$type])) {
return FALSE;
}
$config_directory = config_get_config_directory($type);
if (is_dir($config_directory) && is_writable($config_directory)) {
return TRUE;
// config_get_config_directory() throws an exception when the passed $type
// does not exist in $config_directories. This can happen if there is a
// prepared settings.php that defines $config_directories already.
try {
$config_directory = config_get_config_directory($type);
if (is_dir($config_directory) && is_writable($config_directory)) {
return TRUE;
}
}
catch (\Exception $e) {
}
return FALSE;
}
......
......@@ -6,7 +6,6 @@
*/
use Drupal\Component\Graph\Graph;
use Drupal\Core\Config\DatabaseStorage;
/**
* Loads all the modules that have been enabled in the system table.
......@@ -617,7 +616,7 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE)
$module_list = array_keys($module_list);
}
$storage = new DatabaseStorage();
$storage = drupal_container()->get('config.storage');
foreach ($module_list as $module) {
// Uninstall the module.
module_load_install($module);
......
......@@ -1056,7 +1056,7 @@ function update_variables_to_config($config_name, array $variable_map) {
$module = strtok($config_name, '.');
// Load and set default configuration values.
$file = new FileStorage(array('directory' => drupal_get_path('module', $module) . '/config'));
$file = new FileStorage(drupal_get_path('module', $module) . '/config');
if (!$file->exists($config_name)) {
throw new ConfigException("Default configuration file $config_name for $module extension not found but is required to exist.");
}
......
<?php
/**
* @file
* Contains Drupal\Core\Cache\CacheFactory.
*/
namespace Drupal\Core\Cache;
/**
* Defines the cache backend factory.
*/
class CacheFactory {
/**
* Instantiates a cache backend class for a given cache bin.
*
* By default, this returns an instance of the
* Drupal\Core\Cache\DatabaseBackend class.
*
* Classes implementing Drupal\Core\Cache\CacheBackendInterface can register
* themselves both as a default implementation and for specific bins.
*
* @param string $bin
* The cache bin for which a cache backend object should be returned.
*
* @return Drupal\Core\Cache\CacheBackendInterface
* The cache backend object associated with the specified bin.
*/
public static function get($bin) {
// Check whether there is a custom class defined for the requested bin or
// use the default 'cache' definition otherwise.
$cache_backends = self::getBackends();
$class = isset($cache_backends[$bin]) ? $cache_backends[$bin] : $cache_backends['cache'];
return new $class($bin);
}
/**
* Returns a list of cache backends for this site.
*
* @return array
* An associative array with cache bins as keys, and backend class names as
* value.
*/
public static function getBackends() {
// @todo Improve how cache backend classes are defined. Cannot be
// configuration, since e.g. the CachedStorage config storage controller
// requires the definition in its constructor already.
global $conf;
$cache_backends = isset($conf['cache_classes']) ? $conf['cache_classes'] : array();
// Ensure there is a default 'cache' bin definition.
$cache_backends += array('cache' => 'Drupal\Core\Cache\DatabaseBackend');
return $cache_backends;
}
}
<?php
/**
* @file
* Contains Drupal\Core\Config\CachedStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Cache\CacheBackendInterface;
/**
* Defines the cached storage controller.
*
* The class gets another storage and a cache backend injected. It reads from
* the cache and delegates the read to the storage on a cache miss. It also
* handles cache invalidation.
*/
class CachedStorage implements StorageInterface {
/**
* The configuration storage to be cached.
*
* @var Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* The instantiated Cache backend.
*
* @var Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* Constructs a new CachedStorage controller.
*
* @param Drupal\Core\Config\StorageInterface $storage
* A configuration storage controller to be cached.
* @param Drupal\Core\Cache\CacheBackendInterface $cache
* A cache backend instance to use for caching.
*/
public function __construct(StorageInterface $storage, CacheBackendInterface $cache) {
$this->storage = $storage;
$this->cache = $cache;
}
/**
* Implements Drupal\Core\Config\StorageInterface::exists().
*/
public function exists($name) {
// The cache would read in the entire data (instead of only checking whether
// any data exists), and on a potential cache miss, an additional storage
// lookup would have to happen, so check the storage directly.
return $this->storage->exists($name);
}
/**
* Implements Drupal\Core\Config\StorageInterface::read().
*/
public function read($name) {
if ($cache = $this->cache->get($name)) {
// The cache backend supports primitive data types, but only an array
// represents valid config object data.
if (is_array($cache->data)) {
return $cache->data;
}
}
// Read from the storage on a cache miss and cache the data, if any.
$data = $this->storage->read($name);
if ($data !== FALSE) {
$this->cache->set($name, $data, CacheBackendInterface::CACHE_PERMANENT, array('config' => array($name)));
}
// If the cache contained bogus data and there is no data in the storage,
// wipe the cache entry.
elseif ($cache) {
$this->cache->delete($name);
}
return $data;
}
/**
* Implements Drupal\Core\Config\StorageInterface::write().
*/
public function write($name, array $data) {
if ($this->storage->write($name, $data)) {
// While not all written data is read back, setting the cache instead of
// just deleting it avoids cache rebuild stampedes.
$this->cache->set($name, $data, CacheBackendInterface::CACHE_PERMANENT, array('config' => array($name)));
return TRUE;
}
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::delete().
*/
public function delete($name) {
// If the cache was the first to be deleted, another process might start
// rebuilding the cache before the storage is gone.
if ($this->storage->delete($name)) {
$this->cache->delete($name);
return TRUE;
}
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::rename().
*/
public function rename($name, $new_name) {
// If the cache was the first to be deleted, another process might start
// rebuilding the cache before the storage is renamed.
if ($this->storage->rename($name, $new_name)) {
$this->cache->delete($name);
$this->cache->delete($new_name);
return TRUE;
}
return FALSE;
}
/**
* Implements Drupal\Core\Config\StorageInterface::encode().
*/
public function encode($data) {
return $this->storage->encode($data);
}
/**
* Implements Drupal\Core\Config\StorageInterface::decode().
*/
public function decode($raw) {
return $this->storage->decode($raw);
}
/**
* Implements Drupal\Core\Config\StorageInterface::listAll().
*
* Not supported by CacheBackendInterface.
*/
public function listAll($prefix = '') {
return $this->storage->listAll($prefix);
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
use Exception;
/**
......@@ -16,31 +17,49 @@
class DatabaseStorage implements StorageInterface {
/**
* Database connection options for this storage controller.
* The database connection.
*
* - connection: The connection key to use.
* - target: The target on the connection to use.
* @var Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The database table name.
*
* @var string
*/
protected $table;
/**
* Additional database connection options to use in queries.
*
* @var array
*/
protected $options;
protected $options = array();
/**
* Implements Drupal\Core\Config\StorageInterface::__construct().
* Constructs a new DatabaseStorage controller.
*
* @param Drupal\Core\Database\Connection $connection
* A Database connection to use for reading and writing configuration data.
* @param string $table
* A database table name to store configuration data in.
* @param array $options
* (optional) Any additional database connection options to use in queries.
*/
public function __construct(array $options = array()) {
$options += array(
'connection' => 'default',
'target' => 'default',
);
public function __construct(Connection $connection, $table, array $options = array()) {
$this->connection = $connection;
$this->table = $table;
$this->options = $options;
}
/**
* Returns the database connection to use.
* Implements Drupal\Core\Config\StorageInterface::exists().
*/
protected function getConnection() {
return Database::getConnection($this->options['target'], $this->options['connection']);
public function exists($name) {
return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', 0, 1, array(
':name' => $name,
), $this->options)->fetchField();
}
/**
......@@ -56,10 +75,8 @@ public function read($name) {
// read without actually having the database available. In this case,
// catch the exception and just return an empty array so the caller can
// handle it if need be.
// @todo Remove this and use appropriate config.storage service definition
// in the installer instead.
try {
$raw = $this->getConnection()->query('SELECT data FROM {config} WHERE name = :name', array(':name' => $name), $this->options)->fetchField();
$raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', array(':name' => $name), $this->options)->fetchField();
if ($raw !== FALSE) {
$data = $this->decode($raw);
}
......@@ -79,7 +96,7 @@ public function read($name) {
public function write($name, array $data) {
$data = $this->encode($data);
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->getConnection()->merge('config', $options)
return (bool) $this->connection->merge($this->table, $options)
->key(array('name' => $name))
->fields(array('data' => $data))
->execute();
......@@ -94,7 +111,7 @@ public function write($name, array $data) {
*/
public function delete($name) {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->getConnection()->delete('config', $options)
return (bool) $this->connection->delete($this->table, $options)
->condition('name', $name)
->execute();
}
......@@ -107,7 +124,7 @@ public function delete($name) {
*/
public function rename($name, $new_name) {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->getConnection()->update('config', $options)
return (bool) $this->connection->update($this->table, $options)
->fields(array('name' => $new_name))
->condition('name', $name)
->execute();
......@@ -139,7 +156,7 @@ public function decode($raw) {
* Only thrown in case $this->options['throw_exception'] is TRUE.
*/
public function listAll($prefix = '') {
return $this->getConnection()->query('SELECT name FROM {config} WHERE name LIKE :name', array(
return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name', array(
':name' => db_like($prefix) . '%',
), $this->options)->fetchCol();
}
......
......@@ -16,13 +16,11 @@
class FileStorage implements StorageInterface {
/**
* Configuration options for this storage controller.
* The filesystem path for configuration objects.
*
* - directory: The filesystem path for configuration objects.
*
* @var array
* @var string
*/
protected $options;
protected $directory = '';
/**
* A shared YAML dumper instance.
......@@ -39,13 +37,13 @@ class FileStorage implements StorageInterface {
protected $parser;
/**
* Implements Drupal\Core\Config\StorageInterface::__construct().
* Constructs a new FileStorage controller.
*
* @param string $directory
* A directory path to use for reading and writing of configuration files.
*/
public function __construct(array $options = array()) {
if (!isset($options['directory'])) {
$options['directory'] = config_get_config_directory();
}
$this->options = $options;
public function __construct($directory) {
$this->directory = $directory;
}
/**
......@@ -55,7 +53,7 @@ public function __construct(array $options = array()) {
* The path to the configuration file.
*/
public function getFilePath($name) {
return $this->options['directory'] . '/' . $name . '.' . self::getFileExtension();
return $this->directory . '/' . $name . '.' . self::getFileExtension();
}
/**
......@@ -114,8 +112,8 @@ public function write($name, array $data) {
*/
public function delete($name) {
if (!$this->exists($name)) {
if (!file_exists($this->options['directory'])) {
throw new StorageException($this->options['directory'] . '/ not found.');
if (!file_exists($this->directory)) {
throw new StorageException($this->directory . '/ not found.');
}
return FALSE;
}
......@@ -188,11 +186,11 @@ 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->options['directory'])) {
throw new StorageException($this->options['directory'] . '/ not found.');
if (!file_exists($this->directory)) {
throw new StorageException($this->directory . '/ not found.');
}
$extension = '.' . self::getFileExtension();
$files = glob($this->options['directory'] . '/' . $prefix . '*' . $extension);
$files = glob($this->directory . '/' . $prefix . '*' . $extension);
$clean_name = function ($value) use ($extension) {
return basename($value, $extension);
};
......
<?php
/**
* @file
* Contains Drupal\Core\Config\InstallStorage.
*/
namespace Drupal\Core\Config;
/**
* Storage controller used by the Drupal installer.
*
* @see install_begin_request()
*/
class InstallStorage extends FileStorage {
/**
* Overrides Drupal\Core\Config\FileStorage::__construct().
*/
public function __construct() {
}
/**
* Overrides Drupal\Core\Config\FileStorage::getFilePath().
*