diff --git a/core/includes/config.inc b/core/includes/config.inc index c143dd4..6919bfe 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -1,7 +1,6 @@ $module_config_dir)); - $target_storage = new DatabaseStorage(); + $target_storage = drupal_container()->get('config.storage'); $null_storage = new NullStorage(); // Upon module installation, only new config objects need to be created. @@ -45,8 +44,7 @@ function config_install_default_config($module) { * @todo Modules need a way to access the active store, whatever it is. */ function config_get_storage_names_with_prefix($prefix = '') { - $storage = new DatabaseStorage(); - return $storage->listAll($prefix); + return drupal_container()->get('config.storage')->listAll($prefix); } /** @@ -129,17 +127,16 @@ function config_sync_changes(array $config_changes, StorageInterface $source_sto } /** - * Imports configuration from FileStorage to DatabaseStorage. + * Imports configuration from FileStorage to 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. + // Retrieve a list of differences between FileStorage and the active store. $source_storage = new FileStorage(); - $target_storage = new DatabaseStorage(); + $target_storage = drupal_container()->get('config.storage'); $config_changes = config_sync_get_changes($source_storage, $target_storage); if (empty($config_changes)) { @@ -183,8 +180,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 @@ -197,13 +192,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); @@ -220,12 +215,11 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou } /** - * Exports configuration from DatabaseStorage to FileStorage. + * Exports configuration from the active store to FileStorage. */ function config_export() { - // Retrieve a list of differences between DatabaseStorage and FileStorage. - // @todo Leverage DI + config.storage.info. - $source_storage = new DatabaseStorage(); + // Retrieve a list of differences between the active store and FileStorage. + $source_storage = drupal_container()->get('config.storage'); $target_storage = new FileStorage(); $config_changes = config_sync_get_changes($source_storage, $target_storage); diff --git a/core/includes/form.inc b/core/includes/form.inc index 6a747b6..846e684 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 f40161d..bed5bc9 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. @@ -628,19 +628,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 a749a4b..3b97b69 100644 --- a/core/lib/Drupal/Core/Config/Config.php +++ b/core/lib/Drupal/Core/Config/Config.php @@ -34,21 +34,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; } /** @@ -222,7 +222,7 @@ class Config { * 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()); @@ -239,7 +239,7 @@ class Config { */ 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; } @@ -268,8 +268,9 @@ class Config { * 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..5e93c69 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactory.php +++ b/core/lib/Drupal/Core/Config/ConfigFactory.php @@ -15,29 +15,28 @@ namespace Drupal\Core\Config; * * @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 to use 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 +67,7 @@ class ConfigFactory { // @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 @@ class DatabaseStorage implements StorageInterface { } /** + * 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 @@ class DatabaseStorage implements StorageInterface { // 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 @@ class FileStorage implements StorageInterface { } /** - * 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 @@ class NullStorage implements StorageInterface { } /** + * 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 @@ interface StorageInterface { 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 @@ +register(LANGUAGE_TYPE_CONTENT, 'Drupal\\Core\\Language\\Language'); - // Register configuration storage dispatcher. - $this->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. + $this->setParameter('config.storage.options', array( +// 'Drupal\Core\Config\DatabaseStorage' => array( +// 'connection' => 'default', +// 'target' => 'default', +// ), 'Drupal\Core\Config\FileStorage' => array( 'directory' => config_get_config_directory(), - 'read' => TRUE, - 'write' => FALSE, + ), + 'Drupal\Core\Config\CacheStorage' => array( + // @todo Add and replace with 'config' default bin. + 'bin' => 'cache', ), )); - $this->register('config.storage.dispatcher', 'Drupal\Core\Config\StorageDispatcher') - ->addArgument('%config.storage.info%'); + // @todo Use this for exporting your current config in the DB... ;) + #$this->register('config.storage', 'Drupal\Core\Config\MultiStorage') + $this->register('config.storage', 'Drupal\Core\Config\CachedFileStorage') + ->addArgument('%config.storage.options%'); // Register configuration object factory. $this->register('config.factory', 'Drupal\Core\Config\ConfigFactory') - ->addArgument(new Reference('config.storage.dispatcher')); + ->addArgument(new Reference('config.storage')); // Register the HTTP kernel services. $this->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher') diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc new file mode 100644 index 0000000..30cc090 --- /dev/null +++ b/core/modules/config/config.admin.inc @@ -0,0 +1,129 @@ + 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() + */ +function config_admin_import_form($form, &$form_state) { + // Retrieve a list of differences between FileStorage and the active store. + $source_storage = new FileStorage(); + $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() + */ +function config_admin_export_form($form, &$form_state) { + // Retrieve a list of differences between the active store and FileStorage. + $source_storage = drupal_container()->get('config.storage'); + $target_storage = new FileStorage(); + + 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 @@ class ConfigImportTest extends WebTestBase { 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 @@ class ConfigImportTest extends WebTestBase { $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 @@ class ConfigImportTest extends WebTestBase { $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 @@ class ConfigImportTest extends WebTestBase { // 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 @@ class ConfigImportTest extends WebTestBase { // 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 @@ class ConfigImportTest extends WebTestBase { $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 @@ class ConfigInstallTest extends WebTestBase { // 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 @@ abstract class ConfigStorageTestBase extends WebTestBase { 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 @@ abstract class ConfigStorageTestBase extends WebTestBase { $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/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