Commit 8df23c8a authored by Dries's avatar Dries

- Patch #1605324 by sun, Jose Reyero, alexpott, chx, Rob Loach, beejeebus:...

- Patch #1605324 by sun, Jose Reyero, alexpott, chx, Rob Loach, beejeebus: configuration system cleanup and rearchitecture.
parent 48a0b693
......@@ -288,8 +288,6 @@
*/
const DRUPAL_PHP_FUNCTION_PATTERN = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*';
require_once DRUPAL_ROOT . '/core/includes/config.inc';
/**
* Starts the timer with the specified name.
*
......@@ -479,6 +477,25 @@ function find_conf_path($http_host, $script_name, $require_settings = TRUE) {
return $conf;
}
/**
* Returns the path of the configuration directory.
*
* @return string
* The configuration directory path.
*/
function config_get_config_directory() {
global $config_directory_name;
if ($test_prefix = drupal_valid_test_ua()) {
// @see Drupal\simpletest\WebTestBase::setUp()
$path = conf_path() . '/files/simpletest/' . substr($test_prefix, 10) . '/config';
}
else {
$path = conf_path() . '/files/' . $config_directory_name;
}
return $path;
}
/**
* Sets appropriate server variables needed for command line scripts to work.
*
......@@ -2265,6 +2282,9 @@ function _drupal_bootstrap_configuration() {
// Activate the class loader.
drupal_classloader();
// Load the procedural configuration system helper functions.
require_once DRUPAL_ROOT . '/core/includes/config.inc';
}
/**
......
......@@ -8,25 +8,6 @@
* This is the API for configuration storage.
*/
/**
* Gets the randomly generated config directory name.
*
* @return
* The directory name.
*/
function config_get_config_directory() {
global $config_directory_name;
if ($test_prefix = drupal_valid_test_ua()) {
// @see Drupal\simpletest\WebTestBase::setUp()
$path = conf_path() . '/files/simpletest/' . substr($test_prefix, 10) . '/config';
}
else {
$path = conf_path() . '/files/' . $config_directory_name;
}
return $path;
}
/**
* Installs the default configuration of a given module.
*
......@@ -38,28 +19,23 @@ function config_get_config_directory() {
*/
function config_install_default_config($module) {
$module_config_dir = drupal_get_path('module', $module) . '/config';
$drupal_config_dir = config_get_config_directory();
if (is_dir(drupal_get_path('module', $module) . '/config')) {
$files = glob($module_config_dir . '/*.' . FileStorage::getFileExtension());
foreach ($files as $key => $file) {
// Load config data into the active store and write it out to the
// file system in the drupal config directory. Note the config name
// needs to be the same as the file name WITHOUT the extension.
$config_name = basename($file, '.' . FileStorage::getFileExtension());
if (is_dir($module_config_dir)) {
$database_storage = new DatabaseStorage();
$module_file_storage = new FileStorage(array('directory' => $module_config_dir));
$database_storage = new DatabaseStorage($config_name);
$file_storage = new FileStorage($config_name);
$file_storage->setPath($module_config_dir);
$database_storage->write($file_storage->read());
foreach ($module_file_storage->listAll() as $config_name) {
$data = $module_file_storage->read($config_name);
$database_storage->write($config_name, $data);
}
}
}
/**
* @todo http://drupal.org/node/1552396 renames this into config_load_all().
* @todo Modules need a way to access the active store, whatever it is.
*/
function config_get_storage_names_with_prefix($prefix = '') {
return DatabaseStorage::getNamesWithPrefix($prefix);
$storage = new DatabaseStorage();
return $storage->listAll($prefix);
}
/**
......@@ -73,16 +49,11 @@ function config_get_storage_names_with_prefix($prefix = '') {
* The name of the configuration object to retrieve. The name corresponds to
* a configuration file. For @code config(book.admin) @endcode, the config
* object returned will contain the contents of book.admin configuration file.
* @param $class
* The class name of the config object to be returned. Defaults to
* DrupalConfig.
*
* @return
* An instance of the class specified in the $class parameter.
*
* @todo Replace this with an appropriate factory / ability to inject in
* alternate storage engines..
* @return Drupal\Core\Config\Config
* A configuration object.
*/
function config($name, $class = 'Drupal\Core\Config\DrupalConfig') {
return new $class(new DatabaseStorage($name));
function config($name) {
return drupal_container()->get('config.factory')->get($name)->load();
}
<?php
use Drupal\Core\Database\Database;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\DatabaseStorage;
/**
* Indicates that a module has not been installed yet.
......@@ -376,24 +376,17 @@ function drupal_uninstall_modules($module_list = array(), $uninstall_dependents
$module_list = array_keys($module_list);
}
$storage = new DatabaseStorage();
foreach ($module_list as $module) {
// Uninstall the module.
module_load_install($module);
module_invoke($module, 'uninstall');
drupal_uninstall_schema($module);
// Remove any stray configuration settings.
// Get the names of default configurations provided by this module
// by scanning its config directory.
$module_config_dir = drupal_get_path('module', $module) . '/config';
if (is_dir($module_config_dir)) {
$files = glob($module_config_dir . '/*.' . FileStorage::getFileExtension());
foreach ($files as $file) {
$config_name = basename($file, '.' . FileStorage::getFileExtension());
$file_storage = new FileStorage($config_name);
// Delete the configuration from storage.
$file_storage->delete();
}
// Remove all configuration belonging to the module.
$config_names = $storage->listAll($module . '.');
foreach ($config_names as $config_name) {
config($config_name)->delete();
}
watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO);
......
......@@ -487,7 +487,7 @@ function module_enable($module_list, $enable_dependencies = TRUE) {
$versions = drupal_get_schema_versions($module);
$version = $versions ? max($versions) : SCHEMA_INSTALLED;
// Copy any default configuration data to the system config directory/
// Install default configuration of the module.
config_install_default_config($module);
// If the module has no current updates, but has some that were
......
......@@ -10,6 +10,7 @@
use Drupal\Component\Graph\Graph;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\ConfigException;
/**
* Minimum schema version of Drupal 7 required for upgrade to Drupal 8.
......@@ -942,11 +943,11 @@ function update_variables_to_config($config_name, array $variable_map) {
$module = strtok($config_name, '.');
// Load and set default configuration values.
// Throws a FileStorageReadException if there is no default configuration
// file, which is required to exist.
$file = new FileStorage($config_name);
$file->setPath(drupal_get_path('module', $module) . '/config');
$default_data = $file->read();
$file = new FileStorage(array('directory' => 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.");
}
$default_data = $file->read($config_name);
// Merge any possibly existing original data into default values.
// Only relevant when being called repetitively on the same config object.
......
<?php
/**
* @file
* Definition of Drupal\Core\Config\ConfigException.
*/
namespace Drupal\Core\Config;
use RuntimeException;
/**
* @todo
* A base exception thrown in any configuration system operations.
*/
class ConfigException extends \Exception {}
class ConfigException extends RuntimeException {}
<?php
/**
* @file
* Definition of Drupal\Core\Config\DatabaseStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Config\StorageBase;
use Drupal\Core\Database\Database;
use Exception;
/**
* Represents an SQL-based configuration storage object.
* Defines the Database storage controller.
*/
class DatabaseStorage extends StorageBase {
class DatabaseStorage implements StorageInterface {
/**
* Database connection options for this storage controller.
*
* - connection: The connection key to use.
* - target: The target on the connection to use.
*
* @var array
*/
protected $options;
/**
* Implements Drupal\Core\Config\StorageInterface::__construct().
*/
public function __construct(array $options = array()) {
$options += array(
'connection' => 'default',
'target' => 'default',
);
$this->options = $options;
}
/**
* Returns the database connection to use.
*/
protected function getConnection() {
return Database::getConnection($this->options['target'], $this->options['connection']);
}
/**
* Implements StorageInterface::read().
* Implements Drupal\Core\Config\StorageInterface::read().
*
* @throws PDOException
* @throws Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
*/
public function read() {
public function read($name) {
$data = array();
// There are situations, like in the installer, where we may attempt a
// 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.
$data = array();
// @todo Remove this and use appropriate StorageDispatcher configuration in
// the installer instead.
try {
$raw = db_query('SELECT data FROM {config} WHERE name = :name', array(':name' => $this->name))->fetchField();
$raw = $this->getConnection()->query('SELECT data FROM {config} WHERE name = :name', array(':name' => $name), $this->options)->fetchField();
if ($raw !== FALSE) {
$data = $this->decode($raw);
}
......@@ -31,43 +70,63 @@ public function read() {
}
/**
* Implements StorageInterface::writeToActive().
* Implements Drupal\Core\Config\StorageInterface::write().
*
* @throws PDOException
*
* @todo Ignore slave targets for data manipulation operations.
*/
public function writeToActive($data) {
public function write($name, array $data) {
$data = $this->encode($data);
return db_merge('config')
->key(array('name' => $this->name))
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->getConnection()->merge('config', $options)
->key(array('name' => $name))
->fields(array('data' => $data))
->execute();
}
/**
* @todo
* Implements Drupal\Core\Config\StorageInterface::delete().
*
* @throws PDOException
*
* @todo Ignore slave targets for data manipulation operations.
*/
public function deleteFromActive() {
db_delete('config')
->condition('name', $this->name)
public function delete($name) {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->getConnection()->delete('config', $options)
->condition('name', $name)
->execute();
}
/**
* Implements StorageInterface::encode().
* Implements Drupal\Core\Config\StorageInterface::encode().
*/
public static function encode($data) {
return serialize($data);
}
/**
* Implements StorageInterface::decode().
* Implements Drupal\Core\Config\StorageInterface::decode().
*
* @throws ErrorException
* unserialize() triggers E_NOTICE if the string cannot be unserialized.
*/
public static function decode($raw) {
return unserialize($raw);
$data = @unserialize($raw);
return $data !== FALSE ? $data : array();
}
/**
* Implements StorageInterface::getNamesWithPrefix().
* Implements Drupal\Core\Config\StorageInterface::listAll().
*
* @throws PDOException
* @throws Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
*/
static public function getNamesWithPrefix($prefix = '') {
return db_query('SELECT name FROM {config} WHERE name LIKE :name', array(':name' => db_like($prefix) . '%'))->fetchCol();
public function listAll($prefix = '') {
return $this->getConnection()->query('SELECT name FROM {config} WHERE name LIKE :name', array(
':name' => db_like($prefix) . '%',
), $this->options)->fetchCol();
}
}
<?php
namespace Drupal\Core\Config;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\ConfigException;
/**
* Represents the default configuration storage object.
*/
class DrupalConfig {
/**
* The storage engine to save this config object to.
*
* @var StorageInterface
*/
protected $storage;
/**
* The data of the configuration object.
*
* @var array
*/
protected $data = array();
/**
* Constructs a DrupalConfig object.
*
* @param StorageInterface $storage
* The storage engine where this config object should be saved.
*
* @todo $this should really know about $name and make it publicly accessible.
*/
public function __construct(StorageInterface $storage) {
$this->storage = $storage;
$this->read();
}
/**
* Reads config data from the active store into our object.
*/
public function read() {
$data = $this->storage->read();
$this->setData($data !== FALSE ? $data : array());
return $this;
}
/**
* Checks whether a particular value is overridden.
*
* @param $key
* @todo
*
* @return
* @todo
*/
public function isOverridden($key) {
return isset($this->_overrides[$key]);
}
/**
* Gets data from this config object.
*
* @param $key
* A string that maps to a key within the configuration data.
* For instance in the following configuation array:
* @code
* array(
* 'foo' => array(
* 'bar' => 'baz',
* ),
* );
* @endcode
* A key of 'foo.bar' would return the string 'baz'. However, a key of 'foo'
* would return array('bar' => 'baz').
* If no key is specified, then the entire data array is returned.
*
* The configuration system does not retain data types. Every saved value is
* casted to a string. In most cases this is not an issue; however, it can
* cause issues with Booleans, which are casted to "1" (TRUE) or "0" (FALSE).
* In particular, code relying on === or !== will no longer function properly.
*
* @see http://php.net/manual/language.operators.comparison.php.
*
* @return
* The data that was requested.
*/
public function get($key = '') {
global $conf;
$name = $this->storage->getName();
if (isset($conf[$name])) {
$merged_data = drupal_array_merge_deep($this->data, $conf[$name]);
}
else {
$merged_data = $this->data;
}
if (empty($key)) {
return $merged_data;
}
else {
$parts = explode('.', $key);
if (count($parts) == 1) {
return isset($merged_data[$key]) ? $merged_data[$key] : NULL;
}
else {
$key_exists = NULL;
$value = drupal_array_get_nested_value($merged_data, $parts, $key_exists);
return $key_exists ? $value : NULL;
}
}
}
/**
* Replaces the data of this configuration object.
*
* @param array $data
* The new configuration data.
*/
public function setData(array $data) {
$this->data = $data;
return $this;
}
/**
* Sets value in this config object.
*
* @param $key
* @todo
* @param $value
* @todo
*/
public function set($key, $value) {
// Type-cast value into a string.
$value = $this->castValue($value);
// The dot/period is a reserved character; it may appear between keys, but
// not within keys.
$parts = explode('.', $key);
if (count($parts) == 1) {
$this->data[$key] = $value;
}
else {
drupal_array_set_nested_value($this->data, $parts, $value);
}
return $this;
}
/**
* Casts a saved value to a string.
*
* The configuration system only saves strings or arrays. Any scalar
* non-string value is cast to a string. The one exception is boolean FALSE
* which would normally become '' when cast to a string, but is manually
* cast to '0' here for convenience and consistency.
*
* Any non-scalar value that is not an array (aka objects) gets cast
* to an array.
*
* @param $value
* A value being saved into the configuration system.
* @param $value
* The value cast to a string or array.
*/
public function castValue($value) {
if (is_scalar($value)) {
// Handle special case of FALSE, which should be '0' instead of ''.
if ($value === FALSE) {
$value = '0';
}
else {
$value = (string) $value;
}
}
else {
// Any non-scalar value must be an array.
if (!is_array($value)) {
$value = (array) $value;
}
// Recurse into any nested keys.
foreach ($value as $key => $nested_value) {
$value[$key] = $this->castValue($nested_value);
}
}
return $value;
}
/**
* Unsets value in this config object.
*
* @param $key
* Name of the key whose value should be unset.
*/
public function clear($key) {
$parts = explode('.', $key);
if (count($parts) == 1) {
unset($this->data[$key]);
}
else {
drupal_array_unset_nested_value($this->data, $parts);
}
}
/**
* Saves the configuration object.
*/
public function save() {
$this->storage->write($this->data);
}
/**
* Deletes the configuration object.
*/
public function delete() {
$this->data = array();
$this->storage->delete();
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Config\FileStorage.
*/
namespace Drupal\Core\Config;
use Symfony\Component\Yaml\Yaml;
/**
* Represents the file storage controller.
*
* @todo Implement StorageInterface after removing DrupalConfig methods.
* @todo Consider to extend StorageBase.
* Defines the file storage controller.
*/
class FileStorage {
class FileStorage implements StorageInterface {
/**
* The name of the configuration object.
* Configuration options for this storage controller.
*
* @var string
*/
protected $name;
/**
* The filesystem path containing the configuration object.
* - directory: The filesystem path for configuration objects.
*
* @var string
* @var array
*/
protected $path;
protected $options;
/**
* Implements StorageInterface::__construct().
*/
public function __construct($name = NULL) {
$this->name = $name;
}
/**
* Returns the path containing the configuration file.
*
* @return string
* The relative path to the configuration object.
* Implements Drupal\Core\Config\StorageInterface::__construct().
*/
public function getPath() {
// If the path has not been set yet, retrieve and assign the default path
// for configuration files.
if (!isset($this->path)) {
$this->setPath(config_get_config_directory());
public function __construct(array $options = array()) {
if (!isset($options['directory'])) {
$options['directory'] = config_get_config_directory();
}
return $this->path;
}
/**
* Sets the path containing the configuration file.
*/
public function setPath($directory) {
$this->path = $directory;
return $this;
$this->options = $options;
}
/**
......@@ -62,8 +39,8 @@ public function setPath($directory) {
* @return string
* The path to the configuration file.
*/
public function getFilePath() {
return $this->getPath() . '/' . $this->getName() . '.' . self::getFileExtension();
public function getFilePath($name) {
return $this->options['directory'] . '/' . $name . '.' . self::getFileExtension();
}
/**
......@@ -82,50 +59,65 @@ public static function getFileExtension() {
* @return bool
* TRUE if the configuration file exists, FALSE otherwise.
*/
protected function exists() {
return file_exists($this->getFilePath());
public function exists($name) {
return file_exists($this->getFilePath($name));
}
/**
* Implements StorageInterface::write().
* Implements Drupal\Core\Config\StorageInterface::read().
*
* @throws FileStorageException
* @throws Symfony\Component\Yaml\Exception\ParseException
*/
public function write($data) {
$data = $this->encode($data);
if (!file_put_contents($this->getFilePath(), $data)) {
throw new FileStorageException('Failed to write configuration file: ' . $this->getFilePath());
public function read($name) {
if (!$this->exists($name)) {
return array();
}
$data = file_get_contents($this->getFilePath($name));
// @todo Yaml throws a ParseException on invalid data. Is it expected to be
// caught or not?
$data = $this->decode($data);
if ($data === FALSE) {
return array();
}
// A simple string is valid YAML for any reason.
if (!is_array($data)) {
return array();
}
return $data;
}
/**
* Implements StorageInterface::read().
* Implements Drupal\Core\Config\StorageInterface::write().
*
* @throws FileStorageReadException
* @throws Symfony\Component\Yaml\Exception\DumpException
* @throws Drupal\Core\Config\StorageException
*/
public function read() {
if (!$this->exists()) {
throw new FileStorageReadException("Configuration file '$this->name' does not exist.");
}