Commit abc7e15f authored by Dries's avatar Dries

Issue #2161591 by pwolanin, beejeebus, sun: Change default active config from...

Issue #2161591 by pwolanin, beejeebus, sun: Change default active config from file storage to DB storage.
parent 23b38b72
...@@ -74,16 +74,9 @@ services: ...@@ -74,16 +74,9 @@ services:
factory_method: get factory_method: get
factory_service: cache_factory factory_service: cache_factory
arguments: [discovery] arguments: [discovery]
config.cachedstorage.storage:
class: Drupal\Core\Config\FileStorage
factory_class: Drupal\Core\Config\FileStorageFactory
factory_method: getActive
config.manager: config.manager:
class: Drupal\Core\Config\ConfigManager class: Drupal\Core\Config\ConfigManager
arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage'] arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage']
config.storage:
class: Drupal\Core\Config\CachedStorage
arguments: ['@config.cachedstorage.storage', '@cache.config']
config.factory: config.factory:
class: Drupal\Core\Config\ConfigFactory class: Drupal\Core\Config\ConfigFactory
tags: tags:
...@@ -92,6 +85,15 @@ services: ...@@ -92,6 +85,15 @@ services:
config.installer: config.installer:
class: Drupal\Core\Config\ConfigInstaller class: Drupal\Core\Config\ConfigInstaller
arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher'] arguments: ['@config.factory', '@config.storage', '@config.typed', '@config.manager', '@event_dispatcher']
config.storage:
alias: config.storage.active
config.storage.active:
class: Drupal\Core\Config\DatabaseStorage
arguments: ['@database', 'config']
config.storage.file:
class: Drupal\Core\Config\FileStorage
factory_class: Drupal\Core\Config\FileStorageFactory
factory_method: getActive
config.storage.staging: config.storage.staging:
class: Drupal\Core\Config\FileStorage class: Drupal\Core\Config\FileStorage
factory_class: Drupal\Core\Config\FileStorageFactory factory_class: Drupal\Core\Config\FileStorageFactory
......
...@@ -438,7 +438,7 @@ function install_begin_request(&$install_state) { ...@@ -438,7 +438,7 @@ function install_begin_request(&$install_state) {
// Ensure that the active configuration directory is empty before installation // Ensure that the active configuration directory is empty before installation
// starts. // starts.
if ($install_state['config_verified'] && empty($task)) { if ($install_state['config_verified'] && empty($task)) {
$config = glob(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY) . '/*.' . FileStorage::getFileExtension()); $config = \Drupal::service('config.storage')->listAll();
if (!empty($config)) { if (!empty($config)) {
$task = NULL; $task = NULL;
throw new AlreadyInstalledException($container->get('string_translation')); throw new AlreadyInstalledException($container->get('string_translation'));
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Core\Database\Database;
use Drupal\Component\Utility\Settings; use Drupal\Component\Utility\Settings;
/** /**
...@@ -21,13 +22,29 @@ class BootstrapConfigStorageFactory { ...@@ -21,13 +22,29 @@ class BootstrapConfigStorageFactory {
* A configuration storage implementation. * A configuration storage implementation.
*/ */
public static function get() { public static function get() {
$drupal_bootstrap_config_storage = Settings::get('drupal_bootstrap_config_storage'); $bootstrap_config_storage = Settings::get('bootstrap_config_storage');
if ($drupal_bootstrap_config_storage && is_callable($drupal_bootstrap_config_storage)) { if (!empty($bootstrap_config_storage) && is_callable($bootstrap_config_storage)) {
return call_user_func($drupal_bootstrap_config_storage); return call_user_func($bootstrap_config_storage);
}
else {
return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
} }
// Fallback to the DatabaseStorage.
return self::getDatabaseStorage();
}
/**
* Returns a Database configuration storage implementation.
*
* @return \Drupal\Core\Config\DatabaseStorage
*/
public static function getDatabaseStorage() {
return new DatabaseStorage(Database::getConnection(), 'config');
} }
/**
* Returns a File-based configuration storage implementation.
*
* @return \Drupal\Core\Config\FileStorage
*/
public static function getFileStorage() {
return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
}
} }
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/** /**
* @file * @file
* Definition of Drupal\Core\Config\Config. * Contains \Drupal\Core\Config\Config.
*/ */
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
...@@ -215,6 +215,11 @@ public function save() { ...@@ -215,6 +215,11 @@ public function save() {
$this->data[$key] = $this->castValue($key, $value); $this->data[$key] = $this->castValue($key, $value);
} }
} }
else {
foreach ($this->data as $key => $value) {
$this->validateValue($key, $value);
}
}
$this->storage->write($this->name, $this->data); $this->storage->write($this->name, $this->data);
$this->isNew = FALSE; $this->isNew = FALSE;
......
...@@ -2,13 +2,14 @@ ...@@ -2,13 +2,14 @@
/** /**
* @file * @file
* Definition of Drupal\Core\Config\DatabaseStorage. * Contains \Drupal\Core\Config\DatabaseStorage.
*/ */
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Core\Database\Database; use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection; use Drupal\Core\Database\Connection;
use Drupal\Core\Database\SchemaObjectExistsException;
/** /**
* Defines the Database storage. * Defines the Database storage.
...@@ -56,24 +57,23 @@ public function __construct(Connection $connection, $table, array $options = arr ...@@ -56,24 +57,23 @@ public function __construct(Connection $connection, $table, array $options = arr
* Implements Drupal\Core\Config\StorageInterface::exists(). * Implements Drupal\Core\Config\StorageInterface::exists().
*/ */
public function exists($name) { public function exists($name) {
return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', 0, 1, array( try {
':name' => $name, return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', 0, 1, array(
), $this->options)->fetchField(); ':name' => $name,
), $this->options)->fetchField();
}
catch (\Exception $e) {
// If we attempt a read without actually having the database or the table
// available, just return FALSE so the caller can handle it.
return FALSE;
}
} }
/** /**
* Implements Drupal\Core\Config\StorageInterface::read(). * {@inheritdoc}
*
* @throws PDOException
* @throws \Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
*/ */
public function read($name) { public function read($name) {
$data = FALSE; $data = FALSE;
// 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.
try { 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 name = :name', array(':name' => $name), $this->options)->fetchField();
if ($raw !== FALSE) { if ($raw !== FALSE) {
...@@ -81,6 +81,8 @@ public function read($name) { ...@@ -81,6 +81,8 @@ public function read($name) {
} }
} }
catch (\Exception $e) { catch (\Exception $e) {
// If we attempt a read without actually having the database or the table
// available, just return FALSE so the caller can handle it.
} }
return $data; return $data;
} }
...@@ -89,10 +91,6 @@ public function read($name) { ...@@ -89,10 +91,6 @@ public function read($name) {
* {@inheritdoc} * {@inheritdoc}
*/ */
public function readMultiple(array $names) { public function readMultiple(array $names) {
// 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.
$list = array(); $list = array();
try { 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 name IN (:names)', array(':names' => $names), $this->options)->fetchAllKeyed();
...@@ -100,20 +98,42 @@ public function readMultiple(array $names) { ...@@ -100,20 +98,42 @@ public function readMultiple(array $names) {
$data = $this->decode($data); $data = $this->decode($data);
} }
} }
catch (Exception $e) { catch (\Exception $e) {
// If we attempt a read without actually having the database or the table
// available, just return an empty array so the caller can handle it.
} }
return $list; return $list;
} }
/** /**
* Implements Drupal\Core\Config\StorageInterface::write(). * {@inheritdoc}
*
* @throws PDOException
*
* @todo Ignore slave targets for data manipulation operations.
*/ */
public function write($name, array $data) { public function write($name, array $data) {
$data = $this->encode($data); $data = $this->encode($data);
try {
return $this->doWrite($name, $data);
}
catch (\Exception $e) {
// If there was an exception, try to create the table.
if ($this->ensureTableExists()) {
return $this->doWrite($name, $data);
}
// Some other failure that we can not recover from.
throw $e;
}
}
/**
* Helper method so we can re-try a write.
*
* @param string $name
* The config name.
* @param string $data
* The config data, already dumped to a string.
*
* @return bool
*/
protected function doWrite($name, $data) {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options; $options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->connection->merge($this->table, $options) return (bool) $this->connection->merge($this->table, $options)
->key('name', $name) ->key('name', $name)
...@@ -121,6 +141,60 @@ public function write($name, array $data) { ...@@ -121,6 +141,60 @@ public function write($name, array $data) {
->execute(); ->execute();
} }
/**
* Check if the config table exists and create it if not.
*
* @return bool
* TRUE if the table was created, FALSE otherwise.
*
* @throws \Drupal\Core\Config\StorageException
* If a database error occurs.
*/
protected function ensureTableExists() {
try {
if (!$this->connection->schema()->tableExists($this->table)) {
$this->connection->schema()->createTable($this->table, static::schemaDefinition());
return TRUE;
}
}
// If another process has already created the config table, attempting to
// recreate it will throw an exception. In this case just catch the
// exception and do nothing.
catch (SchemaObjectExistsException $e) {
return TRUE;
}
catch (\Exception $e) {
throw new StorageException($e->getMessage(), NULL, $e);
}
return FALSE;
}
/**
* Defines the schema for the configuration table.
*/
protected static function schemaDefinition() {
$schema = array(
'description' => 'The base table for configuration data.',
'fields' => array(
'name' => array(
'description' => 'Primary Key: Unique config object name.',
'type' => 'varchar',
'length' => 255,
'not null' => TRUE,
'default' => '',
),
'data' => array(
'description' => 'A serialized configuration object data.',
'type' => 'blob',
'not null' => FALSE,
'size' => 'big',
),
),
'primary key' => array('name'),
);
return $schema;
}
/** /**
* Implements Drupal\Core\Config\StorageInterface::delete(). * Implements Drupal\Core\Config\StorageInterface::delete().
* *
...@@ -168,29 +242,31 @@ public function decode($raw) { ...@@ -168,29 +242,31 @@ public function decode($raw) {
} }
/** /**
* Implements Drupal\Core\Config\StorageInterface::listAll(). * {@inheritdoc}
*
* @throws PDOException
* @throws \Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
*/ */
public function listAll($prefix = '') { public function listAll($prefix = '') {
return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name', array( try {
':name' => db_like($prefix) . '%', return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name', array(
), $this->options)->fetchCol(); ':name' => $this->connection->escapeLike($prefix) . '%',
), $this->options)->fetchCol();
}
catch (\Exception $e) {
return array();
}
} }
/** /**
* Implements Drupal\Core\Config\StorageInterface::deleteAll(). * {@inheritdoc}
*
* @throws PDOException
* @throws \Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
*/ */
public function deleteAll($prefix = '') { public function deleteAll($prefix = '') {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options; try {
return (bool) $this->connection->delete($this->table, $options) $options = array('return' => Database::RETURN_AFFECTED) + $this->options;
->condition('name', $prefix . '%', 'LIKE') return (bool) $this->connection->delete($this->table, $options)
->execute(); ->condition('name', $prefix . '%', 'LIKE')
->execute();
}
catch (\Exception $e) {
return FALSE;
}
} }
} }
...@@ -68,6 +68,18 @@ public static function getFileExtension() { ...@@ -68,6 +68,18 @@ public static function getFileExtension() {
return 'yml'; return 'yml';
} }
/**
* 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);
if (!$success) {
throw new StorageException("Failed to create config directory {$this->directory}");
}
return $this;
}
/** /**
* Implements Drupal\Core\Config\StorageInterface::exists(). * Implements Drupal\Core\Config\StorageInterface::exists().
*/ */
...@@ -105,21 +117,23 @@ public function readMultiple(array $names) { ...@@ -105,21 +117,23 @@ public function readMultiple(array $names) {
} }
/** /**
* Implements Drupal\Core\Config\StorageInterface::write(). * {@inheritdoc}
*
* @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
* @throws \Drupal\Core\Config\StorageException
*/ */
public function write($name, array $data) { public function write($name, array $data) {
try { try {
$data = $this->encode($data); $data = $this->encode($data);
} }
catch(DumpException $e) { catch(DumpException $e) {
throw new UnsupportedDataTypeConfigException(String::format('Invalid data type for used in config: @name', array('@name' => $name))); throw new StorageException(String::format('Invalid data type for used in config: @name', array('@name' => $name)));
} }
$target = $this->getFilePath($name); $target = $this->getFilePath($name);
$status = @file_put_contents($target, $data); $status = @file_put_contents($target, $data);
if ($status === FALSE) {
// Try to make sure the directory exists and try witing again.
$this->ensureStorage();
$status = @file_put_contents($target, $data);
}
if ($status === FALSE) { if ($status === FALSE) {
throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name)); throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name));
} }
...@@ -213,7 +227,7 @@ public function listAll($prefix = '') { ...@@ -213,7 +227,7 @@ public function listAll($prefix = '') {
// glob() silently ignores the error of a non-existing search directory, // glob() silently ignores the error of a non-existing search directory,
// even with the GLOB_ERR flag. // even with the GLOB_ERR flag.
if (!file_exists($this->directory)) { if (!file_exists($this->directory)) {
throw new StorageException($this->directory . '/ not found.'); return array();
} }
$extension = '.' . static::getFileExtension(); $extension = '.' . static::getFileExtension();
// \GlobIterator on Windows requires an absolute path. // \GlobIterator on Windows requires an absolute path.
......
...@@ -131,6 +131,27 @@ protected function getSchemaWrapper() { ...@@ -131,6 +131,27 @@ protected function getSchemaWrapper() {
return $this->schemaWrapper; return $this->schemaWrapper;
} }
/**
* Validate the values are allowed data types.
*
* @throws UnsupportedDataTypeConfigException
* If there is any invalid value.
*/
protected function validateValue($key, $value) {
// Minimal validation. Should not try to serialize resources or non-arrays.
if (is_array($value)) {
foreach ($value as $nested_value_key => $nested_value) {
$this->validateValue($key . '.' . $nested_value_key, $nested_value);
}
}
elseif ($value !== NULL && !is_scalar($value)) {
throw new UnsupportedDataTypeConfigException(String::format('Invalid data type for config element @name:@key', array(
'@name' => $this->getName(),
'@key' => $key,
)));
}
}
/** /**
* Casts the value to correct data type using the configuration schema. * Casts the value to correct data type using the configuration schema.
* *
......
...@@ -41,7 +41,7 @@ public function read($name); ...@@ -41,7 +41,7 @@ public function read($name);
/** /**
* Reads configuration data from the storage. * Reads configuration data from the storage.
* *
* @param array $name * @param array $names
* List of names of the configuration objects to load. * List of names of the configuration objects to load.
* *
* @return array * @return array
...@@ -60,6 +60,9 @@ public function readMultiple(array $names); ...@@ -60,6 +60,9 @@ public function readMultiple(array $names);
* *
* @return bool * @return bool
* TRUE on success, FALSE in case of an error. * TRUE on success, FALSE in case of an error.
*
* @throws \Drupal\Core\Config\StorageException
* If the back-end storage does not exist and cannot be created.
*/ */
public function write($name, array $data); public function write($name, array $data);
......
...@@ -28,11 +28,11 @@ public function register(ContainerBuilder $container) { ...@@ -28,11 +28,11 @@ public function register(ContainerBuilder $container) {
$container $container
->register('lock', 'Drupal\Core\Lock\NullLockBackend'); ->register('lock', 'Drupal\Core\Lock\NullLockBackend');
// Prevent config from accessing {cache_config}. // Prevent config from being accessed via a cache wrapper by removing
// @see $conf['cache_classes'], update_prepare_d8_bootstrap() // any existing definition and setting an alias to the actual storage.
$container $container->removeDefinition('config.storage');
->register('config.storage', 'Drupal\Core\Config\FileStorage') $container->setAlias('config.storage', 'config.storage.active');
->addArgument(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
$container->register('module_handler', 'Drupal\Core\Extension\UpdateModuleHandler') $container->register('module_handler', 'Drupal\Core\Extension\UpdateModuleHandler')
->addArgument('%container.modules%'); ->addArgument('%container.modules%');
$container $container
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\StorageInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\system\FileDownloadController; use Drupal\system\FileDownloadController;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
...@@ -81,13 +82,15 @@ public function __construct(StorageInterface $target_storage, StorageInterface $ ...@@ -81,13 +82,15 @@ public function __construct(StorageInterface $target_storage, StorageInterface $