diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index b358269..e483104 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -287,8 +287,6 @@ const REGISTRY_WRITE_LOOKUP_CACHE = 2; */ const DRUPAL_PHP_FUNCTION_PATTERN = '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'; -require_once DRUPAL_ROOT . '/core/includes/config.inc'; - /** * Starts the timer with the specified name. * @@ -2264,6 +2262,9 @@ function _drupal_bootstrap_configuration() { // Activate the class loader. drupal_classloader(); + + // Load the procedural configuration system helper functions. + require_once DRUPAL_ROOT . '/core/includes/config.inc'; } /** diff --git a/core/includes/config.inc b/core/includes/config.inc index 8d0eea1..ffd78e7 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -1,7 +1,10 @@ $file) { - // Load config data into the active store and write it out to the - // file system in the drupal config directory. Note the config name - // needs to be the same as the file name WITHOUT the extension. - $config_name = basename($file, '.' . FileStorage::getFileExtension()); - - $database_storage = new DatabaseStorage($config_name); - $file_storage = new FileStorage($config_name); - $file_storage->setPath($module_config_dir); - $database_storage->write($file_storage->read()); + if (is_dir($module_config_dir)) { + $database_storage = new DatabaseStorage(); + $module_file_storage = new FileStorage(array('directory' => $module_config_dir)); + + foreach ($module_file_storage->getNamesWithPrefix() as $config_name) { + $data = $module_file_storage->read($config_name); + $database_storage->write($config_name, $data); } } } /** - * @todo http://drupal.org/node/1552396 renames this into config_load_all(). + * @todo Modules need a way to access the active store, whatever it is. */ function config_get_storage_names_with_prefix($prefix = '') { - return DatabaseStorage::getNamesWithPrefix($prefix); + $storage = new DatabaseStorage(); + return $storage->getNamesWithPrefix($prefix); } /** @@ -73,16 +71,214 @@ function config_get_storage_names_with_prefix($prefix = '') { * The name of the configuration object to retrieve. The name corresponds to * a configuration file. For @code config(book.admin) @endcode, the config * object returned will contain the contents of book.admin configuration file. - * @param $class - * The class name of the config object to be returned. Defaults to - * DrupalConfig. * - * @return - * An instance of the class specified in the $class parameter. + * @return Drupal\Core\Config\ConfigObject + * A configuration object. + */ +function config($name) { + return drupal_container()->get('config.factory')->get()->setName($name)->load(); +} + +/** + * Returns a list of differences between configuration storages. * - * @todo Replace this with an appropriate factory / ability to inject in - * alternate storage engines.. + * @param Drupal\Core\Config\StorageInterface $source_storage + * The storage to synchronize configuration from. + * @param Drupal\Core\Config\StorageInterface $target_storage + * The storage to synchronize configuration to. + * + * @return array|bool + * An assocative array containing the differences between source and target + * storage, or FALSE if there are no differences. */ -function config($name, $class = 'Drupal\Core\Config\DrupalConfig') { - return new $class(new DatabaseStorage($name)); +function config_sync_get_changes(StorageInterface $source_storage, StorageInterface $target_storage) { + $source_names = $source_storage->getNamesWithPrefix(); + $target_names = $target_storage->getNamesWithPrefix(); + $config_changes = array( + 'create' => array_diff($source_names, $target_names), + 'change' => array(), + 'delete' => array_diff($target_names, $source_names), + ); + foreach (array_intersect($source_names, $target_names) as $name) { + $source_config_data = $source_storage->read($name); + $target_config_data = $target_storage->read($name); + if ($source_config_data != $target_config_data) { + $config_changes['change'][] = $name; + } + } + + // Do not trigger subsequent synchronization operations if there are no + // changes in either category. + if (empty($config_changes['create']) && empty($config_changes['change']) && empty($config_changes['delete'])) { + return FALSE; + } + return $config_changes; } + +/** + * Imports configuration from FileStorage to DatabaseStorage. + */ +function config_import() { + $config_changes = config_import_get_changes(); + if (empty($config_changes)) { + return; + } + + if (!lock_acquire(__FUNCTION__)) { + // Another request is synchronizing configuration. + // Return a negative result for UI purposes. We do not make a difference + // between an actual synchronization error and a failed lock, because a + // concurrent request synchronizing configuration is an edge-case in the + // first place and would mean that more than one developer or site builder + // attempts to do it without coordinating with others. + return FALSE; + } + + try { + $remaining_changes = config_import_invoke_sync_hooks($config_changes); + config_import_save_changes($remaining_changes); + // Flush all caches and reset static variables after a successful import. + drupal_flush_all_caches(); + } + catch (ConfigException $e) { + watchdog_exception('config_import', $e); + lock_release(__FUNCTION__); + return FALSE; + } + lock_release(__FUNCTION__); + return TRUE; +} + +/** + * Returns a list of differences between FileStorage and DatabaseStorage. + * + * @see config_sync_get_changes() + */ +function config_import_get_changes() { + // @todo Leverage DI + config.storage.info. + $source_storage = new FileStorage(); + $target_storage = new DatabaseStorage(); + + return config_sync_get_changes($source_storage, $target_storage); +} + +/** + * Writes an array of config file changes to the active store. + * + * @param array $config_changes + * An array of changes to be written. + */ +function config_import_save_changes(array $config_changes) { + // @todo Leverage DI + config.storage.info. + $source_storage = new FileStorage(); + $target_storage = new DatabaseStorage(); + foreach (array('delete', 'create', 'change') as $op) { + foreach ($config_changes[$op] as $name) { + if ($op == 'delete') { + $target_storage->delete($name); + } + else { + $data = $source_storage->read($name); + $target_storage->write($name, $data); + } + } + } +} + +/** + * Invokes hook_config_import() implementations for configuration changes. + * + * @param array $config_changes + * An array of changes to be loaded. + */ +function config_import_invoke_sync_hooks(array $config_changes) { + // @todo Leverage DI + config.storage.info. + $source_storage = new FileStorage(); + $target_storage = new DatabaseStorage(); + $storage_manager = drupal_container()->get('config.manager'); + + // 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 + // handle dependencies correctly. + foreach (array('delete', 'create', 'change') as $op) { + foreach ($config_changes[$op] as $key => $name) { + // Extract owner from configuration object name. + $module = strtok($name, '.'); + // Check whether the module implements hook_config_import() and ask it to + // handle the configuration change. + $handled_by_module = FALSE; + if (module_hook($module, 'config_import')) { + $old_config = new ConfigObject($storage_manager); + $old_config->setName($name); + try { + $old_config->load(); + } + catch (ConfigException $e) { + } + try { + $data = $source_storage->read($name); + } + catch (ConfigException $e) { + $data = array(); + } + $new_config = new ConfigObject($storage_manager); + $new_config->setName($name)->setData($data); + $handled_by_module = module_invoke($module, 'config_import', $op, $name, $new_config, $old_config); + } + if (!empty($handled_by_module)) { + unset($config_changes[$op][$key]); + } + } + } + return $config_changes; +} + +/** + * Exports configuration from DatabaseStorage to FileStorage. + */ +function config_export() { + $config_changes = config_export_get_changes(); + if (empty($config_changes)) { + return; + } + config_export_save_changes($config_changes); + return TRUE; +} + +/** + * Returns a list of differences between DatabaseStorage and FileStorage. + * + * @see config_sync_get_changes() + */ +function config_export_get_changes() { + // @todo Leverage DI + config.storage.info. + $source_storage = new DatabaseStorage(); + $target_storage = new FileStorage(); + + return config_sync_get_changes($source_storage, $target_storage); +} + +/** + * Writes an array of configuration changes to FileStorage. + * + * @param array $config_changes + * An array of changes to be written. + */ +function config_export_save_changes(array $config_changes) { + // @todo Leverage DI + config.storage.info. + $source_storage = new DatabaseStorage(); + $target_storage = new FileStorage(); + foreach (array('delete', 'create', 'change') as $op) { + foreach ($config_changes[$op] as $name) { + if ($op == 'delete') { + $target_storage->delete($name); + } + else { + $data = $source_storage->read($name); + $target_storage->write($name, $data); + } + } + } +} + diff --git a/core/includes/install.inc b/core/includes/install.inc index 1ebfb21..0266a8d 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -1,7 +1,7 @@ delete(); - } + // Remove all configuration belonging to the module. + $config_names = $storage->getNamesWithPrefix($module . '.'); + foreach ($config_names as $config_name) { + config($config_name)->delete(); } watchdog('system', '%module module uninstalled.', array('%module' => $module), WATCHDOG_INFO); diff --git a/core/includes/module.inc b/core/includes/module.inc index 928abc9..eb69bf5 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -487,7 +487,7 @@ function module_enable($module_list, $enable_dependencies = TRUE) { $versions = drupal_get_schema_versions($module); $version = $versions ? max($versions) : SCHEMA_INSTALLED; - // Copy any default configuration data to the system config directory/ + // Install default configuration of the module. config_install_default_config($module); // If the module has no current updates, but has some that were diff --git a/core/includes/update.inc b/core/includes/update.inc index f711507..50b8ae5 100644 --- a/core/includes/update.inc +++ b/core/includes/update.inc @@ -900,11 +900,10 @@ function update_variables_to_config($config_name, array $variable_map) { $module = strtok($config_name, '.'); // Load and set default configuration values. - // Throws a FileStorageReadException if there is no default configuration - // file, which is required to exist. - $file = new FileStorage($config_name); - $file->setPath(drupal_get_path('module', $module) . '/config'); - $default_data = $file->read(); + // Throws a StorageException if there is no default configuration file, which + // is required to exist. + $file = new FileStorage(array('directory' => drupal_get_path('module', $module) . '/config')); + $default_data = $file->read($config_name); // Merge any possibly existing original data into default values. // Only relevant when being called repetitively on the same config object. diff --git a/core/lib/Drupal/Core/Config/ConfigException.php b/core/lib/Drupal/Core/Config/ConfigException.php index c60a449..8a81b6e 100644 --- a/core/lib/Drupal/Core/Config/ConfigException.php +++ b/core/lib/Drupal/Core/Config/ConfigException.php @@ -1,8 +1,15 @@ container = $container; + } + + /** + * Returns a configuration object for a given name. + * + * @param string $name + * The name of the configuration object to construct. + * + * @return Drupal\Core\Config\ConfigObject + * A configuration object with the given $name. + */ + public function get() { + // "Caching" the instantiated config object cuts off a fair amount of CPU + // time and memory. Only the data within the configuration object changes, + // so the additional cost of instantiating duplicate objects can be happily + // avoided. It is not uncommon for a configuration object to be retrieved + // many times during a single request; e.g., 'system.performance' alone is + // retrieved around 10-20 times within a single page request. Sub-requests + // via HttpKernel will most likely only increase these counts. + // @todo Benchmarks were performed with a script that essentially retained + // all instantiated configuration objects in memory until script execution + // ended. A variant of that script called config() within a helper + // function only, which inherently meant that PHP destroyed all + // configuration objects after leaving the function. Consequently, + // benchmark results looked entirely different. Profiling should probably + // redone under more realistic conditions; e.g., actual HTTP requests. + // @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). + if (!is_object($this->configObject)) { + $class = $this->container->getParameter('config.object'); + $this->configObject = new $class($this->container->get('config.manager')); + } + return $this->configObject; + } +} diff --git a/core/lib/Drupal/Core/Config/ConfigObject.php b/core/lib/Drupal/Core/Config/ConfigObject.php new file mode 100644 index 0000000..583af92 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigObject.php @@ -0,0 +1,250 @@ +storageManager = $storageManager; + } + + /** + * Returns the name of this configuration object. + */ + public function getName() { + return $this->name; + } + + /** + * Sets the name of this configuration object. + */ + public function setName($name) { + $this->name = $name; + return $this; + } + + /** + * Gets data from this config object. + * + * @param $key + * A string that maps to a key within the configuration data. + * For instance in the following configuation array: + * @code + * array( + * 'foo' => array( + * 'bar' => 'baz', + * ), + * ); + * @endcode + * A key of 'foo.bar' would return the string 'baz'. However, a key of 'foo' + * would return array('bar' => 'baz'). + * If no key is specified, then the entire data array is returned. + * + * The configuration system does not retain data types. Every saved value is + * casted to a string. In most cases this is not an issue; however, it can + * cause issues with Booleans, which are casted to "1" (TRUE) or "0" (FALSE). + * In particular, code relying on === or !== will no longer function properly. + * + * @see http://php.net/manual/language.operators.comparison.php. + * + * @return + * The data that was requested. + */ + public function get($key = '') { + global $conf; + + $name = $this->getName(); + if (isset($conf[$name])) { + $merged_data = drupal_array_merge_deep($this->data, $conf[$name]); + } + else { + $merged_data = $this->data; + } + + if (empty($key)) { + return $merged_data; + } + else { + $parts = explode('.', $key); + if (count($parts) == 1) { + return isset($merged_data[$key]) ? $merged_data[$key] : NULL; + } + else { + $key_exists = NULL; + $value = drupal_array_get_nested_value($merged_data, $parts, $key_exists); + return $key_exists ? $value : NULL; + } + } + } + + /** + * Replaces the data of this configuration object. + * + * @param array $data + * The new configuration data. + */ + public function setData(array $data) { + $this->data = $data; + return $this; + } + + /** + * Sets value in this config object. + * + * @param $key + * @todo + * @param $value + * @todo + */ + public function set($key, $value) { + // Type-cast value into a string. + $value = $this->castValue($value); + + // The dot/period is a reserved character; it may appear between keys, but + // not within keys. + $parts = explode('.', $key); + if (count($parts) == 1) { + $this->data[$key] = $value; + } + else { + drupal_array_set_nested_value($this->data, $parts, $value); + } + return $this; + } + + /** + * Casts a saved value to a string. + * + * The configuration system only saves strings or arrays. Any scalar + * non-string value is cast to a string. The one exception is boolean FALSE + * which would normally become '' when cast to a string, but is manually + * cast to '0' here for convenience and consistency. + * + * Any non-scalar value that is not an array (aka objects) gets cast + * to an array. + * + * @param $value + * A value being saved into the configuration system. + * @param $value + * The value cast to a string or array. + */ + public function castValue($value) { + if (is_scalar($value)) { + // Handle special case of FALSE, which should be '0' instead of ''. + if ($value === FALSE) { + $value = '0'; + } + else { + $value = (string) $value; + } + } + else { + // Any non-scalar value must be an array. + if (!is_array($value)) { + $value = (array) $value; + } + // Recurse into any nested keys. + foreach ($value as $key => $nested_value) { + $value[$key] = $this->castValue($nested_value); + } + } + return $value; + } + + /** + * Unsets value in this config object. + * + * @param $key + * Name of the key whose value should be unset. + */ + public function clear($key) { + $parts = explode('.', $key); + if (count($parts) == 1) { + unset($this->data[$key]); + } + else { + drupal_array_unset_nested_value($this->data, $parts); + } + return $this; + } + + /** + * Loads configuration data into this object. + */ + public function load() { + $this->setData(array()); + $data = $this->storageManager->selectStorage('read', $this->name)->read($this->name); + if ($data !== FALSE) { + $this->setData($data); + } + return $this; + } + + /** + * Saves the configuration object. + */ + public function save() { + $this->sortByKey($this->data); + $this->storageManager->selectStorage('write', $this->name)->write($this->name, $this->data); + return $this; + } + + /** + * Sorts all keys in configuration data. + * + * Ensures that re-inserted keys appear in the same location as before, in + * order to ensure an identical order regardless of storage controller. + * A consistent order is important for any storage that allows any kind of + * diff operation. + * + * @param array $data + * An associative array to sort recursively by key name. + */ + public function sortByKey(array &$data) { + ksort($data); + foreach ($data as &$value) { + if (is_array($value)) { + $this->sortByKey($value); + } + } + } + + /** + * Deletes the configuration object. + */ + public function delete() { + $this->data = array(); + $this->storageManager->selectStorage('write', $this->name)->delete($this->name); + return $this; + } +} diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index c736245..cb1d925 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -2,25 +2,52 @@ namespace Drupal\Core\Config; -use Drupal\Core\Config\StorageBase; +use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Database\Database; use Exception; /** - * Represents an SQL-based configuration storage object. + * Defines the Database storage controller. */ -class DatabaseStorage extends StorageBase { +class DatabaseStorage implements StorageInterface { + + /** + * Database connection options for this storage controller. + * + * - target: The connection to use for storage operations. + * + * @var array + */ + protected $options; + + /** + * Implements StorageInterface::__construct(). + */ + public function __construct(array $options = array()) { + $options += array( + 'target' => 'default', + ); + $this->options = $options; + } + + /** + * Returns the database connection to use. + */ + protected function getConnection() { + return Database::getConnection($this->options['target']); + } /** * Implements StorageInterface::read(). */ - public function read() { + public function read($name) { // There are situations, like in the installer, where we may attempt a // read without actually having the database available. In this case, // catch the exception and just return an empty array so the caller can // handle it if need be. $data = array(); try { - $raw = db_query('SELECT data FROM {config} WHERE name = :name', array(':name' => $this->name))->fetchField(); + $raw = $this->getConnection()->query('SELECT data FROM {config} WHERE name = :name', array(':name' => $name), $this->options)->fetchField(); if ($raw !== FALSE) { $data = $this->decode($raw); } @@ -31,22 +58,22 @@ class DatabaseStorage extends StorageBase { } /** - * Implements StorageInterface::writeToActive(). + * Implements StorageInterface::write(). */ - public function writeToActive($data) { + public function write($name, array $data) { $data = $this->encode($data); - return db_merge('config') - ->key(array('name' => $this->name)) + return $this->getConnection()->merge('config', $this->options) + ->key(array('name' => $name)) ->fields(array('data' => $data)) ->execute(); } /** - * @todo + * Implements StorageInterface::delete(). */ - public function deleteFromActive() { - db_delete('config') - ->condition('name', $this->name) + public function delete($name) { + $this->getConnection()->delete('config', $this->options) + ->condition('name', $name) ->execute(); } @@ -67,7 +94,9 @@ class DatabaseStorage extends StorageBase { /** * Implements StorageInterface::getNamesWithPrefix(). */ - static public function getNamesWithPrefix($prefix = '') { - return db_query('SELECT name FROM {config} WHERE name LIKE :name', array(':name' => db_like($prefix) . '%'))->fetchCol(); + public function getNamesWithPrefix($prefix = '') { + return $this->getConnection()->query('SELECT name FROM {config} WHERE name LIKE :name', array( + ':name' => db_like($prefix) . '%', + ), $this->options)->fetchCol(); } } diff --git a/core/lib/Drupal/Core/Config/DrupalConfig.php b/core/lib/Drupal/Core/Config/DrupalConfig.php deleted file mode 100644 index f5a9220..0000000 --- a/core/lib/Drupal/Core/Config/DrupalConfig.php +++ /dev/null @@ -1,220 +0,0 @@ -storage = $storage; - $this->read(); - } - - /** - * Reads config data from the active store into our object. - */ - public function read() { - $data = $this->storage->read(); - $this->setData($data !== FALSE ? $data : array()); - return $this; - } - - /** - * Checks whether a particular value is overridden. - * - * @param $key - * @todo - * - * @return - * @todo - */ - public function isOverridden($key) { - return isset($this->_overrides[$key]); - } - - /** - * Gets data from this config object. - * - * @param $key - * A string that maps to a key within the configuration data. - * For instance in the following configuation array: - * @code - * array( - * 'foo' => array( - * 'bar' => 'baz', - * ), - * ); - * @endcode - * A key of 'foo.bar' would return the string 'baz'. However, a key of 'foo' - * would return array('bar' => 'baz'). - * If no key is specified, then the entire data array is returned. - * - * The configuration system does not retain data types. Every saved value is - * casted to a string. In most cases this is not an issue; however, it can - * cause issues with Booleans, which are casted to "1" (TRUE) or "0" (FALSE). - * In particular, code relying on === or !== will no longer function properly. - * - * @see http://php.net/manual/language.operators.comparison.php. - * - * @return - * The data that was requested. - */ - public function get($key = '') { - global $conf; - - $name = $this->storage->getName(); - if (isset($conf[$name])) { - $merged_data = drupal_array_merge_deep($this->data, $conf[$name]); - } - else { - $merged_data = $this->data; - } - - if (empty($key)) { - return $merged_data; - } - else { - $parts = explode('.', $key); - if (count($parts) == 1) { - return isset($merged_data[$key]) ? $merged_data[$key] : NULL; - } - else { - $key_exists = NULL; - $value = drupal_array_get_nested_value($merged_data, $parts, $key_exists); - return $key_exists ? $value : NULL; - } - } - } - - /** - * Replaces the data of this configuration object. - * - * @param array $data - * The new configuration data. - */ - public function setData(array $data) { - $this->data = $data; - return $this; - } - - /** - * Sets value in this config object. - * - * @param $key - * @todo - * @param $value - * @todo - */ - public function set($key, $value) { - // Type-cast value into a string. - $value = $this->castValue($value); - - // The dot/period is a reserved character; it may appear between keys, but - // not within keys. - $parts = explode('.', $key); - if (count($parts) == 1) { - $this->data[$key] = $value; - } - else { - drupal_array_set_nested_value($this->data, $parts, $value); - } - return $this; - } - - /** - * Casts a saved value to a string. - * - * The configuration system only saves strings or arrays. Any scalar - * non-string value is cast to a string. The one exception is boolean FALSE - * which would normally become '' when cast to a string, but is manually - * cast to '0' here for convenience and consistency. - * - * Any non-scalar value that is not an array (aka objects) gets cast - * to an array. - * - * @param $value - * A value being saved into the configuration system. - * @param $value - * The value cast to a string or array. - */ - public function castValue($value) { - if (is_scalar($value)) { - // Handle special case of FALSE, which should be '0' instead of ''. - if ($value === FALSE) { - $value = '0'; - } - else { - $value = (string) $value; - } - } - else { - // Any non-scalar value must be an array. - if (!is_array($value)) { - $value = (array) $value; - } - // Recurse into any nested keys. - foreach ($value as $key => $nested_value) { - $value[$key] = $this->castValue($nested_value); - } - } - return $value; - } - - /** - * Unsets value in this config object. - * - * @param $key - * Name of the key whose value should be unset. - */ - public function clear($key) { - $parts = explode('.', $key); - if (count($parts) == 1) { - unset($this->data[$key]); - } - else { - drupal_array_unset_nested_value($this->data, $parts); - } - } - - /** - * Saves the configuration object. - */ - public function save() { - $this->storage->write($this->data); - } - - /** - * Deletes the configuration object. - */ - public function delete() { - $this->data = array(); - $this->storage->delete(); - } -} diff --git a/core/lib/Drupal/Core/Config/FileStorage.php b/core/lib/Drupal/Core/Config/FileStorage.php index 2a6d448..bafd099 100644 --- a/core/lib/Drupal/Core/Config/FileStorage.php +++ b/core/lib/Drupal/Core/Config/FileStorage.php @@ -2,58 +2,31 @@ namespace Drupal\Core\Config; +use Drupal\Core\Config\StorageInterface; use Symfony\Component\Yaml\Yaml; /** - * Represents the file storage controller. - * - * @todo Implement StorageInterface after removing DrupalConfig methods. - * @todo Consider to extend StorageBase. + * Defines the file storage controller. */ -class FileStorage { +class FileStorage implements StorageInterface { /** - * The name of the configuration object. + * Configuration options for this storage controller. * - * @var string - */ - protected $name; - - /** - * The filesystem path containing the configuration object. + * - directory: The filesystem path for configuration objects. * - * @var string + * @var array */ - protected $path; + protected $options; /** * Implements StorageInterface::__construct(). */ - public function __construct($name = NULL) { - $this->name = $name; - } - - /** - * Returns the path containing the configuration file. - * - * @return string - * The relative path to the configuration object. - */ - public function getPath() { - // If the path has not been set yet, retrieve and assign the default path - // for configuration files. - if (!isset($this->path)) { - $this->setPath(config_get_config_directory()); + public function __construct(array $options = array()) { + if (!isset($options['directory'])) { + $options['directory'] = config_get_config_directory(); } - return $this->path; - } - - /** - * Sets the path containing the configuration file. - */ - public function setPath($directory) { - $this->path = $directory; - return $this; + $this->options = $options; } /** @@ -62,8 +35,8 @@ class FileStorage { * @return string * The path to the configuration file. */ - public function getFilePath() { - return $this->getPath() . '/' . $this->getName() . '.' . self::getFileExtension(); + public function getFilePath($name) { + return $this->options['directory'] . '/' . $name . '.' . self::getFileExtension(); } /** @@ -82,36 +55,38 @@ class FileStorage { * @return bool * TRUE if the configuration file exists, FALSE otherwise. */ - protected function exists() { - return file_exists($this->getFilePath()); + public function exists($name) { + return file_exists($this->getFilePath($name)); } /** * Implements StorageInterface::write(). * - * @throws FileStorageException + * @throws StorageException */ - public function write($data) { + public function write($name, array $data) { $data = $this->encode($data); - if (!file_put_contents($this->getFilePath(), $data)) { - throw new FileStorageException('Failed to write configuration file: ' . $this->getFilePath()); + if (!file_put_contents($this->getFilePath($name), $data)) { + throw new StorageException('Failed to write configuration file: ' . $this->getFilePath($name)); } + return $this; } /** * Implements StorageInterface::read(). * - * @throws FileStorageReadException + * @throws StorageException */ - public function read() { - if (!$this->exists()) { - throw new FileStorageReadException("Configuration file '$this->name' does not exist."); + public function read($name) { + // @todo DatabaseStorage does not throw an exception if $name does not exist. + if (!$this->exists($name)) { + throw new StorageException("Configuration file '$name' does not exist."); } - $data = file_get_contents($this->getFilePath()); + $data = file_get_contents($this->getFilePath($name)); $data = $this->decode($data); if ($data === FALSE) { - throw new FileStorageReadException("Failed to decode configuration file '$this->name'."); + throw new StorageException("Failed to decode configuration file '$name'."); } return $data; } @@ -119,9 +94,9 @@ class FileStorage { /** * Deletes a configuration file. */ - public function delete() { - // Needs error handling and etc. - @drupal_unlink($this->getFilePath()); + public function delete($name) { + // @todo Error handling. + return @drupal_unlink($this->getFilePath($name)); } /** @@ -144,28 +119,13 @@ class FileStorage { } /** - * Implements StorageInterface::getName(). - */ - public function getName() { - return $this->name; - } - - /** - * Implements StorageInterface::setName(). - */ - public function setName($name) { - $this->name = $name; - } - - /** * Implements StorageInterface::getNamesWithPrefix(). */ - public static function getNamesWithPrefix($prefix = '') { - // @todo Use $this->getPath() to allow for contextual search of files in - // custom paths. - $files = glob(config_get_config_directory() . '/' . $prefix . '*.' . FileStorage::getFileExtension()); - $clean_name = function ($value) { - return basename($value, '.' . FileStorage::getFileExtension()); + public function getNamesWithPrefix($prefix = '') { + $extension = '.' . self::getFileExtension(); + $files = glob($this->options['directory'] . '/' . $prefix . '*' . $extension); + $clean_name = function ($value) use ($extension) { + return basename($value, $extension); }; return array_map($clean_name, $files); } diff --git a/core/lib/Drupal/Core/Config/FileStorageException.php b/core/lib/Drupal/Core/Config/FileStorageException.php deleted file mode 100644 index bf3ae5f..0000000 --- a/core/lib/Drupal/Core/Config/FileStorageException.php +++ /dev/null @@ -1,10 +0,0 @@ -name = $name; - } - - /** - * Instantiates a new file storage object or returns the existing one. - * - * @return Drupal\Core\Config\FileStorage - * The file object for this configuration object. - */ - protected function fileStorage() { - if (!isset($this->fileStorage)) { - $this->fileStorage = new FileStorage($this->name); - } - return $this->fileStorage; - } - - /** - * Implements StorageInterface::copyToFile(). - */ - public function copyToFile() { - return $this->writeToFile($this->read()); - } - - /** - * Implements StorageInterface::deleteFile(). - */ - public function deleteFile() { - return $this->fileStorage()->delete(); - } - - /** - * Implements StorageInterface::copyFromFile(). - */ - public function copyFromFile() { - return $this->writeToActive($this->readFromFile()); - } - - /** - * @todo - * - * @return - * @todo - */ - public function readFromFile() { - return $this->fileStorage()->read($this->name); - } - - /** - * Implements StorageInterface::isOutOfSync(). - */ - public function isOutOfSync() { - return $this->read() !== $this->readFromFile(); - } - - /** - * Implements StorageInterface::write(). - */ - public function write($data) { - $this->writeToActive($data); - $this->writeToFile($data); - } - - /** - * Implements StorageInterface::writeToFile(). - */ - public function writeToFile($data) { - return $this->fileStorage()->write($data); - } - - /** - * Implements StorageInterface::delete(). - */ - public function delete() { - $this->deleteFromActive(); - $this->deleteFile(); - } - - /** - * Implements StorageInterface::getName(). - */ - public function getName() { - return $this->name; - } - - /** - * Implements StorageInterface::setName(). - */ - public function setName($name) { - $this->name = $name; - } -} diff --git a/core/lib/Drupal/Core/Config/StorageException.php b/core/lib/Drupal/Core/Config/StorageException.php new file mode 100644 index 0000000..8fddd17 --- /dev/null +++ b/core/lib/Drupal/Core/Config/StorageException.php @@ -0,0 +1,15 @@ + 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 configuration manager by determining + * which storage can handle a particular configuration object, depending on + * the operation being performed. + * + * @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. + */ + 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/DependencyInjection/ContainerBuilder.php b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php index b9b3431..e5ba57e 100644 --- a/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php +++ b/core/lib/Drupal/Core/DependencyInjection/ContainerBuilder.php @@ -8,6 +8,8 @@ namespace Drupal\Core\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder as BaseContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + /** * Drupal's dependency injection container. @@ -24,5 +26,31 @@ class ContainerBuilder extends BaseContainerBuilder { // functions. This default is overridden by drupal_language_initialize() // during language negotiation. $this->register(LANGUAGE_TYPE_INTERFACE, 'Drupal\\Core\\Language\\Language'); + + // Register configuration system manager. + $this->setParameter('config.storage.manager', 'Drupal\Core\Config\StorageManager'); + $this->setParameter('config.storage.info', array( + 'Drupal\Core\Config\DatabaseStorage' => array( + 'target' => 'default', + 'read' => TRUE, + 'write' => TRUE, + ), + 'Drupal\Core\Config\FileStorage' => array( + 'directory' => config_get_config_directory(), + 'read' => TRUE, + 'write' => FALSE, + ), + )); + $this->register('config.manager', '%config.storage.manager%') + ->addArgument('%config.storage.info%'); + + // Register configuration system. + $this->setParameter('config.factory', 'Drupal\Core\Config\ConfigFactory'); + $this->setParameter('config.object', 'Drupal\Core\Config\ConfigObject'); + $this->register('config.factory', '%config.factory%') + ->addArgument(new Reference('service_container')); + // @todo Obsolete? + $this->register('config', '%config.object%') + ->addArgument(new Reference('config.manager')); } } diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc new file mode 100644 index 0000000..6f77bf2 --- /dev/null +++ b/core/modules/config/config.admin.inc @@ -0,0 +1,62 @@ + 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['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'); + } +} + diff --git a/core/modules/config/config.api.php b/core/modules/config/config.api.php new file mode 100644 index 0000000..8f95d32 --- /dev/null +++ b/core/modules/config/config.api.php @@ -0,0 +1,56 @@ +get(); + return image_style_delete($style); + } + if ($op == 'create') { + $style = $new_config->get(); + return image_style_save($style); + } + if ($op == 'change') { + $style = $new_config->get(); + return image_style_save($style); + } +} + diff --git a/core/modules/config/config.module b/core/modules/config/config.module index b3d9bbc..3d4fcfe 100644 --- a/core/modules/config/config.module +++ b/core/modules/config/config.module @@ -1 +1,33 @@ t('Import configuration'), + 'restrict access' => TRUE, + ); + return $permissions; +} + +/** + * Implements hook_menu(). + */ +function config_menu() { + $items['admin/config/development/import'] = array( + 'title' => 'Import configuration', + 'description' => 'Import and synchronize configuration changes.', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('config_admin_import_form'), + 'access arguments' => array('import configuration'), + 'file' => 'config.admin.inc', + ); + return $items; +} + diff --git a/core/modules/config/config_test/config/config_test.delete.yml b/core/modules/config/config_test/config/config_test.delete.yml new file mode 100644 index 0000000..b8ccb67 --- /dev/null +++ b/core/modules/config/config_test/config/config_test.delete.yml @@ -0,0 +1 @@ +delete_me: bar diff --git a/core/modules/config/config_test/config/config_test.dynamic.default.yml b/core/modules/config/config_test/config/config_test.dynamic.default.yml new file mode 100644 index 0000000..d285439 --- /dev/null +++ b/core/modules/config/config_test/config/config_test.dynamic.default.yml @@ -0,0 +1,2 @@ +id: default +name: Default diff --git a/core/modules/config/config_test/config/config_test.system.yml b/core/modules/config/config_test/config/config_test.system.yml new file mode 100644 index 0000000..20e9ff3 --- /dev/null +++ b/core/modules/config/config_test/config/config_test.system.yml @@ -0,0 +1 @@ +foo: bar diff --git a/core/modules/config/config_test/config_test.info b/core/modules/config/config_test/config_test.info new file mode 100644 index 0000000..8735450 --- /dev/null +++ b/core/modules/config/config_test/config_test.info @@ -0,0 +1,6 @@ +name = Configuration test module +package = Core +version = VERSION +core = 8.x +dependencies[] = config +hidden = TRUE diff --git a/core/modules/config/config_test/config_test.module b/core/modules/config/config_test/config_test.module new file mode 100644 index 0000000..de602a5 --- /dev/null +++ b/core/modules/config/config_test/config_test.module @@ -0,0 +1,242 @@ +delete(); + } + if ($op == 'create') { + $config_test = new ConfigTest($new_config); + $config_test->save(); + } + if ($op == 'change') { + $config_test = new ConfigTest($new_config); + $config_test->setOriginal($old_config); + $config_test->save(); + } + 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 returns the config object used to load all config in order to use + // this safely later we need to clone the object to protect it from change. + $config = clone config('config_test.dynamic.' . $id); + // @todo Requires a more reliable + generic method to check for whether the + // configuration object exists. + if ($config->get('id') === NULL) { + 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['name'] = 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', + ), + ); + $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/config_test/lib/Drupal/config_test/ConfigTest.php b/core/modules/config/config_test/lib/Drupal/config_test/ConfigTest.php new file mode 100644 index 0000000..2bb25c7 --- /dev/null +++ b/core/modules/config/config_test/lib/Drupal/config_test/ConfigTest.php @@ -0,0 +1,178 @@ +load($config); + } + + /** + * Implements Drupal\Core\Config\ConfigObjectInterface::getConfigPrefix(). + */ + public function getConfigPrefix() { + return 'config_test.dynamic'; + } + + /** + * Implements ConfigObjectInterface::getName(). + */ + public function getConfigName() { + return $this->getConfigPrefix() . '.' . $this->getId; + } + + /** + * Implements ConfigObjectInterface::getId(). + */ + public function getId() { + return $this->config->get($this->idKey); + } + + /** + * Implements ConfigObjectInterface::isNew(). + */ + public function isNew() { + return isset($this->originalId); + } + + /** + * Implements ConfigObjectInterface::getLabel(). + */ + public function getLabel() { + return $this->config->get($this->labelKey); + } + + /** + * Implements ConfigObjectInterface::getUri(). + */ + public function getUri() { + // @todo Check whether this makes sense. + return 'admin/structure/config_test/manage/' . $this->getId(); + } + + /** + * Implements ConfigObjectInterface::get(). + */ + public function get($property_name, $langcode = NULL) { + return $this->config->get($property_name); + } + + /** + * Implements ConfigObjectInterface::set(). + */ + public function set($property_name, $value, $langcode = NULL) { + return $this->config->set($property_name, $value); + } + + /** + * Implements ConfigObjectInterface::setOriginal(). + */ + public function setOriginal($config) { + $this->original = $config; + $this->originalId = $config->get($this->idKey); + return $this; + } + + /** + * Implements ConfigObjectInterface::load(). + */ + public function load($config) { + $this->config = $config; + $this->originalId = $config->get($this->idKey); + return $this; + } + + /** + * Implements ConfigObjectInterface::save(). + */ + public function save() { + // Provide the original configuration in $config->original, if any. + if (isset($this->originalId)) { + $original_config = config($this->getConfigPrefix() . '.' . $this->originalId); + $this->setOriginal(new $this($original_config)); + + // 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. + $original_data = $original_config->get(); + $original_config->delete(); + $original_config->setData($original_data); + } + } + + // Save the new configuration. + $this->config->setName($this->getConfigPrefix() . '.' . $this->getId()); + $this->config->save(); + + return $this; + } + + /** + * Implements ConfigObjectInterface::delete(). + */ + public function delete() { + $this->config->delete(); + } +} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php new file mode 100644 index 0000000..881cf8f --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php @@ -0,0 +1,106 @@ + 'CRUD operations', + 'description' => 'Tests CRUD operations on configuration objects.', + 'group' => 'Configuration', + ); + } + + /** + * Tests CRUD operations. + */ + function testCRUD() { + $storage = new DatabaseStorage(); + $name = 'config_test.crud'; + + // Create a new configuration object. + $config = config($name); + $config->set('value', 'initial'); + $config->save(); + + // Verify the active store contains the saved value. + $actual_data = $storage->read($name); + $this->assertIdentical($actual_data, array('value' => 'initial')); + + // Update the configuration object instance. + $config->set('value', 'instance-update'); + $config->save(); + + // Verify the active store contains the updated value. + $actual_data = $storage->read($name); + $this->assertIdentical($actual_data, array('value' => 'instance-update')); + + // Verify a call to config() immediately returns the updated value. + $new_config = config($name); + $this->assertIdentical($new_config->get(), $config->get()); + + // Delete the configuration object. + $config->delete(); + + // Verify the configuration object is empty. + $this->assertIdentical($config->get(), array()); + + // Verify the active store contains no value. + $actual_data = $storage->read($name); + $this->assertIdentical($actual_data, array()); + + // Verify config() returns no data. + $new_config = config($name); + $this->assertIdentical($new_config->get(), $config->get()); + + // Re-create the configuration object. + $config->set('value', 're-created'); + $config->save(); + + // Verify the active store contains the updated value. + $actual_data = $storage->read($name); + $this->assertIdentical($actual_data, array('value' => 're-created')); + + // Verify a call to config() immediately returns the updated value. + $new_config = config($name); + $this->assertIdentical($new_config->get(), $config->get()); + } + + /** + * Tests Drupal\Core\Config\ConfigObject::sortByKey(). + */ + function testDataKeySort() { + $config = config('config_test.keysort'); + $config->set('new', 'Value to be replaced'); + $config->set('static', 'static'); + $config->save(); + // Clone this ConfigObject, so this test does not rely on any particular + // architecture. + $config = clone $config; + + // Load the configuration data into a new object. + $new_config = config('config_test.keysort'); + // Clear the 'new' key that came first. + $new_config->clear('new'); + // Add a new 'new' key and save. + $new_config->set('new', 'Value to be replaced'); + $new_config->save(); + + // Verify that the data of both objects is in the identical order. + // assertIdentical() is the required essence of this test; it performs a + // strict comparison, which means that keys and values must be identical and + // their order must be identical. + $this->assertIdentical($new_config->get(), $config->get()); + } +} 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..68df5f1 --- /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, + 'name' => '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( + 'name' => '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, + 'name' => '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, + 'name' => '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/ConfigFileContentTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigFileContentTest.php index bfd27ac..29fad3e 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigFileContentTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigFileContentTest.php @@ -7,6 +7,7 @@ namespace Drupal\config\Tests; +use Drupal\Core\Config\DatabaseStorage; use Drupal\Core\Config\FileStorage; use Drupal\simpletest\WebTestBase; @@ -14,8 +15,6 @@ use Drupal\simpletest\WebTestBase; * Tests reading and writing file contents. */ class ConfigFileContentTest extends WebTestBase { - protected $fileExtension; - public static function getInfo() { return array( 'name' => 'File content', @@ -26,15 +25,14 @@ class ConfigFileContentTest extends WebTestBase { function setUp() { parent::setUp(); - - $this->fileExtension = FileStorage::getFileExtension(); } /** * Tests setting, writing, and reading of a configuration setting. */ function testReadWriteConfig() { - $config_dir = config_get_config_directory(); + $database_storage = new DatabaseStorage(); + $name = 'foo.bar'; $key = 'foo'; $value = 'bar'; @@ -62,16 +60,15 @@ class ConfigFileContentTest extends WebTestBase { $config = config($name); // Verify an configuration object is returned. -// $this->assertEqual($config->name, $name); + $this->assertEqual($config->getName(), $name); $this->assertTrue($config, t('Config object created.')); // Verify the configuration object is empty. $this->assertEqual($config->get(), array(), t('New config object is empty.')); // Verify nothing was saved. - $db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $name))->fetch(); - $this->assertIdentical($db_config, FALSE, t('Active store does not have a record for %name', array('%name' => $name))); - $this->assertFalse(file_exists($config_dir . '/' . $name . '.' . $this->fileExtension), 'Configuration file does not exist.'); + $db_data = $database_storage->read($name); + $this->assertIdentical($db_data, array()); // Add a top level value $config = config($name); @@ -97,15 +94,12 @@ class ConfigFileContentTest extends WebTestBase { $config->save(); // Verify the database entry exists. - $db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $name))->fetch(); - $this->assertEqual($db_config->name, $name, t('After saving configuration, active store has a record for %name', array('%name' => $name))); - - // Verify the file exists. - $this->assertTrue(file_exists($config_dir . '/' . $name . '.' . $this->fileExtension), t('After saving configuration, config file exists.')); + $db_data = $database_storage->read($name); + $this->assertTrue($db_data); // Read top level value $config = config($name); -// $this->assertEqual($config->name, $name); + $this->assertEqual($config->getName(), $name); $this->assertTrue($config, 'Config object created.'); $this->assertEqual($config->get($key), 'bar', t('Top level configuration value found.')); @@ -158,30 +152,27 @@ class ConfigFileContentTest extends WebTestBase { $config->set($key, $value)->save(); // Verify the database entry exists from a chained save. - $db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $chained_name))->fetch(); - $this->assertEqual($db_config->name, $chained_name, t('After saving configuration by chaining through set(), active store has a record for %name', array('%name' => $chained_name))); - - // Verify the file exists from a chained save. - $this->assertTrue(file_exists($config_dir . '/' . $chained_name . '.' . $this->fileExtension), t('After saving configuration by chaining through set(), config file exists.')); + $db_data = $database_storage->read($chained_name); + $this->assertEqual($db_data, $config->get()); // Get file listing for all files starting with 'foo'. Should return // two elements. - $files = FileStorage::getNamesWithPrefix('foo'); + $files = $database_storage->getNamesWithPrefix('foo'); $this->assertEqual(count($files), 2, 'Two files listed with the prefix \'foo\'.'); // Get file listing for all files starting with 'biff'. Should return // one element. - $files = FileStorage::getNamesWithPrefix('biff'); + $files = $database_storage->getNamesWithPrefix('biff'); $this->assertEqual(count($files), 1, 'One file listed with the prefix \'biff\'.'); // Get file listing for all files starting with 'foo.bar'. Should return // one element. - $files = FileStorage::getNamesWithPrefix('foo.bar'); + $files = $database_storage->getNamesWithPrefix('foo.bar'); $this->assertEqual(count($files), 1, 'One file listed with the prefix \'foo.bar\'.'); // Get file listing for all files starting with 'bar'. Should return // an empty array. - $files = FileStorage::getNamesWithPrefix('bar'); + $files = $database_storage->getNamesWithPrefix('bar'); $this->assertEqual($files, array(), 'No files listed with the prefix \'bar\'.'); // Delete the configuration. @@ -189,17 +180,14 @@ class ConfigFileContentTest extends WebTestBase { $config->delete(); // Verify the database entry no longer exists. - $db_config = db_query('SELECT * FROM {config} WHERE name = :name', array(':name' => $name))->fetch(); - $this->assertIdentical($db_config, FALSE); - $this->assertFalse(file_exists($config_dir . '/' . $name . $this->fileExtension)); - - // Attempt to delete non-existing configuration. + $db_data = $database_storage->read($name); + $this->assertIdentical($db_data, array()); } /** * Tests serialization of configuration to file. */ - function testConfigSerialization() { + function testSerialization() { $name = $this->randomName(10) . '.' . $this->randomName(10); $config_data = array( // Indexed arrays; the order of elements is essential. @@ -216,17 +204,10 @@ class ConfigFileContentTest extends WebTestBase { 'invalid xml' => ' & < > " \' ', ); - // Attempt to read non-existing configuration. - $config = config($name); - - foreach ($config_data as $key => $value) { - $config->set($key, $value); - } - - $config->save(); - - $config_filestorage = new FileStorage($name); - $config_parsed = $config_filestorage->read(); + // Encode and write, and reload and decode the configuration data. + $filestorage = new FileStorage(); + $filestorage->write($name, $config_data); + $config_parsed = $filestorage->read($name); $key = 'numeric keys'; $this->assertIdentical($config_data[$key], $config_parsed[$key]); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigFileSecurityTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigFileSecurityTest.php deleted file mode 100644 index 5f9ec07..0000000 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigFileSecurityTest.php +++ /dev/null @@ -1,51 +0,0 @@ - 'Good morning, Denver!'); - - public static function getInfo() { - return array( - 'name' => 'File security', - 'description' => 'Tests security of saved configuration files.', - 'group' => 'Configuration', - ); - } - - /** - * Tests that a file written by this system can be successfully read back. - */ - function testFilePersist() { - $file = new FileStorage($this->filename); - $file->write($this->testContent); - - unset($file); - - // Reading should throw an exception in case of bad validation. - // Note that if any other exception is thrown, we let the test system - // handle catching and reporting it. - try { - $file = new FileStorage($this->filename); - $saved_content = $file->read(); - - $this->assertEqual($saved_content, $this->testContent); - } - catch (Exception $e) { - $this->fail('File failed verification when being read.'); - } - } -} diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php new file mode 100644 index 0000000..09baded --- /dev/null +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -0,0 +1,162 @@ + 'Import configuration', + 'description' => 'Tests importing configuration from files into active store.', + 'group' => 'Configuration', + ); + } + + function setUp() { + parent::setUp('config_test'); + } + + /** + * Tests deletion of configuration during import. + */ + function testDeleted() { + $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'); + + // Export. + config_export(); + + // Delete the configuration objects. + $file_storage = new FileStorage(); + $file_storage->delete($name); + $file_storage->delete($dynamic_name); + + // Import. + config_import(); + + // Verify the values have disappeared. + $database_storage = new DatabaseStorage(); + $this->assertIdentical($database_storage->read($name), array()); + $this->assertIdentical($database_storage->read($dynamic_name), array()); + + $config = config($name); + $this->assertIdentical($config->get('foo'), NULL); + $config = config($dynamic_name); + $this->assertIdentical($config->get('id'), NULL); + } + + /** + * Tests creation of configuration during import. + */ + function testNew() { + $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.'); + + // Export. + config_export(); + + // Create new configuration objects. + $file_storage->write($name, array( + 'add_me' => 'new value', + )); + $file_storage->write($dynamic_name, array( + 'id' => 'new', + 'name' => 'New', + )); + $this->assertIdentical($file_storage->exists($name), TRUE, $name . ' found.'); + $this->assertIdentical($file_storage->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); + + // Import. + config_import(); + + // Verify the values appeared. + $config = config($name); + $this->assertIdentical($config->get('add_me'), 'new value'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('name'), 'New'); + } + + /** + * Tests updating of configuration during import. + */ + function testUpdated() { + $name = 'config_test.system'; + $dynamic_name = 'config_test.dynamic.default'; + + // Export. + config_export(); + + // Replace the file content of the existing configuration objects. + $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( + 'foo' => 'beer', + )); + $file_storage->write($dynamic_name, array( + 'id' => 'default', + 'name' => 'Updated', + )); + + // Verify the active store still returns the default values. + $config = config($name); + $this->assertIdentical($config->get('foo'), 'bar'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('name'), 'Default'); + + // Import. + config_import(); + + // Verify the values were updated. + $config = config($name); + $this->assertIdentical($config->get('foo'), 'beer'); + $config = config($dynamic_name); + $this->assertIdentical($config->get('name'), 'Updated'); + } + + /** + * Tests config_import() hook invocations. + */ + function testSyncHooks() { + $name = 'config_test.system'; + $dynamic_name = 'config_test.dynamic.default'; + + // Export. + config_export(); + + // Delete a file so that hook_config_import() hooks are run. + $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->delete($name); + $file_storage->delete($dynamic_name); + + // Import. + config_import(); + + // Verify hook_config_import() was invoked. + $this->assertIdentical($GLOBALS['hook_config_import'], 'config_test_config_import'); + } +} diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 905e6a7..1be8fe6 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -500,6 +500,36 @@ function image_path_flush($path) { } /** + * Implements hook_config_import(). + */ +function image_config_import($op, $name, $new_config, $old_config) { + // Only image styles require custom handling. Any other module settings can be + // synchronized directly. + if (strpos($name, 'image.style.') !== 0) { + return FALSE; + } + + if ($op == 'delete') { + // @todo image_style_delete() supports the notion of a "replacement style" + // to be used by other modules instead of the deleted style. Good idea. + // But squeezing that into a "delete" operation is the worst idea ever. + // Regardless of Image module insanity, add a 'replaced' stack to + // config_import()? And how can that work? If an 'old_ID' key would be a + // standard, wouldn't this belong into 'changed' instead? + $style = $old_config->get(); + return image_style_delete($style); + } + if ($op == 'create') { + $style = $new_config->get(); + return image_style_save($style); + } + if ($op == 'change') { + $style = $new_config->get(); + return image_style_save($style); + } +} + +/** * Get an array of all styles and their settings. * * @return diff --git a/core/modules/system/system.info b/core/modules/system/system.info index 4628b7f..37593ba 100644 --- a/core/modules/system/system.info +++ b/core/modules/system/system.info @@ -9,19 +9,13 @@ configure = admin/config/system ; Tests in tests directory. files[] = tests/cache.test -files[] = tests/common.test files[] = tests/database.test files[] = tests/file.test files[] = tests/filetransfer.test files[] = tests/form.test files[] = tests/image.test -files[] = tests/menu.test -files[] = tests/module.test -files[] = tests/pager.test files[] = tests/registry.test -files[] = tests/schema.test files[] = tests/symfony.test -files[] = tests/tablesort.test files[] = tests/theme.test files[] = tests/update.test files[] = tests/uuid.test diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 39755dc..58e7605 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -376,7 +376,8 @@ function system_element_info() { $types['email'] = array( '#input' => TRUE, '#size' => 60, - '#maxlength' => EMAIL_MAX_LENGTH, + // user.module is not loaded in case of early bootstrap errors. + '#maxlength' => defined('EMAIL_MAX_LENGTH') ? EMAIL_MAX_LENGTH : 255, '#autocomplete_path' => FALSE, '#process' => array('form_process_autocomplete', 'ajax_process_form', 'form_process_pattern'), '#element_validate' => array('form_validate_email'), diff --git a/core/modules/system/system.test b/core/modules/system/system.test index 17b96c8..72d78b4 100644 --- a/core/modules/system/system.test +++ b/core/modules/system/system.test @@ -1,6 +1,7 @@ assertTrue($files); - $config_dir = config_get_config_directory(); - // Get the filename of each config file. - foreach ($files as $file) { - $parts = explode('/', $file); - $filename = array_pop($parts); - if (!file_exists($config_dir . '/' . $filename)) { - $files_exist = FALSE; - } - } + if (!is_dir($module_config_dir)) { + return; } + $module_file_storage = new FileStorage(array('directory' => $module_config_dir)); + $names = $module_file_storage->getNamesWithPrefix(); + + // Verify that the config directory is not empty. + $this->assertTrue($names); - return $this->assertTrue($files_exist, t('All config files defined by the @module module have been copied to the live config directory.', array('@module' => $module))); + // Look up each default configuration object name in the active store, and + // if it exists, remove it from the stack. + foreach ($names as $key => $name) { + if (config($name)->get()) { + unset($names[$key]); + } + } + // Verify that all configuration has been installed (which means that $names + // is empty). + return $this->assertFalse($names, format_string('All default configuration of @module module found.', array('@module' => $module))); } /** - * Assert that none of a module's default config files are loaded. + * Asserts that no configuration exists for a given module. * * @param string $module * The name of the module. * * @return bool - * TRUE if the module's config files do not exist, FALSE otherwise. + * TRUE if no configuration was found, FALSE otherwise. */ - function assertModuleConfigFilesDoNotExist($module) { - // Define test variable. - $files_exist = FALSE; - // Get the path to the module's config dir. - $module_config_dir = drupal_get_path('module', $module) . '/config'; - if (is_dir($module_config_dir)) { - $files = glob($module_config_dir . '/*.' . FileStorage::getFileExtension()); - $this->assertTrue($files); - $config_dir = config_get_config_directory(); - // Get the filename of each config file. - foreach ($files as $file) { - $parts = explode('/', $file); - $filename = array_pop($parts); - if (file_exists($config_dir . '/' . $filename)) { - $files_exist = TRUE; - } - } - } - - return $this->assertFalse($files_exist, t('All config files defined by the @module module have been deleted from the live config directory.', array('@module' => $module))); + function assertNoModuleConfig($module) { + $names = config_get_storage_names_with_prefix($module . '.'); + return $this->assertFalse($names, format_string('No configuration found for @module module.', array('@module' => $module))); } /** @@ -293,7 +278,7 @@ class EnableDisableTestCase extends ModuleTestCase { $this->assertText(t('hook_modules_enabled fired for @module', array('@module' => $module_to_enable))); $this->assertModules(array($module_to_enable), TRUE); $this->assertModuleTablesExist($module_to_enable); - $this->assertModuleConfigFilesExist($module_to_enable); + $this->assertModuleConfig($module_to_enable); $this->assertLogMessage('system', "%module module installed.", array('%module' => $module_to_enable), WATCHDOG_INFO); $this->assertLogMessage('system', "%module module enabled.", array('%module' => $module_to_enable), WATCHDOG_INFO); } @@ -374,7 +359,7 @@ class EnableDisableTestCase extends ModuleTestCase { // Check that the module's database tables still exist. $this->assertModuleTablesExist($module); // Check that the module's config files still exist. - $this->assertModuleConfigFilesExist($module); + $this->assertModuleConfig($module); // Uninstall the module. $edit = array(); @@ -396,7 +381,7 @@ class EnableDisableTestCase extends ModuleTestCase { // Check that the module's database tables no longer exist. $this->assertModuleTablesDoNotExist($module); // Check that the module's config files no longer exist. - $this->assertModuleConfigFilesDoNotExist($module); + $this->assertNoModuleConfig($module); } }