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:
factory_method: get
factory_service: cache_factory
arguments: [discovery]
config.cachedstorage.storage:
class: Drupal\Core\Config\FileStorage
factory_class: Drupal\Core\Config\FileStorageFactory
factory_method: getActive
config.manager:
class: Drupal\Core\Config\ConfigManager
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:
class: Drupal\Core\Config\ConfigFactory
tags:
......@@ -92,6 +85,15 @@ services:
config.installer:
class: Drupal\Core\Config\ConfigInstaller
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:
class: Drupal\Core\Config\FileStorage
factory_class: Drupal\Core\Config\FileStorageFactory
......
......@@ -438,7 +438,7 @@ function install_begin_request(&$install_state) {
// Ensure that the active configuration directory is empty before installation
// starts.
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)) {
$task = NULL;
throw new AlreadyInstalledException($container->get('string_translation'));
......
......@@ -7,6 +7,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\Database\Database;
use Drupal\Component\Utility\Settings;
/**
......@@ -21,13 +22,29 @@ class BootstrapConfigStorageFactory {
* A configuration storage implementation.
*/
public static function get() {
$drupal_bootstrap_config_storage = Settings::get('drupal_bootstrap_config_storage');
if ($drupal_bootstrap_config_storage && is_callable($drupal_bootstrap_config_storage)) {
return call_user_func($drupal_bootstrap_config_storage);
}
else {
return new FileStorage(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
$bootstrap_config_storage = Settings::get('bootstrap_config_storage');
if (!empty($bootstrap_config_storage) && is_callable($bootstrap_config_storage)) {
return call_user_func($bootstrap_config_storage);
}
// 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 @@
/**
* @file
* Definition of Drupal\Core\Config\Config.
* Contains \Drupal\Core\Config\Config.
*/
namespace Drupal\Core\Config;
......@@ -215,6 +215,11 @@ public function save() {
$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->isNew = FALSE;
......
......@@ -2,13 +2,14 @@
/**
* @file
* Definition of Drupal\Core\Config\DatabaseStorage.
* Contains \Drupal\Core\Config\DatabaseStorage.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\SchemaObjectExistsException;
/**
* Defines the Database storage.
......@@ -56,24 +57,23 @@ public function __construct(Connection $connection, $table, array $options = arr
* Implements Drupal\Core\Config\StorageInterface::exists().
*/
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();
try {
return (bool) $this->connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', 0, 1, array(
':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().
*
* @throws PDOException
* @throws \Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
* {@inheritdoc}
*/
public function read($name) {
$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 {
$raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', array(':name' => $name), $this->options)->fetchField();
if ($raw !== FALSE) {
......@@ -81,6 +81,8 @@ public function read($name) {
}
}
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;
}
......@@ -89,10 +91,6 @@ public function read($name) {
* {@inheritdoc}
*/
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();
try {
$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) {
$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;
}
/**
* Implements Drupal\Core\Config\StorageInterface::write().
*
* @throws PDOException
*
* @todo Ignore slave targets for data manipulation operations.
* {@inheritdoc}
*/
public function write($name, array $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;
return (bool) $this->connection->merge($this->table, $options)
->key('name', $name)
......@@ -121,6 +141,60 @@ public function write($name, array $data) {
->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().
*
......@@ -168,29 +242,31 @@ public function decode($raw) {
}
/**
* Implements Drupal\Core\Config\StorageInterface::listAll().
*
* @throws PDOException
* @throws \Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
* {@inheritdoc}
*/
public function listAll($prefix = '') {
return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name', array(
':name' => db_like($prefix) . '%',
), $this->options)->fetchCol();
try {
return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name', array(
':name' => $this->connection->escapeLike($prefix) . '%',
), $this->options)->fetchCol();
}
catch (\Exception $e) {
return array();
}
}
/**
* Implements Drupal\Core\Config\StorageInterface::deleteAll().
*
* @throws PDOException
* @throws \Drupal\Core\Database\DatabaseExceptionWrapper
* Only thrown in case $this->options['throw_exception'] is TRUE.
* {@inheritdoc}
*/
public function deleteAll($prefix = '') {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->connection->delete($this->table, $options)
->condition('name', $prefix . '%', 'LIKE')
->execute();
try {
$options = array('return' => Database::RETURN_AFFECTED) + $this->options;
return (bool) $this->connection->delete($this->table, $options)
->condition('name', $prefix . '%', 'LIKE')
->execute();
}
catch (\Exception $e) {
return FALSE;
}
}
}
......@@ -68,6 +68,18 @@ public static function getFileExtension() {
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().
*/
......@@ -105,21 +117,23 @@ public function readMultiple(array $names) {
}
/**
* Implements Drupal\Core\Config\StorageInterface::write().
*
* @throws \Drupal\Core\Config\UnsupportedDataTypeConfigException
* @throws \Drupal\Core\Config\StorageException
* {@inheritdoc}
*/
public function write($name, array $data) {
try {
$data = $this->encode($data);
}
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);
$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) {
throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name));
}
......@@ -213,7 +227,7 @@ 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)) {
throw new StorageException($this->directory . '/ not found.');
return array();
}
$extension = '.' . static::getFileExtension();
// \GlobIterator on Windows requires an absolute path.
......
......@@ -131,6 +131,27 @@ protected function getSchemaWrapper() {
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.
*
......
......@@ -41,7 +41,7 @@ public function read($name);
/**
* Reads configuration data from the storage.
*
* @param array $name
* @param array $names
* List of names of the configuration objects to load.
*
* @return array
......@@ -60,6 +60,9 @@ public function readMultiple(array $names);
*
* @return bool
* 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);
......
......@@ -28,11 +28,11 @@ public function register(ContainerBuilder $container) {
$container
->register('lock', 'Drupal\Core\Lock\NullLockBackend');
// Prevent config from accessing {cache_config}.
// @see $conf['cache_classes'], update_prepare_d8_bootstrap()
$container
->register('config.storage', 'Drupal\Core\Config\FileStorage')
->addArgument(config_get_config_directory(CONFIG_ACTIVE_DIRECTORY));
// Prevent config from being accessed via a cache wrapper by removing
// any existing definition and setting an alias to the actual storage.
$container->removeDefinition('config.storage');
$container->setAlias('config.storage', 'config.storage.active');
$container->register('module_handler', 'Drupal\Core\Extension\UpdateModuleHandler')
->addArgument('%container.modules%');
$container
......
......@@ -12,6 +12,7 @@
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\system\FileDownloadController;
use Symfony\Component\Yaml\Dumper;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
......@@ -81,13 +82,15 @@ public function __construct(StorageInterface $target_storage, StorageInterface $
* Downloads a tarball of the site configuration.
*/
public function downloadExport() {
file_unmanaged_delete(file_directory_temp() . '/config.tar.gz');
$dumper = new Dumper();
$dumper->setIndentation(2);
$archiver = new ArchiveTar(file_directory_temp() . '/config.tar.gz', 'gz');
$config_dir = config_get_config_directory();
$config_files = array();
foreach (\Drupal::service('config.storage')->listAll() as $config_name) {
$config_files[] = $config_dir . '/' . $config_name . '.yml';
foreach (\Drupal::service('config.storage')->listAll() as $name) {
$archiver->addString("$name.yml", $dumper->dump(\Drupal::config($name)->get(), PHP_INT_MAX, 0, TRUE));
}
$archiver->createModify($config_files, '', config_get_config_directory());
$request = new Request(array('file' => 'config.tar.gz'));
return $this->fileDownloadController->download($request, 'temporary');
......
......@@ -12,6 +12,7 @@
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Yaml\Dumper;
/**
* Provides a form for exporting a single configuration file.
......@@ -32,6 +33,13 @@ class ConfigSingleExportForm extends FormBase {
*/
protected $configStorage;
/**
* The YAML dumper.
*
* @var \Symfony\Component\Yaml\Dumper
*/
protected $dumper;
/**
* Tracks the valid config entity type definitions.
*
......@@ -46,10 +54,14 @@ class ConfigSingleExportForm extends FormBase {
* The entity manager.
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The config storage.
* @param \Symfony\Component\Yaml\Dumper $dumper
* The yaml dumper.
*/
public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage) {
public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage, Dumper $dumper) {
$this->entityManager = $entity_manager;
$this->configStorage = $config_storage;
$this->dumper = $dumper;
$this->dumper->setIndentation(2);
}
/**
......@@ -58,7 +70,8 @@ public function __construct(EntityManagerInterface $entity_manager, StorageInter
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('config.storage')
$container->get('config.storage'),
new Dumper()
);
}
......@@ -151,8 +164,7 @@ public function updateExport($form, &$form_state) {
$name = $form_state['values']['config_name'];
}
// Read the raw data for this config name, encode it, and display it.
$data = $this->configStorage->read($name);
$form['export']['#value'] = $this->configStorage->encode($data);
$form['export']['#value'] = $this->dumper->dump($this->configStorage->read($name), PHP_INT_MAX);
$form['export']['#description'] = $this->t('The filename is %name.', array('%name' => $name . '.yml'));
return $form['export'];
}
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Yaml\Yaml;
/**
* Provides a form for importing a single configuration file.
......@@ -31,6 +32,13 @@ class ConfigSingleImportForm extends ConfirmFormBase {
*/
protected $configStorage;
/**
* The YAML component.
*
* @var \Symfony\Component\Yaml\Yaml
*/
protected $yaml;
/**
* If the config exists, this is that object. Otherwise, FALSE.
*
......@@ -52,10 +60,13 @@ class ConfigSingleImportForm extends ConfirmFormBase {
* The entity manager.
* @param \Drupal\Core\Config\StorageInterface $config_storage
* The config storage.
* @param \Symfony\Component\Yaml\Yaml $yaml
* The YAML component.
*/
public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage) {
public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage, Yaml $yaml) {
$this->entityManager = $entity_manager;
$this->configStorage = $config_storage;
$this->yaml = $yaml;
}
/**
......@@ -64,7 +75,8 @@ public function __construct(EntityManagerInterface $entity_manager, StorageInter
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager'),
$container->get('config.storage')
$container->get('config.storage'),
new Yaml()
);
}
......@@ -184,7 +196,7 @@ public function validateForm(array &$form, array &$form_state) {
}
// Decode the submitted import.
$data = $this->configStorage->decode($form_state['values']['import']);
$data = $this->yaml->parse($form_state['values']['import']);
// Validate for config entities.
if ($form_state['values']['config_type'] !== 'system.simple') {
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Config\ConfigNameException;
use Drupal\simpletest\DrupalUnitTestBase;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\DatabaseStorage;
use Drupal\Core\Config\UnsupportedDataTypeConfigException;
/**
......@@ -192,10 +193,10 @@ function testNameValidation() {
*/
public function testDataTypes() {
\Drupal::moduleHandler()->install(array('config_test'));
$storage = new FileStorage($this->configDirectories[CONFIG_ACTIVE_DIRECTORY]);
$storage = new DatabaseStorage($this->container->get('database'), 'config');
$name = 'config_test.types';
$config = $this->container->get('config.factory')->get($name);
$original_content = file_get_contents($storage->getFilePath($name));
$original_content = file_get_contents(drupal_get_path('module', 'config_test') . "/config/$name.yml");
$this->verbose('<pre>' . $original_content . "\n" . var_export($storage->read($name), TRUE));
// Verify variable data types are intact.
......@@ -220,7 +221,7 @@ public function testDataTypes() {
$this->assertIdentical($config->get(), $data);
// Assert the data against the file storage.
$this->assertIdentical($storage->read($name), $data);
$this->verbose('<pre>' . file_get_contents($storage->getFilePath($name)) . var_export($storage->read($name), TRUE));
$this->verbose('<pre>' . $name . var_export($storage->read($name), TRUE));
// Set data using config::setData().
$config->setData($data)->save();
......
......@@ -8,7 +8,7 @@
namespace Drupal\config\Tests;
use Drupal\simpletest\WebTestBase;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Dumper;
/**
* Tests the user interface for importing/exporting a single configuration.
......@@ -22,6 +22,13 @@ class ConfigSingleImportExportTest extends WebTestBase {
*/
public static $modules = array('config', 'config_test');
/**
* The YAML dumper.
*
* @var \Symfony\Component\Yaml\Dumper
*/
protected $dumper;
public static function getInfo() {
return array(
'name' => 'Configuration Single Import/Export UI',
......@@ -30,6 +37,15 @@ public static function getInfo() {
);
}
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->dumper = new Dumper();
$this->dumper->setIndentation(2);
}
/**
* Tests importing a single configuration file.
*/
......@@ -111,12 +127,11 @@ public function testImport() {
*/
public function testImportSimpleConfiguration() {
$this->drupalLogin($this->drupalCreateUser(array('import configuration')));
$yaml = new Yaml();
$config = \Drupal::config('system.site')->set('name', 'Test simple import');
$edit = array(
'config_type' => 'system.simple',
'config_name' => $config->getName(),
'import' => $yaml->dump($config->get()),
'import' => $this->dumper->dump($config->get(), PHP_INT_MAX),
);
$this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import'));
$this->assertRaw(t('Are you sure you want to update the %name @type?', array('%name' => $config->getName(), '@type' => 'simple configuration')));
......@@ -152,7 +167,7 @@ public function testExport() {
$this->assertFieldByXPath('//select[@name="config_name"]//option[@selected="selected"]', t('Fallback date format'), 'The fallback date format config entity is selected when specified in the URL.');
$fallback_date = \Drupal::entityManager()->getStorage('date_format')->load('fallback');
$data = \Drupal::service('config.storage')->encode($fallback_date->toArray());
$data = $this->dumper->dump($fallback_date->toArray(), PHP_INT_MAX);
$this->assertFieldByXPath('//textarea[@name="export"]', $data, 'The fallback date format config entity export code is displayed.');
}
......
......@@ -95,6 +95,10 @@ function testCRUD() {
$result = $this->invalidStorage->read($name);
$this->assertIdentical($result, FALSE);
// Listing on a non-existing storage bin returns an empty array.
$result = $this->invalidStorage->listAll();
$this->assertIdentical($result, array());
// Deleting all names with prefix deletes the appropriate data and returns
// TRUE.
$files = array(
......@@ -111,15 +115,6 @@ function testCRUD() {
$this->assertIdentical($result, TRUE);
$this->assertIdentical($names, array());
// Writing to a non-existing storage bin throws an exception.
try {