diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 7daf311..23eed11 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -2437,26 +2437,39 @@ function drupal_container(Container $reset = NULL) {
// This will get merged with the full Kernel-built Container on normal page
// requests.
$container = new ContainerBuilder();
- // Register configuration storage dispatcher.
- $container->setParameter('config.storage.info', array(
- 'Drupal\Core\Config\DatabaseStorage' => array(
- 'connection' => 'default',
- 'target' => 'default',
- 'read' => TRUE,
- 'write' => TRUE,
- ),
+
+ // 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(
'Drupal\Core\Config\FileStorage' => array(
'directory' => config_get_config_directory(),
- 'read' => TRUE,
- 'write' => FALSE,
+ ),
+ 'Drupal\Core\Config\CacheStorage' => array(
+ 'backend' => 'Drupal\Core\Cache\DatabaseBackend',
+ // @todo Add and replace with 'config' default bin.
+ 'bin' => 'cache',
),
));
- $container->register('config.storage.dispatcher', 'Drupal\Core\Config\StorageDispatcher')
- ->addArgument('%config.storage.info%');
+ // @todo Use this for exporting your current config in the DB... ;)
+ #$container->register('config.storage', 'Drupal\Core\Config\MultiStorage')
+ $container->register('config.storage', 'Drupal\Core\Config\CachedFileStorage')
+ ->addArgument('%config.storage.options%');
// Register configuration object factory.
$container->register('config.factory', 'Drupal\Core\Config\ConfigFactory')
- ->addArgument(new Reference('config.storage.dispatcher'));
+ ->addArgument(new Reference('config.storage'));
+
+ // Register configuration state.
+ $container->setParameter('config.state.options', array(
+ 'connection' => 'default',
+ 'target' => 'default',
+ 'table' => 'config',
+ ));
+ $container->register('config.state', 'Drupal\Core\Config\DatabaseStorage')
+ ->addArgument('%config.state.options%');
}
return $container;
}
diff --git a/core/includes/config.inc b/core/includes/config.inc
index 5b1fe21..14f41f9 100644
--- a/core/includes/config.inc
+++ b/core/includes/config.inc
@@ -1,7 +1,6 @@
$config_dir));
- $target_storage = new DatabaseStorage();
+ $target_storage = drupal_container()->get('config.storage');
$null_storage = new NullStorage();
// Upon installation, only new config objects need to be created.
@@ -44,11 +40,12 @@ 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 = '') {
- $storage = new DatabaseStorage();
- return $storage->listAll($prefix);
+ return drupal_container()->get('config.storage')->listAll($prefix);
}
/**
@@ -131,17 +128,16 @@ function config_sync_changes(array $config_changes, StorageInterface $source_sto
}
/**
- * Imports configuration from FileStorage to DatabaseStorage.
+ * Imports configuration into the active store.
*
* @return bool|null
* TRUE if configuration was imported successfully, FALSE in case of a
* synchronization error, or NULL if there are no changes to synchronize.
*/
function config_import() {
- // Retrieve a list of differences between FileStorage and DatabaseStorage.
- // @todo Leverage DI + config.storage.info.
- $source_storage = new FileStorage();
- $target_storage = new DatabaseStorage();
+ // Retrieve a list of differences between last known state and active store.
+ $source_storage = drupal_container()->get('config.state');
+ $target_storage = drupal_container()->get('config.storage');
$config_changes = config_sync_get_changes($source_storage, $target_storage);
if (empty($config_changes)) {
@@ -185,8 +181,6 @@ function config_import() {
* @todo Add support for other extension types; e.g., themes etc.
*/
function config_import_invoke_owner(array $config_changes, StorageInterface $source_storage, StorageInterface $target_storage) {
- $storage_dispatcher = drupal_container()->get('config.storage.dispatcher');
-
// Allow modules to take over configuration change operations for
// higher-level configuration data.
// First pass deleted, then new, and lastly changed configuration, in order to
@@ -199,13 +193,13 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou
// handle the configuration change.
$handled_by_module = FALSE;
if (module_hook($module, 'config_import_' . $op)) {
- $old_config = new Config($storage_dispatcher);
+ $old_config = new Config($target_storage);
$old_config
->setName($name)
->load();
$data = $source_storage->read($name);
- $new_config = new Config($storage_dispatcher);
+ $new_config = new Config($target_storage);
$new_config->setName($name);
if ($data !== FALSE) {
$new_config->setData($data);
@@ -222,13 +216,14 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou
}
/**
- * Exports configuration from DatabaseStorage to FileStorage.
+ * Updates the last known state with the active store configuration.
+ *
+ * @todo config_export() is a misnomer now. Rename to config_state_update().
*/
function config_export() {
- // Retrieve a list of differences between DatabaseStorage and FileStorage.
- // @todo Leverage DI + config.storage.info.
- $source_storage = new DatabaseStorage();
- $target_storage = new FileStorage();
+ // Retrieve a list of differences between active store and last known state.
+ $source_storage = drupal_container()->get('config.storage');
+ $target_storage = drupal_container()->get('config.state');
$config_changes = config_sync_get_changes($source_storage, $target_storage);
if (empty($config_changes)) {
diff --git a/core/includes/form.inc b/core/includes/form.inc
index 7498ea5..f162d85 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -3533,6 +3533,7 @@ function form_process_machine_name($element, &$form_state) {
// 'source' only) would leave all other properties undefined, if the defaults
// were defined in hook_element_info(). Therefore, we apply the defaults here.
$element['#machine_name'] += array(
+ // @todo Use 'label' by default.
'source' => array('name'),
'target' => '#' . $element['#id'],
'label' => t('Machine name'),
diff --git a/core/includes/module.inc b/core/includes/module.inc
index ab45576..6c4f24d 100644
--- a/core/includes/module.inc
+++ b/core/includes/module.inc
@@ -6,7 +6,7 @@
*/
use Drupal\Component\Graph\Graph;
-use Drupal\Core\Config\DatabaseStorage;
+use Drupal\Core\Config\NullStorage;
/**
* Load all the modules that have been enabled in the system table.
@@ -622,19 +622,26 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE)
$module_list = array_keys($module_list);
}
- $storage = new DatabaseStorage();
+ $source_storage = new NullStorage();
+ $target_storage = drupal_container()->get('config.storage');
foreach ($module_list as $module) {
+ // Remove all configuration belonging to the module.
+ $config_changes = $target_storage->listAll($module . '.');
+ if (!empty($config_changes)) {
+ $config_changes = array(
+ 'delete' => $config_changes,
+ 'change' => array(),
+ 'create' => array(),
+ );
+ $remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
+ config_sync_changes($remaining_changes, $source_storage, $target_storage);
+ }
+
// Uninstall the module.
module_load_install($module);
module_invoke($module, 'uninstall');
drupal_uninstall_schema($module);
- // 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);
drupal_set_installed_schema_version($module, SCHEMA_UNINSTALLED);
}
diff --git a/core/lib/Drupal/Core/Config/CacheStorage.php b/core/lib/Drupal/Core/Config/CacheStorage.php
new file mode 100644
index 0000000..6e77ae2
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/CacheStorage.php
@@ -0,0 +1,112 @@
+ 'Drupal\Core\Cache\DatabaseBackend',
+ 'bin' => 'config',
+ );
+ $this->options = $options;
+ }
+
+ /**
+ * Returns the instantiated Cache backend to use.
+ */
+ protected function getBackend() {
+ if (!isset($this->storage)) {
+ $this->storage = new $this->options['backend']($this->options['bin']);
+ }
+ return $this->storage;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::exists().
+ */
+ public function exists($name) {
+ return (bool) $this->getBackend()->get($name);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::read().
+ */
+ public function read($name) {
+ if ($cache = $this->getBackend()->get($name)) {
+ return $cache->data;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::write().
+ */
+ public function write($name, array $data) {
+ $this->getBackend()->set($name, $data, CACHE_PERMANENT, array('config' => array($name)));
+ return TRUE;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::delete().
+ */
+ public function delete($name) {
+ $this->getBackend()->delete($name);
+ return TRUE;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::encode().
+ */
+ public static function encode($data) {
+ return serialize($data);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::decode().
+ *
+ * @throws ErrorException
+ * unserialize() triggers E_NOTICE if the string cannot be unserialized.
+ */
+ public static function decode($raw) {
+ $data = @unserialize($raw);
+ return is_array($data) ? $data : FALSE;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::listAll().
+ *
+ * Not supported by CacheBackendInterface.
+ */
+ public function listAll($prefix = '') {
+ return array();
+ }
+}
diff --git a/core/lib/Drupal/Core/Config/CachedFileStorage.php b/core/lib/Drupal/Core/Config/CachedFileStorage.php
new file mode 100644
index 0000000..7fbaef9
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/CachedFileStorage.php
@@ -0,0 +1,125 @@
+options = $options;
+
+ $this->storages['file'] = new FileStorage($options['Drupal\Core\Config\FileStorage']);
+
+ unset($options['Drupal\Core\Config\FileStorage']);
+ list($active_class, $active_options) = each($options);
+ $this->storages['active'] = new $active_class($active_options);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::exists().
+ */
+ public function exists($name) {
+ // A single filestat is faster than a complex cache lookup and possibly
+ // subsequent filestat.
+ return $this->storages['file']->exists($name);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::read().
+ */
+ public function read($name) {
+ // Check the cache.
+ $data = $this->storages['active']->read($name);
+ // If the cache returns no result, check the file storage.
+ if ($data === FALSE) {
+ $data = $this->storages['file']->read($name);
+ // @todo Should the config object be cached if it does not exist?
+ if ($data !== FALSE) {
+ $this->storages['active']->write($name, $data);
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::write().
+ */
+ public function write($name, array $data) {
+ $success = $this->storages['file']->write($name, $data);
+ $this->storages['active']->delete($name);
+ return $success;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::delete().
+ */
+ public function delete($name) {
+ $success = TRUE;
+ foreach ($this->storages as $storage) {
+ if (!$storage->delete($name)) {
+ $success = FALSE;
+ }
+ }
+ return $success;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::encode().
+ *
+ * @todo Remove encode() from StorageInterface.
+ */
+ public static function encode($data) {
+ return $this->storages['file']->encode($data);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::decode().
+ *
+ * @todo Remove decode() from StorageInterface.
+ */
+ public static function decode($raw) {
+ return $this->storages['file']->decode($raw);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::listAll().
+ */
+ public function listAll($prefix = '') {
+ return $this->storages['file']->listAll($prefix);
+ }
+}
diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php
index ffddd61..c5423ca 100644
--- a/core/lib/Drupal/Core/Config/Config.php
+++ b/core/lib/Drupal/Core/Config/Config.php
@@ -36,21 +36,21 @@ class Config {
protected $data = array();
/**
- * The injected storage dispatcher object.
+ * The storage used for reading and writing.
*
- * @var Drupal\Core\Config\StorageDispatcher
+ * @var Drupal\Core\Config\StorageInterface
*/
- protected $storageDispatcher;
+ protected $storage;
/**
* Constructs a configuration object.
*
- * @param Drupal\Core\Config\StorageDispatcher $storageDispatcher
- * A storage dispatcher object to use for reading and writing the
+ * @param Drupal\Core\Config\StorageInterface $storage
+ * A storage controller object to use for reading and writing the
* configuration data.
*/
- public function __construct(StorageDispatcher $storageDispatcher) {
- $this->storageDispatcher = $storageDispatcher;
+ public function __construct(StorageInterface $storage) {
+ $this->storage = $storage;
}
/**
@@ -224,7 +224,7 @@ public function clear($key) {
* Loads configuration data into this object.
*/
public function load() {
- $data = $this->storageDispatcher->selectStorage('read', $this->name)->read($this->name);
+ $data = $this->storage->read($this->name);
if ($data === FALSE) {
$this->isNew = TRUE;
$this->setData(array());
@@ -241,7 +241,7 @@ public function load() {
*/
public function save() {
$this->sortByKey($this->data);
- $this->storageDispatcher->selectStorage('write', $this->name)->write($this->name, $this->data);
+ $this->storage->write($this->name, $this->data);
$this->isNew = FALSE;
return $this;
}
@@ -270,8 +270,9 @@ public function sortByKey(array &$data) {
* Deletes the configuration object.
*/
public function delete() {
+ // @todo Consider to remove the pruning of data for Config::delete().
$this->data = array();
- $this->storageDispatcher->selectStorage('write', $this->name)->delete($this->name);
+ $this->storage->delete($this->name);
$this->isNew = TRUE;
return $this;
}
diff --git a/core/lib/Drupal/Core/Config/ConfigFactory.php b/core/lib/Drupal/Core/Config/ConfigFactory.php
index 1fbca62..7c26b6e 100644
--- a/core/lib/Drupal/Core/Config/ConfigFactory.php
+++ b/core/lib/Drupal/Core/Config/ConfigFactory.php
@@ -15,29 +15,29 @@
*
* @see Drupal\Core\Config\Config
*
- * Each configuration object gets a storage dispatcher object injected, which
- * determines the storage controller to use for reading and writing the
- * configuration data.
+ * Each configuration object gets a storage controller object injected, which
+ * is used for reading and writing the configuration data.
*
- * @see Drupal\Core\Config\StorageDispatcher
+ * @see Drupal\Core\Config\StorageInterface
*/
class ConfigFactory {
+
/**
- * A storage dispatcher instance to use for reading and writing configuration data.
+ * A storage controller instance for reading and writing configuration data.
*
- * @var Drupal\Core\Config\StorageDispatcher
+ * @var Drupal\Core\Config\StorageInterface
*/
- protected $storageDispatcher;
+ protected $storage;
/**
* Constructs the Config factory.
*
- * @param Drupal\Core\Config\StorageDispatcher $storage_dispatcher
- * The storage dispatcher object to use for reading and writing
+ * @param Drupal\Core\Config\StorageInterface $storage
+ * The storage controller object to use for reading and writing
* configuration data.
*/
- public function __construct(StorageDispatcher $storage_dispatcher) {
- $this->storageDispatcher = $storage_dispatcher;
+ public function __construct(StorageInterface $storage) {
+ $this->storage = $storage;
}
/**
@@ -68,7 +68,8 @@ public function get($name) {
// @todo The decrease of CPU time is interesting, since that means that
// ContainerBuilder involves plenty of function calls (which are known to
// be slow in PHP).
- $config = new Config($this->storageDispatcher);
+ $config = new Config($this->storage);
return $config->setName($name);
}
+
}
diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php
index f30011c..8ab64f6 100644
--- a/core/lib/Drupal/Core/Config/DatabaseStorage.php
+++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php
@@ -44,6 +44,15 @@ protected function getConnection() {
}
/**
+ * Implements Drupal\Core\Config\StorageInterface::exists().
+ */
+ public function exists($name) {
+ return (bool) $this->getConnection()->queryRange('SELECT 1 FROM {config} WHERE name = :name', 0, 1, array(
+ ':name' => $name,
+ ), $this->options)->fetchField();
+ }
+
+ /**
* Implements Drupal\Core\Config\StorageInterface::read().
*
* @throws PDOException
@@ -56,8 +65,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 StorageDispatcher configuration in
- // the installer instead.
+ // @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();
if ($raw !== FALSE) {
diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php
index 033555d..727adef 100644
--- a/core/lib/Drupal/Core/Config/FileStorage.php
+++ b/core/lib/Drupal/Core/Config/FileStorage.php
@@ -54,10 +54,7 @@ public static function getFileExtension() {
}
/**
- * Returns whether the configuration file exists.
- *
- * @return bool
- * TRUE if the configuration file exists, FALSE otherwise.
+ * Implements Drupal\Core\Config\StorageInterface::exists().
*/
public function exists($name) {
return file_exists($this->getFilePath($name));
diff --git a/core/lib/Drupal/Core/Config/MultiStorage.php b/core/lib/Drupal/Core/Config/MultiStorage.php
new file mode 100644
index 0000000..484501a
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/MultiStorage.php
@@ -0,0 +1,117 @@
+ $storage_options) {
+ $this->storages[] = new $storage_controller($storage_options);
+ }
+ $this->options = $options;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::exists().
+ */
+ public function exists($name) {
+ return $this->storages[0]->exists($name);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::read().
+ */
+ public function read($name) {
+ return $this->storages[0]->read($name);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::write().
+ */
+ public function write($name, array $data) {
+ $success = TRUE;
+ foreach ($this->storages as $storage) {
+ if (!$storage->write($name, $data)) {
+ $success = FALSE;
+ }
+ }
+ return $success;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::delete().
+ */
+ public function delete($name) {
+ $success = TRUE;
+ foreach ($this->storages as $storage) {
+ if (!$storage->delete($name)) {
+ $success = FALSE;
+ }
+ }
+ return $success;
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::encode().
+ *
+ * @todo Remove encode() from StorageInterface.
+ */
+ public static function encode($data) {
+ return $this->storages[0]->encode($data);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::decode().
+ *
+ * @todo Remove decode() from StorageInterface.
+ */
+ public static function decode($raw) {
+ return $this->storages[0]->decode($raw);
+ }
+
+ /**
+ * Implements Drupal\Core\Config\StorageInterface::listAll().
+ */
+ public function listAll($prefix = '') {
+ return $this->storages[0]->listAll($prefix);
+ }
+}
diff --git a/core/lib/Drupal/Core/Config/NullStorage.php b/core/lib/Drupal/Core/Config/NullStorage.php
index fede4f0..2e1c06c 100644
--- a/core/lib/Drupal/Core/Config/NullStorage.php
+++ b/core/lib/Drupal/Core/Config/NullStorage.php
@@ -29,6 +29,13 @@ public function __construct(array $options = array()) {
}
/**
+ * Implements Drupal\Core\Config\StorageInterface::exists().
+ */
+ public function exists($name) {
+ return FALSE;
+ }
+
+ /**
* Implements Drupal\Core\Config\StorageInterface::read().
*/
public function read($name) {
diff --git a/core/lib/Drupal/Core/Config/StorageDispatcher.php b/core/lib/Drupal/Core/Config/StorageDispatcher.php
deleted file mode 100644
index ebf7049..0000000
--- a/core/lib/Drupal/Core/Config/StorageDispatcher.php
+++ /dev/null
@@ -1,105 +0,0 @@
- array(
- * 'target' => 'default',
- * 'read' => TRUE,
- * 'write' => TRUE,
- * ),
- * 'Drupal\Core\Config\FileStorage' => array(
- * 'directory' => 'sites/default/files/config',
- * 'read' => TRUE,
- * 'write' => FALSE,
- * ),
- * )
- * @endcode
- */
- public function __construct(array $storage_info) {
- $this->storageInfo = $storage_info;
- }
-
- /**
- * Returns a storage controller to use for a given operation.
- *
- * Handles the core functionality of the storage dispatcher by determining
- * which storage can handle a particular storage access operation and
- * configuration object.
- *
- * @param string $access_operation
- * The operation access level; either 'read' or 'write'. Use 'write' both
- * for saving and deleting configuration.
- * @param string $name
- * The name of the configuration object that is operated on.
- *
- * @return Drupal\Core\Config\StorageInterface
- * The storage controller instance that can handle the requested operation.
- *
- * @throws Drupal\Core\Config\ConfigException
- *
- * @todo Allow write operations to write to multiple storages.
- */
- public function selectStorage($access_operation, $name) {
- // Determine the appropriate storage controller to use.
- // Take the first defined storage that allows $op.
- foreach ($this->storageInfo as $class => $storage_config) {
- if (!empty($storage_config[$access_operation])) {
- $storage_class = $class;
- break;
- }
- }
- if (!isset($storage_class)) {
- throw new ConfigException("Failed to find storage controller that allows $access_operation access for $name.");
- }
-
- // Instantiate a new storage controller object, if there is none yet.
- if (!isset($this->storageInstances[$storage_class])) {
- $this->storageInstances[$storage_class] = new $storage_class($this->storageInfo[$storage_class]);
- }
- return $this->storageInstances[$storage_class];
- }
-}
diff --git a/core/lib/Drupal/Core/Config/StorageInterface.php b/core/lib/Drupal/Core/Config/StorageInterface.php
index 806ee87..4fb9869 100644
--- a/core/lib/Drupal/Core/Config/StorageInterface.php
+++ b/core/lib/Drupal/Core/Config/StorageInterface.php
@@ -25,6 +25,17 @@
public function __construct(array $options = array());
/**
+ * Returns whether a configuration object exists.
+ *
+ * @param string $name
+ * The name of a configuration object to test.
+ *
+ * @return bool
+ * TRUE if the configuration object exists, FALSE otherwise.
+ */
+ public function exists($name);
+
+ /**
* Reads configuration data from the storage.
*
* @param string $name
diff --git a/core/lib/Drupal/Core/ConfigThingie/ConfigThingieBase.php b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieBase.php
new file mode 100644
index 0000000..3f83cf4
--- /dev/null
+++ b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieBase.php
@@ -0,0 +1,232 @@
+setConfig($config);
+ }
+
+ /**
+ * Returns the configuration thingie object's name prefix.
+ *
+ * @return string
+ * The configuration object name prefix; e.g., for a 'node.type.article'
+ * thingie, this returns 'node.type'.
+ */
+ abstract public function getConfigPrefix();
+
+ /**
+ * Returns the fully qualified configuration object name for this configuration thingie.
+ *
+ * @return string
+ * The configuration object name for this configuration thingie. Normally,
+ * ConfigThingieInterface::getConfigPrefix() and
+ * ConfigThingieInterface::getID() form the full configuration object name;
+ * i.e., '[prefix].[id]'.
+ */
+ public function getConfigName() {
+ return $this->getConfigPrefix() . '.' . $this->getID();
+ }
+
+ /**
+ * Returns the basename used for events/hooks for this configuration thingie.
+ *
+ * @return string
+ * The basename for events/hooks; e.g., for a 'node.type.article' thingie,
+ * this returns 'node_type'. Normally, this is auto-generated based on
+ * ConfigThingieInterface::getConfigPrefix().
+ */
+ public function getEventBasename() {
+ return str_replace('.', '_', $this->getConfigPrefix());
+ }
+
+ /**
+ * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::getID().
+ */
+ public function getID() {
+ return $this->config->get($this->idKey);
+ }
+
+ /**
+ * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::isNew().
+ */
+ public function isNew() {
+ return $this->config->isNew();
+ }
+
+ /**
+ * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::getLabel().
+ */
+ public function getLabel() {
+ return $this->config->get($this->labelKey);
+ }
+
+ /**
+ * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::get().
+ */
+ public function get($property_name) {
+ return $this->config->get($property_name);
+ }
+
+ /**
+ * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::set().
+ */
+ public function set($property_name, $value) {
+ return $this->config->set($property_name, $value);
+ }
+
+ /**
+ * Sets the configuration object for this configuration thingie.
+ *
+ * @param Drupal\Core\Config\Config $config
+ * The configuration object containing the data for this configuration
+ * thingie.
+ *
+ * @return Drupal\Core\ConfigThingie\ConfigThingieInterface
+ * This configuration thingie (suitable for chained method calls).
+ */
+ public function setConfig(Config $config) {
+ $this->config = $config;
+ // Only set originalID, if the passed in configuration object is stored
+ // already.
+ if (!$this->config->isNew()) {
+ $this->originalID = $this->getID();
+ }
+
+ // Allow modules to react upon load.
+ module_invoke_all($this->getEventBasename() . '_load', $this);
+
+ return $this;
+ }
+
+ /**
+ * Sets the unchanged original of this configuration thingie.
+ *
+ * @param Drupal\Core\Config\Config $config
+ * The configuration object containing the original data for this
+ * configuration thingie.
+ *
+ * @return Drupal\Core\ConfigThingie\ConfigThingieInterface
+ * This configuration thingie (suitable for chained method calls).
+ */
+ public function setOriginal(Config $config) {
+ $this->original = new $this($config);
+ // Ensure that originalID contains the ID of the supplied original
+ // configuration object. setOriginal() may be called from outside of this
+ // class (e.g., hook_config_import()) in order to set a specific original.
+ $this->originalID = $this->original->getID();
+ return $this;
+ }
+
+ /**
+ * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::save().
+ */
+ public function save() {
+ // Provide the original configuration thingie in $this->original, if any.
+ // originalID is only set, if this configuration thingie already existed
+ // prior to saving.
+ if (isset($this->originalID)) {
+ // Load the original configuration object.
+ // This cannot use ConfigThingieBase::getConfigName(), since that would
+ // yield the name using the current/new ID.
+ $original_config = config($this->getConfigPrefix() . '.' . $this->originalID);
+ // Given the original configuration object, instantiate a new class of the
+ // current class, and provide it in $this->original.
+ $this->setOriginal($original_config);
+ }
+
+ // Ensure that the configuration object name uses the current ID.
+ $this->config->setName($this->getConfigName());
+
+ // Allow modules to react prior to saving.
+ module_invoke_all($this->getEventBasename() . '_presave', $this);
+
+ // Save the configuration object.
+ $this->config->save();
+
+ if (isset($this->originalID)) {
+ // Allow modules to react after inserting new configuration.
+ module_invoke_all($this->getEventBasename() . '_update', $this);
+
+ // Delete the original configuration, if it was renamed.
+ if ($this->originalID !== $this->getID()) {
+ // Configuration data is emptied out upon delete, so back it up and
+ // re-inject it. Delete the old configuration data directly; hooks will
+ // get and will be able to react to the data in $this->original.
+ // @todo Consider to remove the pruning of data for Config::delete().
+ $original_data = $original_config->get();
+ $original_config->delete();
+ $original_config->setData($original_data);
+ }
+ }
+ else {
+ // Allow modules to react after updating existing configuration.
+ module_invoke_all($this->getEventBasename() . '_insert', $this);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Implements Drupal\Core\ConfigThingie\ConfigThingieInterface::delete().
+ */
+ public function delete() {
+ // Allow modules to react prior to deleting the configuration.
+ module_invoke_all($this->getEventBasename() . '_predelete', $this);
+
+ // Delete the configuration object.
+ $this->config->delete();
+
+ // Allow modules to react after deleting the configuration.
+ module_invoke_all($this->getEventBasename() . '_delete', $this);
+ }
+}
diff --git a/core/lib/Drupal/Core/ConfigThingie/ConfigThingieInterface.php b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieInterface.php
new file mode 100644
index 0000000..506ffa2
--- /dev/null
+++ b/core/lib/Drupal/Core/ConfigThingie/ConfigThingieInterface.php
@@ -0,0 +1,91 @@
+ t('There are no configuration changes.'),
+ );
+ return $form;
+ }
+
+ foreach ($config_changes as $config_change_type => $config_files) {
+ if (empty($config_files)) {
+ continue;
+ }
+ $form[$config_change_type] = array(
+ '#type' => 'fieldset',
+ '#title' => $config_change_type . ' (' . count($config_files) . ')',
+ '#collapsible' => TRUE,
+ );
+ $form[$config_change_type]['config_files'] = array(
+ '#theme' => 'table',
+ '#header' => array('Name'),
+ );
+ foreach ($config_files as $config_file) {
+ $form[$config_change_type]['config_files']['#rows'][] = array($config_file);
+ }
+ }
+}
+
+/**
+ * Form constructor for configuration import form.
+ *
+ * @see config_admin_import_form_submit()
+ * @see config_import()
+ */
+function config_admin_import_form($form, &$form_state) {
+ // Retrieve a list of differences between last known state and active store.
+ $source_storage = drupal_container()->get('config.state');
+ $target_storage = drupal_container()->get('config.storage');
+
+ // Prevent users from deleting all configuration.
+ // If the source storage is empty, that signals the unique condition of not
+ // having exported anything at all, and thus no valid storage to compare the
+ // active storage against.
+ // @todo StorageInterface::listAll() can easily yield hundreds or even
+ // thousands of entries; consider to add a dedicated isEmpty() method for
+ // storage controllers.
+ $all = $source_storage->listAll();
+ if (empty($all)) {
+ form_set_error('', t('There is no base configuration. Export it first.', array(
+ '@export-url' => url('admin/config/development/sync/export'),
+ )));
+ return $form;
+ }
+
+ config_admin_sync_form($form, $form_state, $source_storage, $target_storage);
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Import'),
+ );
+ return $form;
+}
+
+/**
+ * Form submission handler for config_admin_import_form().
+ */
+function config_admin_import_form_submit($form, &$form_state) {
+ if (config_import()) {
+ drupal_set_message(t('The configuration was imported successfully.'));
+ }
+ else {
+ // Another request may be synchronizing configuration already. Wait for it
+ // to complete before returning the error, so already synchronized changes
+ // do not appear again.
+ lock_wait(__FUNCTION__);
+ drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error');
+ }
+}
+
+/**
+ * Form constructor for configuration export form.
+ *
+ * @see config_admin_export_form_submit()
+ * @see config_export()
+ *
+ * @todo "export" is a misnomer with config.state.
+ */
+function config_admin_export_form($form, &$form_state) {
+ // Retrieve a list of differences between active store and last known state.
+ $source_storage = drupal_container()->get('config.storage');
+ $target_storage = drupal_container()->get('config.state');
+
+ config_admin_sync_form($form, $form_state, $source_storage, $target_storage);
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array(
+ '#type' => 'submit',
+ '#value' => t('Export'),
+ );
+ return $form;
+}
+
+/**
+ * Form submission handler for config_admin_export_form().
+ */
+function config_admin_export_form_submit($form, &$form_state) {
+ config_export();
+ drupal_set_message(t('The configuration was exported successfully.'));
+}
+
diff --git a/core/modules/config/config.info b/core/modules/config/config.info
index 380f17e..efab7a1 100644
--- a/core/modules/config/config.info
+++ b/core/modules/config/config.info
@@ -3,3 +3,4 @@ description = Allows administrators to manage configuration changes.
package = Core
version = VERSION
core = 8.x
+configure = admin/config/development/sync
diff --git a/core/modules/config/config.module b/core/modules/config/config.module
index b3d9bbc..fd38fd2 100644
--- a/core/modules/config/config.module
+++ b/core/modules/config/config.module
@@ -1 +1,46 @@
t('Synchronize configuration'),
+ 'restrict access' => TRUE,
+ );
+ return $permissions;
+}
+
+/**
+ * Implements hook_menu().
+ */
+function config_menu() {
+ $items['admin/config/development/sync'] = array(
+ 'title' => 'Synchronize configuration',
+ 'description' => 'Synchronize configuration changes.',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('config_admin_import_form'),
+ 'access arguments' => array('synchronize configuration'),
+ 'file' => 'config.admin.inc',
+ );
+ $items['admin/config/development/sync/import'] = array(
+ 'title' => 'Import',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+ $items['admin/config/development/sync/export'] = array(
+ 'title' => 'Export',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('config_admin_export_form'),
+ 'access arguments' => array('synchronize configuration'),
+ 'file' => 'config.admin.inc',
+ 'type' => MENU_LOCAL_TASK,
+ );
+ return $items;
+}
+
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php
new file mode 100644
index 0000000..cfb4bcc
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigConfigurableTest.php
@@ -0,0 +1,80 @@
+ 'Configurable configuration',
+ 'description' => 'Tests configurable configuration.',
+ 'group' => 'Configuration',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('config_test'));
+ }
+
+ /**
+ * Tests basic CRUD operations through the UI.
+ */
+ function testCRUD() {
+ // Create a thingie.
+ $id = 'thingie';
+ $edit = array(
+ 'id' => $id,
+ 'label' => 'Thingie',
+ );
+ $this->drupalPost('admin/structure/config_test/add', $edit, 'Save');
+ $this->assertResponse(200);
+ $this->assertText('Thingie');
+
+ // Update the thingie.
+ $this->assertLinkByHref('admin/structure/config_test/manage/' . $id);
+ $edit = array(
+ 'label' => 'Thongie',
+ );
+ $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save');
+ $this->assertResponse(200);
+ $this->assertNoText('Thingie');
+ $this->assertText('Thongie');
+
+ // Delete the thingie.
+ $this->assertLinkByHref('admin/structure/config_test/manage/' . $id . '/delete');
+ $this->drupalPost('admin/structure/config_test/manage/' . $id . '/delete', array(), 'Delete');
+ $this->assertResponse(200);
+ $this->assertNoText('Thingie');
+ $this->assertNoText('Thongie');
+
+ // Re-create a thingie.
+ $edit = array(
+ 'id' => $id,
+ 'label' => 'Thingie',
+ );
+ $this->drupalPost('admin/structure/config_test/add', $edit, 'Save');
+ $this->assertResponse(200);
+ $this->assertText('Thingie');
+
+ // Rename the thingie's ID/machine name.
+ $this->assertLinkByHref('admin/structure/config_test/manage/' . $id);
+ $new_id = 'zingie';
+ $edit = array(
+ 'id' => $new_id,
+ 'label' => 'Zingie',
+ );
+ $this->drupalPost('admin/structure/config_test/manage/' . $id, $edit, 'Save');
+ $this->assertResponse(200);
+ $this->assertNoText('Thingie');
+ $this->assertText('Zingie');
+ }
+}
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
index a6e72b4..88857cd 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php
@@ -25,6 +25,29 @@ public static function getInfo() {
function setUp() {
parent::setUp(array('config_test'));
+
+ // Clear out any possibly existing hook invocation records.
+ unset($GLOBALS['hook_config_test_dynamic']);
+ }
+
+ /**
+ * Tests omission of module APIs for bare configuration operations.
+ */
+ function testNoImport() {
+ $dynamic_name = 'config_test.dynamic.default';
+
+ // Verify the default configuration values exist.
+ $config = config($dynamic_name);
+ $this->assertIdentical($config->get('id'), 'default');
+
+ // Verify that a bare config() does not involve module APIs.
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']));
+
+ // Export.
+ config_export();
+
+ // Verify that config_export() does not involve module APIs.
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']));
}
/**
@@ -60,6 +83,14 @@ function testDeleted() {
$this->assertIdentical($config->get('foo'), NULL);
$config = config($dynamic_name);
$this->assertIdentical($config->get('id'), NULL);
+
+ // Verify that appropriate module API hooks have been invoked.
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['load']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['presave']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['insert']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['update']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['predelete']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['delete']));
}
/**
@@ -69,22 +100,28 @@ function testNew() {
$name = 'config_test.new';
$dynamic_name = 'config_test.dynamic.new';
+ // Export.
+ config_export();
+
// Verify the configuration to create does not exist yet.
+ $database_storage = new DatabaseStorage();
+ $this->assertIdentical($database_storage->exists($name), FALSE, $name . ' not found.');
+ $this->assertIdentical($database_storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.');
+
$file_storage = new FileStorage();
$this->assertIdentical($file_storage->exists($name), FALSE, $name . ' not found.');
$this->assertIdentical($file_storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.');
- // Export.
- config_export();
-
// Create new configuration objects.
- $file_storage->write($name, array(
+ $original_name_data = array(
'add_me' => 'new value',
- ));
- $file_storage->write($dynamic_name, array(
+ );
+ $file_storage->write($name, $original_name_data);
+ $original_dynamic_data = array(
'id' => 'new',
'label' => 'New',
- ));
+ );
+ $file_storage->write($dynamic_name, $original_dynamic_data);
$this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.');
$this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
@@ -93,9 +130,17 @@ function testNew() {
// Verify the values appeared.
$config = config($name);
- $this->assertIdentical($config->get('add_me'), 'new value');
+ $this->assertIdentical($config->get('add_me'), $original_name_data['add_me']);
$config = config($dynamic_name);
- $this->assertIdentical($config->get('label'), 'New');
+ $this->assertIdentical($config->get('label'), $original_dynamic_data['label']);
+
+ // Verify that appropriate module API hooks have been invoked.
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['load']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['presave']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['insert']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['update']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['predelete']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['delete']));
}
/**
@@ -108,17 +153,25 @@ function testUpdated() {
// Export.
config_export();
- // Replace the file content of the existing configuration objects.
+ // Verify that the configuration objects to import exist.
+ $database_storage = new DatabaseStorage();
+ $this->assertIdentical($database_storage->exists($name), TRUE, $name . ' found.');
+ $this->assertIdentical($database_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
+
$file_storage = new FileStorage();
$this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.');
$this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
- $file_storage->write($name, array(
+
+ // Replace the file content of the existing configuration objects.
+ $original_name_data = array(
'foo' => 'beer',
- ));
- $file_storage->write($dynamic_name, array(
+ );
+ $file_storage->write($name, $original_name_data);
+ $original_dynamic_data = array(
'id' => 'default',
'label' => 'Updated',
- ));
+ );
+ $file_storage->write($dynamic_name, $original_dynamic_data);
// Verify the active store still returns the default values.
$config = config($name);
@@ -134,5 +187,17 @@ function testUpdated() {
$this->assertIdentical($config->get('foo'), 'beer');
$config = config($dynamic_name);
$this->assertIdentical($config->get('label'), 'Updated');
+
+ // Verify that the original file content is still the same.
+ $this->assertIdentical($file_storage->read($name), $original_name_data);
+ $this->assertIdentical($file_storage->read($dynamic_name), $original_dynamic_data);
+
+ // Verify that appropriate module API hooks have been invoked.
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['load']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['presave']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['insert']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['update']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['predelete']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['delete']));
}
}
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
new file mode 100644
index 0000000..3a9acdd
--- /dev/null
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php
@@ -0,0 +1,143 @@
+ 'Import/Export UI',
+ 'description' => 'Tests the user interface for importing/exporting configuration.',
+ 'group' => 'Configuration',
+ );
+ }
+
+ function setUp() {
+ parent::setUp(array('config', 'config_test'));
+
+ $this->web_user = $this->drupalCreateUser(array('synchronize configuration'));
+ $this->drupalLogin($this->web_user);
+ }
+
+ /**
+ * Tests exporting configuration.
+ */
+ function testExport() {
+ $name = 'config_test.system';
+ $dynamic_name = 'config_test.dynamic.default';
+
+ // Verify the default configuration values exist.
+ $config = config($name);
+ $this->assertIdentical($config->get('foo'), 'bar');
+ $config = config($dynamic_name);
+ $this->assertIdentical($config->get('id'), 'default');
+
+ // Verify that both appear as deleted by default.
+ $this->drupalGet('admin/config/development/sync/export');
+ $this->assertText($name);
+ $this->assertText($dynamic_name);
+
+ // Export and verify that both do not appear anymore.
+ $this->drupalPost(NULL, array(), t('Export'));
+ $this->assertUrl('admin/config/development/sync/export');
+ $this->assertNoText($name);
+ $this->assertNoText($dynamic_name);
+
+ // Verify that there are no further changes to export.
+ $this->assertText(t('There are no configuration changes.'));
+
+ // Verify that the import screen shows no changes either.
+ $this->drupalGet('admin/config/development/sync');
+ $this->assertText(t('There are no configuration changes.'));
+ }
+
+ /**
+ * Tests importing configuration.
+ */
+ function testImport() {
+ $name = 'config_test.new';
+ $dynamic_name = 'config_test.dynamic.new';
+
+ // Verify the configuration to create does not exist yet.
+ $file_storage = new FileStorage();
+ $this->assertIdentical($file_storage->exists($name), FALSE, $name . ' not found.');
+ $this->assertIdentical($file_storage->exists($dynamic_name), FALSE, $dynamic_name . ' not found.');
+
+ // Verify that the import UI does not allow to import without exported
+ // configuration.
+ $this->drupalGet('admin/config/development/sync');
+ $this->assertText('There is no base configuration.');
+
+ // Verify that the Export link yields to the export UI page, and export.
+ $this->clickLink('Export');
+ $this->drupalPost(NULL, array(), t('Export'));
+
+ // Create new configuration objects.
+ $file_storage->write($name, array(
+ 'add_me' => 'new value',
+ ));
+ $file_storage->write($dynamic_name, array(
+ 'id' => 'new',
+ 'label' => 'New',
+ ));
+ $this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.');
+ $this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
+
+ // Verify that both appear as new.
+ $this->drupalGet('admin/config/development/sync');
+ $this->assertText($name);
+ $this->assertText($dynamic_name);
+
+ // Import and verify that both do not appear anymore.
+ $this->drupalPost(NULL, array(), t('Import'));
+ $this->assertUrl('admin/config/development/sync');
+ $this->assertNoText($name);
+ $this->assertNoText($dynamic_name);
+
+ // Verify that there are no further changes to import.
+ $this->assertText(t('There are no configuration changes.'));
+
+ // Verify that the export screen shows no changes either.
+ $this->drupalGet('admin/config/development/sync/export');
+ $this->assertText(t('There are no configuration changes.'));
+ }
+
+ /**
+ * Tests concurrent importing of configuration.
+ */
+ function testImportLock() {
+ $name = 'config_test.new';
+
+ // Write a configuration object to import.
+ $file_storage = new FileStorage();
+ $file_storage->write($name, array(
+ 'add_me' => 'new value',
+ ));
+
+ // Verify that there are configuration differences to import.
+ $this->drupalGet('admin/config/development/sync');
+ $this->assertNoText(t('There are no configuration changes.'));
+
+ // Acquire a fake-lock on the import mechanism.
+ $lock_name = 'config_import';
+ lock_acquire($lock_name);
+
+ // Attempt to import configuration and verify that an error message appears.
+ $this->drupalPost(NULL, array(), t('Import'));
+ $this->assertUrl('admin/config/development/sync');
+ $this->assertText(t('The import failed due to an error. Any errors have been logged.'));
+
+ // Release the lock, just to keep testing sane.
+ lock_release($lock_name);
+ }
+}
diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php
index 7ec6d8e..a9676ef 100644
--- a/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php
+++ b/core/modules/config/lib/Drupal/config/Tests/ConfigInstallTest.php
@@ -46,5 +46,14 @@ function testModuleInstallation() {
// Verify that configuration import callback was invoked for the dynamic
// thingie.
$this->assertTrue($GLOBALS['hook_config_import']);
+
+ // Verify that config_test API hooks were invoked for the dynamic default
+ // thingie.
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['load']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['presave']));
+ $this->assertTrue(isset($GLOBALS['hook_config_test_dynamic']['insert']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['update']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['predelete']));
+ $this->assertFalse(isset($GLOBALS['hook_config_test_dynamic']['delete']));
}
}
diff --git a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php
index 2dbc627..e1d77be 100644
--- a/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php
+++ b/core/modules/config/lib/Drupal/config/Tests/Storage/ConfigStorageTestBase.php
@@ -32,6 +32,9 @@
function testCRUD() {
$name = 'config_test.storage';
+ // Checking whether a non-existing name exists returns FALSE.
+ $this->assertIdentical($this->storage->exists($name), FALSE);
+
// Reading a non-existing name returns FALSE.
$data = $this->storage->read($name);
$this->assertIdentical($data, FALSE);
@@ -51,9 +54,13 @@ function testCRUD() {
$data = array('foo' => 'bar');
$result = $this->storage->write($name, $data);
$this->assertIdentical($result, TRUE);
+
$raw_data = $this->read($name);
$this->assertIdentical($raw_data, $data);
+ // Checking whether an existing name exists returns TRUE.
+ $this->assertIdentical($this->storage->exists($name), TRUE);
+
// Writing the identical data again still returns TRUE.
$result = $this->storage->write($name, $data);
$this->assertIdentical($result, TRUE);
diff --git a/core/modules/config/tests/config_test/config/config_test.delete.yml b/core/modules/config/tests/config_test/config/config_test.delete.yml
new file mode 100644
index 0000000..b8ccb67
--- /dev/null
+++ b/core/modules/config/tests/config_test/config/config_test.delete.yml
@@ -0,0 +1 @@
+delete_me: bar
diff --git a/core/modules/config/tests/config_test/config_test.hooks.inc b/core/modules/config/tests/config_test/config_test.hooks.inc
new file mode 100644
index 0000000..2ec6831
--- /dev/null
+++ b/core/modules/config/tests/config_test/config_test.hooks.inc
@@ -0,0 +1,52 @@
+save();
+ $config_test = new ConfigTest($new_config);
+ $config_test->save();
return TRUE;
}
@@ -28,7 +33,9 @@ function config_test_config_import_change($name, $new_config, $old_config) {
// Set a global value we can check in test code.
$GLOBALS['hook_config_import'] = __FUNCTION__;
- $new_config->save();
+ $config_test = new ConfigTest($new_config);
+ $config_test->setOriginal($old_config);
+ $config_test->save();
return TRUE;
}
@@ -44,7 +51,208 @@ function config_test_config_import_delete($name, $new_config, $old_config) {
// Set a global value we can check in test code.
$GLOBALS['hook_config_import'] = __FUNCTION__;
- $old_config->delete();
+ $config_test = new ConfigTest($old_config);
+ $config_test->delete();
return TRUE;
}
+/**
+ * Implements hook_menu().
+ */
+function config_test_menu() {
+ $items['admin/structure/config_test'] = array(
+ 'title' => 'Test configuration',
+ 'page callback' => 'config_test_list_page',
+ 'access callback' => TRUE,
+ );
+ $items['admin/structure/config_test/add'] = array(
+ 'title' => 'Add test configuration',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('config_test_form'),
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_ACTION,
+ );
+ $items['admin/structure/config_test/manage/%config_test'] = array(
+ 'title' => 'Edit test configuration',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('config_test_form', 4),
+ 'access callback' => TRUE,
+ );
+ $items['admin/structure/config_test/manage/%config_test/edit'] = array(
+ 'title' => 'Edit',
+ 'type' => MENU_DEFAULT_LOCAL_TASK,
+ 'weight' => -10,
+ );
+ $items['admin/structure/config_test/manage/%config_test/delete'] = array(
+ 'title' => 'Delete',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('config_test_delete_form', 4),
+ 'access callback' => TRUE,
+ 'type' => MENU_LOCAL_TASK,
+ );
+ return $items;
+}
+
+/**
+ * Loads a ConfigTest object.
+ *
+ * @param string $id
+ * The ID of the ConfigTest object to load.
+ */
+function config_test_load($id) {
+ $config = config('config_test.dynamic.' . $id);
+ if ($config->isNew()) {
+ return FALSE;
+ }
+ return new ConfigTest($config);
+}
+
+/**
+ * Saves a ConfigTest object.
+ *
+ * @param Drupal\config_test\ConfigTest $config_test
+ * The ConfigTest object to save.
+ */
+function config_test_save(ConfigTest $config_test) {
+ return $config_test->save();
+}
+
+/**
+ * Deletes a ConfigTest object.
+ *
+ * @param string $id
+ * The ID of the ConfigTest object to delete.
+ */
+function config_test_delete($id) {
+ $config = config('config_test.dynamic.' . $id);
+ $config_test = new ConfigTest($config);
+ return $config_test->delete();
+}
+
+/**
+ * Page callback; Lists available ConfigTest objects.
+ */
+function config_test_list_page() {
+ $config_names = config_get_storage_names_with_prefix('config_test.dynamic.');
+ $rows = array();
+ foreach ($config_names as $config_name) {
+ $config_test = new ConfigTest(config($config_name));
+ $row = array();
+ $row['name']['data'] = array(
+ '#type' => 'link',
+ '#title' => $config_test->getLabel(),
+ '#href' => $config_test->getUri(),
+ );
+ $row['delete']['data'] = array(
+ '#type' => 'link',
+ '#title' => t('Delete'),
+ '#href' => $config_test->getUri() . '/delete',
+ );
+ $rows[] = $row;
+ }
+ $build = array(
+ '#theme' => 'table',
+ '#header' => array('Name', 'Operations'),
+ '#rows' => $rows,
+ '#empty' => format_string('No test configuration defined. Add some', array(
+ '@add-url' => url('admin/structure/config_test/add'),
+ )),
+ );
+ return $build;
+}
+
+/**
+ * Form constructor to add or edit a ConfigTest object.
+ *
+ * @param Drupal\config_test\ConfigTest $config_test
+ * (optional) An existing ConfigTest object to edit. If omitted, the form
+ * creates a new ConfigTest.
+ */
+function config_test_form($form, &$form_state, ConfigTest $config_test = NULL) {
+ if (!isset($config_test)) {
+ $config_test = new ConfigTest(config(NULL));
+ }
+ $form_state['config_test'] = $config_test;
+
+ $form['label'] = array(
+ '#type' => 'textfield',
+ '#title' => 'Label',
+ '#default_value' => $config_test->getLabel(),
+ '#required' => TRUE,
+ );
+ $form['id'] = array(
+ '#type' => 'machine_name',
+ '#default_value' => $config_test->getID(),
+ '#required' => TRUE,
+ '#machine_name' => array(
+ 'exists' => 'config_test_load',
+ // @todo Update form_process_machine_name() to use 'label' by default.
+ 'source' => array('label'),
+ ),
+ );
+ $form['style'] = array(
+ '#type' => 'select',
+ '#title' => 'Image style',
+ '#options' => array(),
+ '#default_value' => $config_test->get('style'),
+ '#access' => FALSE,
+ );
+ if (module_exists('image')) {
+ $form['style']['#access'] = TRUE;
+ $form['style']['#options'] = image_style_options();
+ }
+
+ $form['actions'] = array('#type' => 'actions');
+ $form['actions']['submit'] = array('#type' => 'submit', '#value' => 'Save');
+
+ return $form;
+}
+
+/**
+ * Form submission handler for config_test_form().
+ */
+function config_test_form_submit($form, &$form_state) {
+ form_state_values_clean($form_state);
+
+ $config_test = $form_state['config_test'];
+
+ foreach ($form_state['values'] as $key => $value) {
+ $config_test->set($key, $value);
+ }
+ $config_test->save();
+
+ if (!empty($config_test->original)) {
+ drupal_set_message(format_string('%label configuration has been updated.', array('%label' => $config_test->getLabel())));
+ }
+ else {
+ drupal_set_message(format_string('%label configuration has been created.', array('%label' => $config_test->getLabel())));
+ }
+
+ $form_state['redirect'] = 'admin/structure/config_test';
+}
+
+/**
+ * Form constructor to delete a ConfigTest object.
+ *
+ * @param Drupal\config_test\ConfigTest $config_test
+ * The ConfigTest object to delete.
+ */
+function config_test_delete_form($form, &$form_state, ConfigTest $config_test) {
+ $form_state['config_test'] = $config_test;
+
+ $form['id'] = array('#type' => 'value', '#value' => $config_test->getID());
+ return confirm_form($form,
+ format_string('Are you sure you want to delete %label', array('%label' => $config_test->getLabel())),
+ 'admin/structure/config_test',
+ NULL,
+ 'Delete'
+ );
+}
+
+/**
+ * Form submission handler for config_test_delete_form().
+ */
+function config_test_delete_form_submit($form, &$form_state) {
+ $form_state['config_test']->delete();
+ $form_state['redirect'] = 'admin/structure/config_test';
+}
diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php
new file mode 100644
index 0000000..4efaae5
--- /dev/null
+++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTest.php
@@ -0,0 +1,36 @@
+getID();
+ }
+}
diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php
index b96049e..99fd422 100644
--- a/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php
+++ b/core/modules/search/lib/Drupal/search/Tests/SearchExcerptTest.php
@@ -7,12 +7,14 @@
namespace Drupal\search\Tests;
-use Drupal\simpletest\UnitTestBase;
+use Drupal\simpletest\WebTestBase;
/**
* Tests the search_excerpt() function.
*/
-class SearchExcerptTest extends UnitTestBase {
+class SearchExcerptTest extends WebTestBase {
+ public static $modules = array('search');
+
public static function getInfo() {
return array(
'name' => 'Search excerpt extraction',
@@ -21,11 +23,6 @@ public static function getInfo() {
);
}
- function setUp() {
- drupal_load('module', 'search');
- parent::setUp();
- }
-
/**
* Tests search_excerpt() with several simulated search keywords.
*
diff --git a/profiles/standard/standard.info b/profiles/standard/standard.info
index 8b8a33b..c5240dd 100644
--- a/profiles/standard/standard.info
+++ b/profiles/standard/standard.info
@@ -6,6 +6,7 @@ dependencies[] = node
dependencies[] = block
dependencies[] = color
dependencies[] = comment
+dependencies[] = config
dependencies[] = contextual
dependencies[] = help
dependencies[] = image