Issue #1763640 by beejeebus, alexpott, Jose Reyero, c31ck, das-peter, YesCT, heyrocker, Gábor Hojtsy: Introduce config context to make original config and different overrides accessible.

8.0.x
catch 2013-02-27 09:49:26 +00:00
parent 3714bed1e9
commit 0612c664da
24 changed files with 951 additions and 174 deletions

View File

@ -1,8 +1,8 @@
<?php
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\NullStorage;
use Drupal\Core\Config\StorageInterface;
/**
@ -24,6 +24,10 @@ const CONFIG_IMPORT_LOCK = 'config_import';
* The name of the module or theme to install default configuration for.
*/
function config_install_default_config($type, $name) {
// Use the override free context for config importing so that any overrides do
// not change the data on import.
config_context_enter('config.context.free');
// If this module defines any ConfigEntity types then create an empty
// manifest file for each of them.
foreach (config_get_module_config_entities($name) as $entity_info) {
@ -47,6 +51,8 @@ function config_install_default_config($type, $name) {
$remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
config_sync_changes($remaining_changes, $source_storage, $target_storage);
}
// Exit the override free context.
config_context_leave();
}
/**
@ -87,7 +93,7 @@ function config_get_storage_names_with_prefix($prefix = '') {
* @code config('book.admin') @endcode will return a configuration object in
* which the book module can store its administrative settings.
*
* @param $name
* @param string $name
* 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.
@ -99,6 +105,54 @@ function config($name) {
return drupal_container()->get('config.factory')->get($name);
}
/*
* Sets the config context on the config factory.
*
* This allows configuration objects to be created using special configuration
* contexts eg. global override free or locale using a user preferred language.
* Calling this function affects all subsequent calls to config() until
* config_context_leave() is called.
*
* @see config_context_leave()
* @see \Drupal\Core\Config\ConfigFactory
*
* @param string $context_name
* The name of the config context service on the container or a fully
* qualified class implementing \Drupal\Core\Config\Context\ContextInterface.
*
* @return \Drupal\Core\Config\Context\ContextInterface
* The configuration context object.
*/
function config_context_enter($context_name) {
if (drupal_container()->has($context_name)) {
$context = drupal_container()->get($context_name);
}
elseif (class_exists($context_name) && in_array("Drupal\\Core\\Config\\Context\\ContextInterface", class_implements($context_name))) {
$context = drupal_container()
->get('config.context.factory')
->get($context_name);
}
else {
throw new ConfigException(sprintf('Unknown config context service or class: %s', $context_name));
}
drupal_container()
->get('config.factory')
->enterContext($context);
return $context;
}
/*
* Leaves the current config context returning to the previous context.
*
* @see config_context_enter()
* @see \Drupal\Core\Config\ConfigFactory
*/
function config_context_leave() {
drupal_container()
->get('config.factory')
->leaveContext();
}
/**
* Returns a list of differences between configuration storages.
*
@ -184,10 +238,11 @@ function config_sync_get_changes(StorageInterface $source_storage, StorageInterf
* The storage to synchronize configuration to.
*/
function config_sync_changes(array $config_changes, StorageInterface $source_storage, StorageInterface $target_storage) {
$target_context = drupal_container()->get('config.context.free');
$factory = drupal_container()->get('config.factory');
foreach (array('delete', 'create', 'change') as $op) {
foreach ($config_changes[$op] as $name) {
$config = new Config($name, $target_storage);
$config = new Config($name, $target_storage, $target_context);
if ($op == 'delete') {
$config->delete();
}
@ -230,9 +285,16 @@ function config_import() {
$success = TRUE;
try {
// Use the override free context for config importing so that any overrides do
// not change the data on import.
config_context_enter('config.context.free');
$remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
config_sync_changes($remaining_changes, $source_storage, $target_storage);
config_import_create_snapshot($target_storage, $snapshot_storage);
// Exit the override free context.
config_context_leave();
}
catch (ConfigException $e) {
watchdog_exception('config_import', $e);
@ -271,6 +333,10 @@ function config_import_create_snapshot(StorageInterface $source_storage, Storage
* @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) {
$factory = drupal_container()->get('config.factory');
// Use the admin context for config importing so that any overrides do not
// change the data on import.
$free_context = drupal_container()->get('config.context.free');
// 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
@ -284,11 +350,11 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou
// Validate the configuration object name before importing it.
Config::validateName($name);
if ($entity_type = config_get_entity_type_by_name($name)) {
$old_config = new Config($name, $target_storage);
$old_config = new Config($name, $target_storage, $free_context);
$old_config->load();
$data = $source_storage->read($name);
$new_config = new Config($name, $target_storage);
$new_config = new Config($name, $source_storage, $free_context);
if ($data !== FALSE) {
$new_config->setData($data);
}
@ -297,6 +363,10 @@ function config_import_invoke_owner(array $config_changes, StorageInterface $sou
$handled_by_module = $manager->getStorageController($entity_type)->$method($name, $new_config, $old_config);
}
if (!empty($handled_by_module)) {
$factory->reset($name);
// Reset the manifest config object for the config entity.
$entity_info = drupal_container()->get('plugin.manager.entity')->getDefinition($entity_type);
$factory->reset('manifest.' . $entity_info['config_prefix']);
unset($config_changes[$op][$key]);
}
}

View File

@ -337,9 +337,16 @@ function install_begin_request(&$install_state) {
$container->register('event_dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher');
$container->register('config.storage', 'Drupal\Core\Config\InstallStorage');
$container->register('config.context.factory', 'Drupal\Core\Config\Context\ConfigContextFactory')
->addArgument(new Reference('event_dispatcher'));
$container->register('config.context', 'Drupal\Core\Config\Context\ContextInterface')
->setFactoryService(new Reference('config.context.factory'))
->setFactoryMethod('get');
$container->register('config.factory', 'Drupal\Core\Config\ConfigFactory')
->addArgument(new Reference('config.storage'))
->addArgument(new Reference('event_dispatcher'));
->addArgument(new Reference('config.context'));
// Register the 'language_manager' service.
$container->register('language_manager', 'Drupal\Core\Language\LanguageManager');

View File

@ -9,7 +9,7 @@ namespace Drupal\Core\Config;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\ConfigNameException;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Drupal\Core\Config\Context\ContextInterface;
/**
* Defines the default configuration object.
@ -71,11 +71,11 @@ class Config {
protected $storage;
/**
* The event dispatcher used to notify subscribers.
* The configuration context used for this configuration object.
*
* @var Symfony\Component\EventDispatcher\EventDispatcher
* @var \Drupal\Core\Config\Context\ContextInterface
*/
protected $eventDispatcher;
protected $context;
/**
* Whether the config object has already been loaded.
@ -89,16 +89,16 @@ class Config {
*
* @param string $name
* The name of the configuration object being constructed.
* @param Drupal\Core\Config\StorageInterface $storage
* @param \Drupal\Core\Config\StorageInterface $storage
* A storage controller object to use for reading and writing the
* configuration data.
* @param Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher
* The event dispatcher used to notify subscribers.
* @param \Drupal\Core\Config\Context\ContextInterface $context
* The configuration context used for this configuration object.
*/
public function __construct($name, StorageInterface $storage, EventDispatcher $event_dispatcher = NULL) {
public function __construct($name, StorageInterface $storage, ContextInterface $context) {
$this->name = $name;
$this->storage = $storage;
$this->eventDispatcher = $event_dispatcher ? $event_dispatcher : drupal_container()->get('event_dispatcher');
$this->context = $context;
}
/**
@ -491,7 +491,7 @@ class Config {
* Dispatch a config event.
*/
protected function notify($config_event_name) {
$this->eventDispatcher->dispatch('config.' . $config_event_name, new ConfigEvent($this));
$this->context->notify($config_event_name, $this);
}
/**

View File

@ -2,10 +2,11 @@
namespace Drupal\Core\Config;
use Drupal\Core\Config\Context\ContextInterface;
use Symfony\Component\EventDispatcher\Event;
use Drupal\Core\Config\Config;
class ConfigEvent extends Event {
/**
* Configuration object.
*
@ -14,10 +15,23 @@ class ConfigEvent extends Event {
protected $config;
/**
* Constructor.
* Configuration context object.
*
* @var \Drupal\Core\Config\Context\ContextInterface
*/
public function __construct(Config $config) {
protected $context;
/**
* Constructs a configuration event object.
*
* @param \Drupal\Core\Config\Context\ContextInterface
* Configuration context object.
* @param \Drupal\Core\Config\Config
* (optional) Configuration object.
*/
public function __construct(ContextInterface $context, Config $config = NULL) {
$this->config = $config;
$this->context = $context;
}
/**
@ -26,4 +40,14 @@ class ConfigEvent extends Event {
public function getConfig() {
return $this->config;
}
/**
* Get configuration context object.
*
* @return \Drupal\Core\Config\Context\ContextInterface
* Configuration context.
*/
public function getContext() {
return $this->context;
}
}

View File

@ -7,7 +7,7 @@
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Drupal\Core\Config\Context\ContextInterface;
/**
* Defines the configuration object factory.
@ -21,22 +21,28 @@ use Symfony\Component\EventDispatcher\EventDispatcher;
* is used for reading and writing the configuration data.
*
* @see Drupal\Core\Config\StorageInterface
*
* A configuration context is an object containing parameters that will be
* available to the configuration plug-ins for them to customize the
* configuration data in different ways.
*
* @see Drupal\Core\Config\Context\ContextInterface
*/
class ConfigFactory {
/**
* A storage controller instance for reading and writing configuration data.
*
* @var Drupal\Core\Config\StorageInterface
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
/**
* An event dispatcher instance to use for configuration events.
* A stack of configuration contexts the last being the context in use.
*
* @var Symfony\Component\EventDispatcher\EventDispatcher
* @var array
*/
protected $eventDispatcher;
protected $contextStack = array();
/**
* Cached configuration objects.
@ -48,33 +54,31 @@ class ConfigFactory {
/**
* Constructs the Config factory.
*
* @param Drupal\Core\Config\StorageInterface $storage
* The storage controller object to use for reading and writing
* configuration data.
* @param Symfony\Component\EventDispatcher\EventDispatcher
* An event dispatcher instance to use for configuration events.
* @param \Drupal\Core\Config\StorageInterface
* The configuration storage engine.
* @param \Drupal\Core\Config\Context\ContextInterface
* Configuration context object.
*/
public function __construct(StorageInterface $storage, EventDispatcher $event_dispatcher) {
public function __construct(StorageInterface $storage, ContextInterface $context) {
$this->storage = $storage;
$this->eventDispatcher = $event_dispatcher;
$this->enterContext($context);
}
/**
* Returns a configuration object for a given name.
* Returns a configuration object for a given name and context.
*
* @param string $name
* The name of the configuration object to construct.
*
* @return Drupal\Core\Config\Config
* A configuration object with the given $name.
*/
public function get($name) {
if (isset($this->cache[$name])) {
return $this->cache[$name];
$context = $this->getContext();
$cache_key = $this->getCacheKey($name, $context);
if (isset($this->cache[$cache_key])) {
return $this->cache[$cache_key];
}
$this->cache[$name] = new Config($name, $this->storage, $this->eventDispatcher);
return $this->cache[$name]->init();
$this->cache[$cache_key] = new Config($name, $this->storage, $context);
return $this->cache[$cache_key]->init();
}
/**
@ -83,11 +87,15 @@ class ConfigFactory {
* @param string $name
* (optional) The name of the configuration object to reset. If omitted, all
* configuration objects are reset.
*
* @return \Drupal\Core\Config\ConfigFactory
* The config factory object.
*/
public function reset($name = NULL) {
if ($name) {
if (isset($this->cache[$name])) {
$this->cache[$name]->init();
// Reinitialise the configuration object in all contexts.
foreach ($this->getCacheKeys($name) as $cache_key) {
$this->cache[$cache_key]->init();
}
}
else {
@ -95,6 +103,7 @@ class ConfigFactory {
$config->init();
}
}
return $this;
}
/**
@ -108,14 +117,84 @@ class ConfigFactory {
* @todo D8: Remove after http://drupal.org/node/1865206.
*/
public function rename($old_name, $new_name) {
if (isset($this->cache[$old_name])) {
$config = $this->cache[$old_name];
$old_cache_key = $this->getCacheKey($old_name, $this->getContext());
$new_cache_key = $this->getCacheKey($new_name, $this->getContext());
if (isset($this->cache[$old_cache_key])) {
$config = $this->cache[$old_cache_key];
// Clone the object into the existing slot.
$this->cache[$old_name] = clone $config;
$this->cache[$old_cache_key] = clone $config;
// Change the object's name and re-initialize it.
$config->setName($new_name)->init();
$this->cache[$new_name] = $config;
$this->cache[$new_cache_key] = $config;
}
}
/**
* Sets the config context by adding it to the context stack.
*
* @param \Drupal\Core\Config\Context\ContextInterface $context
* The configuration context to add.
*
* @return \Drupal\Core\Config\ConfigFactory
* The config factory object.
*/
public function enterContext(ContextInterface $context) {
$this->contextStack[] = $context;
return $this;
}
/**
* Gets the current config context.
*
* @return \Drupal\Core\Config\Context\ContextInterface $context
* The current configuration context.
*/
public function getContext() {
return end($this->contextStack);
}
/**
* Leaves the current context by removing it from the context stack.
*
* @return \Drupal\Core\Config\ConfigFactory
* The config factory object.
*/
public function leaveContext() {
if (count($this->contextStack) > 1) {
array_pop($this->contextStack);
}
return $this;
}
/*
* Gets the cache key for a given config name in a particular context.
*
* @param string $name
* The name of the configuration object.
* @param \Drupal\Core\Config\Context\ContextInterface $context
* The configuration context.
*
* @return string
* The cache key.
*/
public function getCacheKey($name, ContextInterface $context) {
return $name . ':' . $context->getUuid();
}
/**
* Gets all the cache keys that match the provided config name.
*
* @param string $name
* The name of the configuration object.
*
* @return array
* An array of cache keys that match the provided config name.
*/
public function getCacheKeys($name) {
$cache_keys = array_keys($this->cache);
return array_filter($cache_keys, function($key) use ($name) {
return ( strpos($key, $name) !== false );
});
}
}

View File

@ -0,0 +1,123 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Context\ConfigContext.
*/
namespace Drupal\Core\Config\Context;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigEvent;
use Drupal\Component\Uuid\Uuid;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Defines the base configuration context object.
*
* A configuration context object provides a data array that can be used:
* - as a parameter to get customized configuration objects.
* - as a store of config data used to override values.
*/
class ConfigContext implements ContextInterface {
/**
* Predefined key, values to override specific configuration objects.
*/
const OVERRIDE = 'config.override';
/**
* The actual storage of key-value pairs.
*
* @var array
*/
protected $data = array();
/**
* An event dispatcher instance to use for configuration events.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcher
*/
protected $eventDispatcher;
/**
* A unique identifier for the context.
*
* @var string
*/
protected $uuid;
/**
* Constructs the configuration context.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher
* An event dispatcher instance to use for configuration events.
*/
public function __construct(EventDispatcher $event_dispatcher) {
$this->eventDispatcher = $event_dispatcher;
}
/**
* Implements Drupal\Core\Config\Context\ContextInterface::init().
*/
public function init($context_key, $data) {
if ($data) {
$this->set($context_key, $data);
}
$this->setUuid();
// Notify event listeners that a configuration context has been created.
$this->notify('context', NULL);
return $this;
}
/**
* Implements Drupal\Core\Config\Context\ContextInterface::get().
*/
public function get($key) {
return array_key_exists($key, $this->data) ? $this->data[$key] : NULL;
}
/**
* Implements Drupal\Core\Config\Context\ContextInterface::set().
*/
public function set($key, $value) {
$this->data[$key] = $value;
}
/**
* Sets override data.
*
* @param mixed $data
* Override data to store.
*
* @return \Drupal\Core\Config\Context\ConfigContext
* The config context object.
*/
public function setOverride($data) {
$this->init(self::OVERRIDE, $data);
return $this;
}
/**
* Implements Drupal\Core\Config\Context\ContextInterface::setUuid().
*/
public function setUuid() {
$uuid = new Uuid();
$this->uuid = $uuid->generate();
}
/**
* Implements Drupal\Core\Config\Context\ContextInterface::getUuid().
*/
public function getUuid() {
return $this->uuid;
}
/**
* Implements Drupal\Core\Config\Context\ContextInterface::notify().
*/
public function notify($config_event_name, Config $config = NULL) {
$this->eventDispatcher->dispatch('config.' . $config_event_name, new ConfigEvent($this, $config));
}
}

View File

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Context\ConfigContextFactory.
*/
namespace Drupal\Core\Config\Context;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigException;
use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* Defines configuration context factory.
*
* The configuration context factory creates configuration context objects.
*
* @see \Drupal\Core\Config\Context\ContextInterface
*/
class ConfigContextFactory {
/**
* An event dispatcher instance to use for configuration events.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcher
*/
protected $eventDispatcher;
/**
* Constructs the configuration context.
*
* @param \Symfony\Component\EventDispatcher\EventDispatcher $event_dispatcher
* An event dispatcher instance to use for configuration events.
*/
public function __construct(EventDispatcher $event_dispatcher) {
$this->eventDispatcher = $event_dispatcher;
}
/**
* Returns a configuration context object.
*
* @param string $class
* (Optional) The name of the configuration class to use. Defaults to
* Drupal\Core\Config\Context\ConfigContext
*
* @return \Drupal\Core\Config\Context\ContextInterface $context
* (Optional) The configuration context to use.
*/
public function get($class = NULL) {
if (!$class) {
$class = "Drupal\\Core\\Config\\Context\\ConfigContext";
}
if (class_exists($class)) {
$context = new $class($this->eventDispatcher);
}
else {
throw new ConfigException(sprintf('Unknown config context class: %s', $class));
}
return $context;
}
}

View File

@ -0,0 +1,88 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Context\ContextInterface.
*/
namespace Drupal\Core\Config\Context;
use Drupal\Core\Config\Config;
/**
* Defines the configuration context interface.
*
* The configuration context object will contain predefined parameters used
* by the configuration object for storage operations and notifications
* and contextual data to be used by configuration event listeners.
*
* @see Drupal\Core\Config\Config
* @see Drupal\Core\Config\ConfigFactory
* @see config()
*/
interface ContextInterface {
/*
* Initialises a config context for use.
*
* Creates a unique context identifier, adds data and notifies system about
* the new context.
*
* @param string $context_key
* The key that is used to set context data.
* @param mixed $data
* The context config data.
*
* @return \Drupal\Core\Config\Context\ConfigContext
* The config context object.
*/
public function init($context_key, $data);
/**
* Returns the stored value for a given key.
*
* @param string $key
* The key of the data to retrieve.
*
* @return mixed
* The stored value, or NULL if no value exists.
*/
public function get($key);
/**
* Saves a value for a given key.
*
* @param string $key
* The key of the data to store.
* @param mixed $value
* The data to store.
*/
public function set($key, $value);
/**
* Sets the uuid for the context.
*
* @return string
* The context's uuid.
*/
public function setUuid();
/**
* Gets the uuid for the context.
*
* @return string
* The context's uuid.
*/
public function getUuid();
/**
* Dispatches a config event.
*
* @param string $config_event_name
* Event name.
* @param \Drupal\Core\Config\Config $config
* (optional) Configuration object.
*/
public function notify($config_event_name, Config $config = NULL);
}

View File

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\Core\Config\Context\GlobalConfigContext.
*/
namespace Drupal\Core\Config\Context;
/**
* Defines the global configuration context object.
*
* The global configuration context allows config object data to be overridden
* with values from the $conf global.
*/
class GlobalConfigContext extends ConfigContext {
/**
* Sets global override data.
*
* @return \Drupal\Core\Config\Context\ConfigContext
* The config context object.
*/
public function setGlobalOverride() {
global $conf;
$this->init(self::OVERRIDE, $conf);
return $this;
}
}

View File

@ -52,9 +52,25 @@ class CoreBundle extends Bundle {
->addArgument(new Reference('config.cachedstorage.storage'))
->addArgument(new Reference('cache.config'));
$container->register('config.context.factory', 'Drupal\Core\Config\Context\ConfigContextFactory')
->addArgument(new Reference('event_dispatcher'));
$container->register('config.context', 'Drupal\Core\Config\Context\ContextInterface')
->setFactoryService(new Reference('config.context.factory'))
->setFactoryMethod('get')
->addArgument('Drupal\Core\Config\Context\GlobalConfigContext')
->addTag('persist')
->addMethodCall('setGlobalOverride');
// Register a config context with no overrides for use in administration
// forms, enabling modules and importing configuration.
$container->register('config.context.free', 'Drupal\Core\Config\Context\ContextInterface')
->setFactoryService(new Reference('config.context.factory'))
->setFactoryMethod('get');
$container->register('config.factory', 'Drupal\Core\Config\ConfigFactory')
->addArgument(new Reference('config.storage'))
->addArgument(new Reference('event_dispatcher'))
->addArgument(new Reference('config.context'))
->addTag('persist');
// Register staging configuration storage.
@ -262,7 +278,7 @@ class CoreBundle extends Bundle {
$container->register('request_close_subscriber', 'Drupal\Core\EventSubscriber\RequestCloseSubscriber')
->addArgument(new Reference('module_handler'))
->addTag('event_subscriber');
$container->register('config_global_override_subscriber', 'Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber')
$container->register('config_global_override_subscriber', 'Drupal\Core\EventSubscriber\ConfigOverrideSubscriber')
->addTag('event_subscriber');
$container->register('language_request_subscriber', 'Drupal\Core\EventSubscriber\LanguageRequestSubscriber')
->addArgument(new Reference('language_manager'))

View File

@ -1,39 +0,0 @@
<?php
/**
* @file
* Definition of Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Override configuration values with values in global $conf variable.
*/
class ConfigGlobalOverrideSubscriber implements EventSubscriberInterface {
/**
* Override configuration values with global $conf.
*
* @param Drupal\Core\Config\ConfigEvent $event
* The Event to process.
*/
public function configInit(ConfigEvent $event) {
global $conf;
$config = $event->getConfig();
if (isset($conf[$config->getName()])) {
$config->setOverride($conf[$config->getName()]);
}
}
/**
* Implements EventSubscriberInterface::getSubscribedEvents().
*/
static function getSubscribedEvents() {
$events['config.init'][] = array('configInit', 30);
return $events;
}
}

View File

@ -0,0 +1,41 @@
<?php
/**
* @file
* Contains \Drupal\Core\EventSubscriber\ConfigOverrideSubscriber.
*/
namespace Drupal\Core\EventSubscriber;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigEvent;
use Drupal\Core\Config\Context\ConfigContext;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Override configuration values with predefined values in context.
*/
class ConfigOverrideSubscriber implements EventSubscriberInterface {
/**
* Overrides configuration values.
*
* @param \Drupal\Core\Config\ConfigEvent $event
* The Event to process.
*/
public function configInit(ConfigEvent $event) {
if ($override = $event->getContext()->get(ConfigContext::OVERRIDE)) {
$config = $event->getConfig();
if (isset($override[$config->getName()])) {
$config->setOverride($override[$config->getName()]);
}
}
}
/**
* Implements EventSubscriberInterface::getSubscribedEvents().
*/
public static function getSubscribedEvents() {
$events['config.init'][] = array('configInit', 30);
return $events;
}
}

View File

@ -172,21 +172,13 @@ class ConfigCRUDTest extends DrupalUnitTestBase {
// Write configuration with an invalid name (missing a namespace) to
// staging.
$storage = $this->container->get('config.storage');
$staging = $this->container->get('config.storage.staging');
$manifest_data = config('manifest.invalid_object_name')->get();
$manifest_data['new']['name'] = 'invalid';
$staging->write('manifest.invalid_object_name', $manifest_data);
// Verify that an exception is thrown when synchronizing.
$message = 'Expected ConfigNameException was thrown when attempting to sync invalid configuration.';
try {
config_import();
$this->fail($message);
}
catch (ConfigNameException $e) {
$this->pass($message);
}
// Assert that config_import returns false indicating a failure.
$this->assertFalse(config_import(), "Config import failed when trying to importing an object with an invalid name");
}
}

View File

@ -0,0 +1,157 @@
<?php
/**
* @file
* Definition of \Drupal\config\Tests\ConfigLocaleOverride.
*/
namespace Drupal\config\Tests;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Language\Language;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Tests locale config override.
*/
class ConfigLocaleOverride extends DrupalUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale', 'config_test', 'user', 'language', 'system');
public static function getInfo() {
return array(
'name' => 'Locale override',
'description' => 'Confirm that locale overrides work',
'group' => 'Configuration',
);
}
public function setUp() {
parent::setUp();
config_install_default_config('module', 'config_test');
}
/*
* Tests basic locale override.
*/
function testConfigLocaleOverride() {
$name = 'config_test.system';
// The default language is en so the config key should be localised.
$config = config($name);
$this->assertIdentical($config->get('foo'), 'en bar');
// Ensure that we get the expected value when we use system_config.
config_context_enter('config.context.free');
$config_admin = config('config_test.system');
$this->assertIdentical($config_admin->get('foo'), 'bar');
// Leave the non override context.
config_context_leave();
$config = config($name);
$this->assertIdentical($config->get('foo'), 'en bar');
}
/*
* Tests locale override based on user's preferred language.
*/
function testConfigLocaleUserOverride() {
$this->installSchema('system', 'variable');
$this->installSchema('language', 'language');
language_save(new Language(array(
'name' => 'French',
'langcode' => 'fr',
)));
language_save(new Language(array(
'name' => 'English',
'langcode' => 'en',
)));
language_save(new Language(array(
'name' => 'German',
'langcode' => 'de',
)));
$this->installSchema('user', 'users');
$account = entity_create('user', array(
'name' => 'French user',
'mail' => 'test@example.com',
'created' => REQUEST_TIME,
'status' => 1,
'preferred_langcode' => 'fr',
));
$config_factory = drupal_container()->get('config.factory');
$user_config_context = config_context_enter("Drupal\\user\\UserConfigContext");
$user_config_context->setAccount($account);
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), 'fr bar');
// Ensure that we get the expected value when we leave the user context.
$config_factory->leaveContext();
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), 'en bar');
$account = entity_create('user', array(
'name' => 'German user',
'mail' => 'test@example.com',
'created' => REQUEST_TIME,
'status' => 1,
'preferred_langcode' => 'de',
));
$config_factory->enterContext($user_config_context->setAccount($account));
// Should not have to re-initialise config object to get new overrides as
// the new context will have a different uuid.
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), 'de bar');
// Enter an english context on top of the german context.
$account = entity_create('user', array(
'name' => 'English user',
'mail' => 'test@example.com',
'created' => REQUEST_TIME,
'status' => 1,
'preferred_langcode' => 'en',
));
// Create a new user config context to stack on top of the existign one.
$en_user_config_context = config_context_enter("Drupal\\user\\UserConfigContext");
$en_user_config_context->setAccount($account);
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), 'en bar');
// Ensure that we get the expected value when we leave the english user
// context.
$config_factory->leaveContext();
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), 'de bar');
// Ensure that we get the expected value when we leave the german user
// context.
$config_factory->leaveContext();
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), 'en bar');
// Ensure that we cannot leave the default context.
$config_factory->leaveContext();
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), 'en bar');
}
/*
* Tests config_context_enter() invalid context name handling.
*/
function testInvalidContextName() {
$message = 'Expected ConfigException was thrown for an invalid context_name argument.';
try {
config_context_enter('invalid.config.context');
$this->fail($message);
}
catch (ConfigException $e) {
$this->pass($message);
}
}
}

View File

@ -8,6 +8,7 @@
namespace Drupal\config\Tests;
use Drupal\simpletest\DrupalUnitTestBase;
use Drupal\Core\Config\Context\ConfigContext;
/**
* Tests configuration overrides via $conf in settings.php.
@ -19,7 +20,7 @@ class ConfigOverrideTest extends DrupalUnitTestBase {
*
* @var array
*/
public static $modules = array('config_test');
public static $modules = array('system', 'config_test');
public static function getInfo() {
return array(
@ -29,10 +30,9 @@ class ConfigOverrideTest extends DrupalUnitTestBase {
);
}
function setUp() {
public function setUp() {
parent::setUp();
config_install_default_config('module', 'config_test');
$this->installSchema('system', 'config_snapshot');
}
/**
@ -46,16 +46,39 @@ class ConfigOverrideTest extends DrupalUnitTestBase {
'404' => 'herp',
);
// Set globals before installing to prove that the installed file does not
// contain these values.
$conf['config_test.system']['foo'] = 'overridden';
$conf['config_test.system']['baz'] = 'injected';
$conf['config_test.system']['404'] = 'derp';
drupal_container()->get('config.context')->setGlobalOverride();
config_install_default_config('module', 'config_test');
// Verify that the original configuration data exists. Have to read storage
// directly otherwise overrides will apply.
$active = $this->container->get('config.storage');
$data = $active->read('config_test.system');
$this->assertIdentical($data['foo'], $expected_original_data['foo']);
$this->assertFalse(isset($data['baz']));
$this->assertIdentical($data['404'], $expected_original_data['404']);
// Remove the $conf overrides and reset value in config.context service.
unset($conf['config_test.system']);
drupal_container()->get('config.context')->setGlobalOverride();
// Verify that the original configuration data exists.
$config = config('config_test.system');
$this->assertIdentical($config->get('foo'), $expected_original_data['foo']);
$this->assertIdentical($config->get('baz'), $expected_original_data['baz']);
$this->assertIdentical($config->get('404'), $expected_original_data['404']);
// Apply the overridden data.
// Apply the overridden data, that needs to be set into the config.context
// service.
$conf['config_test.system']['foo'] = 'overridden';
$conf['config_test.system']['baz'] = 'injected';
$conf['config_test.system']['404'] = 'derp';
drupal_container()->get('config.context')->setGlobalOverride();
// Verify that the in-memory configuration object still contains the
// original data.
@ -93,14 +116,45 @@ class ConfigOverrideTest extends DrupalUnitTestBase {
$this->assertIdentical($config->get('baz'), $conf['config_test.system']['baz']);
$this->assertIdentical($config->get('404'), $conf['config_test.system']['404']);
// Remove the $conf overrides.
// Remove the $conf overrides and reset value in config.context service.
unset($conf['config_test.system']);
drupal_container()->get('config.context')->setGlobalOverride();
// Reload it and verify that it still contains the original data.
$config->init();
$this->assertIdentical($config->get('foo'), $expected_original_data['foo']);
$this->assertIdentical($config->get('baz'), $expected_original_data['baz']);
$this->assertIdentical($config->get('404'), $expected_original_data['404']);
// Set globals before importing to prove that the imported file does not
// contain these values.
$conf['config_test.system']['foo'] = 'overridden';
$conf['config_test.system']['baz'] = 'injected';
$conf['config_test.system']['404'] = 'derp';
// Write file to staging
drupal_container()->get('config.context')->setGlobalOverride();
$staging = $this->container->get('config.storage.staging');
$expected_new_data = array(
'foo' => 'barbar',
'404' => 'herpderp',
);
$staging->write('config_test.system', $expected_new_data);
// Import changed data from staging to active.
config_import();
$data = $active->read('config_test.system');
// Verify that the new configuration data exists. Have to read storage
// directly otherwise overrides will apply.
$this->assertIdentical($data['foo'], $expected_new_data['foo']);
$this->assertFalse(isset($data['baz']));
$this->assertIdentical($data['404'], $expected_new_data['404']);
// Verifiy the overrides are still working.
$this->assertIdentical($config->get('foo'), $conf['config_test.system']['foo']);
$this->assertIdentical($config->get('baz'), $conf['config_test.system']['baz']);
$this->assertIdentical($config->get('404'), $conf['config_test.system']['404']);
}
}

View File

@ -1,38 +0,0 @@
<?php
/**
* @file
* Definition of Drupal\config\Tests\LocaleConfigOverride.
*/
namespace Drupal\config\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests locale config override.
*/
class LocaleConfigOverride extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('locale', 'config_test');
public static function getInfo() {
return array(
'name' => 'Locale override',
'description' => 'Confirm that locale overrides work',
'group' => 'Configuration',
);
}
function testLocaleConfigOverride() {
$name = 'config_test.system';
// Verify the default configuration values exist.
$config = config($name);
$this->assertIdentical($config->get('foo'), 'en bar');
}
}

View File

@ -22,6 +22,7 @@ class LocaleBundle extends Bundle {
public function build(ContainerBuilder $container) {
$container->register('locale_config_subscriber', 'Drupal\locale\LocaleConfigSubscriber')
->addArgument(new Reference('language_manager'))
->addArgument(new Reference('config.context'))
->addTag('event_subscriber');
}

View File

@ -1,15 +1,21 @@
<?php
/**
* @file
* Definition of Drupal\locale\LocaleConfigSubscriber.
* Definition of \Drupal\locale\LocaleConfigSubscriber.
*/
namespace Drupal\locale;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\Context\ConfigContext;
use Drupal\Core\Config\Context\ContextInterface;
use Drupal\Core\Config\ConfigEvent;
use Drupal\Core\Config\StorageDispatcher;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageManager;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -19,36 +25,68 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
* $config is always a DrupalConfig object.
*/
class LocaleConfigSubscriber implements EventSubscriberInterface {
/**
* The language manager for retrieving the interface language.
* The language manager.
*
* @var \Drupal\Core\Language\LanguageManager
*/
protected $languageManager;
protected $defaultConfigContext;
/**
* Constructs a LocaleConfigSubscriber object.
*
* @param \Drupal\Core\Config\Context\ConfigContext $config_context
* The config context service.
* @param \Drupal\Core\Language\LanguageManager $language_manager
* The language manager service.
*/
public function __construct(LanguageManager $language_manager) {
public function __construct(LanguageManager $language_manager, ContextInterface $config_context) {
$this->languageManager = $language_manager;
$this->defaultConfigContext = $config_context;
}
/**
* Initialize configuration context with language.
*
* @param \Drupal\Core\Config\ConfigEvent $event
* The Event to process.
*/
public function configContext(ConfigEvent $event) {
$context = $event->getContext();
// Add user's language for user context.
if ($account = $context->get('user.account')) {
$context->set('locale.language', language_load(user_preferred_langcode($account)));
}
elseif ($language = $this->languageManager->getLanguage(LANGUAGE_TYPE_INTERFACE)) {
$context->set('locale.language', $language);
}
}
/**
* Override configuration values with localized data.
*
* @param Drupal\Core\Config\ConfigEvent $event
* @param \Drupal\Core\Config\ConfigEvent $event
* The Event to process.
*/
public function configLoad(ConfigEvent $event) {
$config = $event->getConfig();
$language = $this->languageManager->getLanguage(LANGUAGE_TYPE_INTERFACE);
$locale_name = $this->getLocaleConfigName($config->getName(), $language);
if ($override = $config->getStorage()->read($locale_name)) {
$config->setOverride($override);
$context = $event->getContext();
if ($language = $context->get('locale.language')) {
$config = $event->getConfig();
$locale_name = $this->getLocaleConfigName($config->getName(), $language);
// Check to see if the config storage has an appropriately named file
// containing override data.
if ($override = $event->getConfig()->getStorage()->read($locale_name)) {
$config->setOverride($override);
}
}
}
public function onKernelRequestSetDefaultConfigContextLocale(GetResponseEvent $event) {
if ($language = $this->languageManager->getLanguage(LANGUAGE_TYPE_INTERFACE)) {
$this->defaultConfigContext->set('locale.language', $language);
}
}
@ -57,8 +95,16 @@ class LocaleConfigSubscriber implements EventSubscriberInterface {
*
* It will be the same name with a prefix depending on language code:
* locale.config.LANGCODE.NAME
*
* @param string $name
* The name of the config object.
* @param \Drupal\Core\Language\Language $language
* The language object.
*
* @return string
* The localised config name.
*/
public function getLocaleConfigName($name, $language) {
public function getLocaleConfigName($name, Language $language) {
return 'locale.config.' . $language->langcode . '.' . $name;
}
@ -66,7 +112,9 @@ class LocaleConfigSubscriber implements EventSubscriberInterface {
* Implements EventSubscriberInterface::getSubscribedEvents().
*/
static function getSubscribedEvents() {
$events['config.context'][] = array('configContext', 20);
$events['config.load'][] = array('configLoad', 20);
$events[KernelEvents::REQUEST][] = array('onKernelRequestSetDefaultConfigContextLocale', 20);
return $events;
}
}

View File

@ -1403,6 +1403,7 @@ function system_modules_uninstall_submit($form, &$form_state) {
* @see system_settings_form()
*/
function system_site_information_settings($form, &$form_state) {
config_context_enter('config.context.free');
$site_config = config('system.site');
$site_mail = $site_config->get('mail');
if (empty($site_mail)) {
@ -1509,6 +1510,7 @@ function system_site_information_settings_validate($form, &$form_state) {
* Form submission handler for system_site_information_settings().
*/
function system_site_information_settings_submit($form, &$form_state) {
config_context_enter('config.context.free');
config('system.site')
->set('name', $form_state['values']['site_name'])
->set('mail', $form_state['values']['site_mail'])
@ -1525,6 +1527,7 @@ function system_site_information_settings_submit($form, &$form_state) {
* @ingroup forms
*/
function system_cron_settings($form, &$form_state) {
config_context_enter('config.context.free');
$form['description'] = array(
'#markup' => '<p>' . t('Cron takes care of running periodic tasks like checking for updates and indexing content for search.') . '</p>',
);
@ -1562,6 +1565,7 @@ function system_cron_settings($form, &$form_state) {
* @ingroup forms
*/
function system_cron_settings_submit($form, &$form_state) {
config_context_enter('config.context.free');
config('system.cron')
->set('threshold.autorun', $form_state['values']['cron_safe_threshold'])
->save();
@ -1591,6 +1595,7 @@ function system_run_cron_submit($form, &$form_state) {
* @see system_logging_settings_submit()
*/
function system_logging_settings($form, &$form_state) {
config_context_enter('config.context.free');
$form['error_level'] = array(
'#type' => 'radios',
'#title' => t('Error messages to display'),
@ -1613,6 +1618,7 @@ function system_logging_settings($form, &$form_state) {
* @ingroup forms
*/
function system_logging_settings_submit($form, &$form_state) {
config_context_enter('config.context.free');
config('system.logging')
->set('error_level', $form_state['values']['error_level'])
->save();
@ -1626,6 +1632,7 @@ function system_logging_settings_submit($form, &$form_state) {
*/
function system_performance_settings($form, &$form_state) {
drupal_add_library('system', 'drupal.system');
config_context_enter('config.context.free');
$config = config('system.performance');
$form['clear_cache'] = array(
@ -1715,6 +1722,7 @@ function system_performance_settings($form, &$form_state) {
* @ingroup forms
*/
function system_performance_settings_submit($form, &$form_state) {
config_context_enter('config.context.free');
$config = config('system.performance');
$config->set('cache.page.enabled', $form_state['values']['cache']);
$config->set('cache.page.max_age', $form_state['values']['page_cache_maximum_age']);
@ -1860,6 +1868,7 @@ function system_image_toolkit_settings() {
* @ingroup forms
*/
function system_rss_feeds_settings($form, &$form_state) {
config_context_enter('config.context.free');
$rss_config = config('system.rss');
$form['feed_description'] = array(
'#type' => 'textarea',
@ -1895,6 +1904,7 @@ function system_rss_feeds_settings($form, &$form_state) {
* @ingroup forms
*/
function system_rss_feeds_settings_submit($form, &$form_state) {
config_context_enter('config.context.free');
config('system.rss')
->set('channel.description', $form_state['values']['feed_description'])
->set('items.limit', $form_state['values']['feed_default_items'])
@ -2015,6 +2025,7 @@ function system_regional_settings_submit($form, &$form_state) {
* @see system_site_maintenance_mode_submit()
*/
function system_site_maintenance_mode($form, &$form_state) {
config_context_enter('config.context.free');
$config = config('system.maintenance');
$form['maintenance_mode'] = array(
'#type' => 'checkbox',
@ -2037,6 +2048,7 @@ function system_site_maintenance_mode($form, &$form_state) {
* @ingroup forms
*/
function system_site_maintenance_mode_submit($form, &$form_state) {
config_context_enter('config.context.free');
config('system.maintenance')
->set('enabled', $form_state['values']['maintenance_mode'])
->set('message', $form_state['values']['maintenance_mode_message'])

View File

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\user\UserConfigContext
*/
namespace Drupal\user;
use Drupal\Core\Config\Context\ConfigContext;
use Drupal\user\Plugin\Core\Entity\User;
/**
* Defines a configuration context object for a user account.
*
* This should be used when configuration objects need a context for a user
* other than the current user.
*
* @see user_mail()
*/
class UserConfigContext extends ConfigContext {
/**
* Predefined key for account object.
*/
const USER_KEY = 'user.account';
/**
* Implements \Drupal\Core\Config\Context\ContextInterface::setUuid().
*/
public function setUuid() {
// Use the user's uuid to identify the config context.
$this->uuid = $this->get(self::USER_KEY)->uuid();
}
/*
* Helper function to create config context for user accounts.
*
* @param \Drupal\user\Plugin\Core\Entity\User $account
* The account to add to the config context.
*
* @return \Drupal\user\UserConfigContext
* The user config context object.
*/
public function setAccount(User $account) {
$this->init(self::USER_KEY, $account);
return $this;
}
}

View File

@ -292,6 +292,7 @@ function user_admin_account_validate($form, &$form_state) {
* @see user_admin_settings_submit()
*/
function user_admin_settings($form, &$form_state) {
config_context_enter('config.context.free');
$config = config('user.settings');
$mail_config = config('user.mail');
@ -641,6 +642,7 @@ function user_admin_settings($form, &$form_state) {
* Form submission handler for user_admin_settings().
*/
function user_admin_settings_submit($form, &$form_state) {
config_context_enter('config.context.free');
config('user.settings')
->set('anonymous', $form_state['values']['anonymous'])
->set('admin_role', $form_state['values']['user_admin_role'])

View File

@ -1756,35 +1756,30 @@ function user_view_multiple($accounts, $view_mode = 'full', $langcode = NULL) {
function user_mail($key, &$message, $params) {
$langcode = $message['langcode'];
$variables = array('user' => $params['account']);
$message['subject'] .= _user_mail_text($key . '.subject', $langcode, $variables);
$message['body'][] = _user_mail_text($key . '.body', $langcode, $variables);
}
/**
* Returns a mail string for a variable name.
*
* @param string $key
* The config key that provides the mail text.
* @param string $langcode
* (optional) A language code to use to generate the e-mail text.
* @param array $variables
* (optional) An array of token keys and values.
*
* @return
* A string value containing the text for the user.mail config key.
*/
function _user_mail_text($key, $langcode = NULL, $variables = array()) {
// We do not sanitize the token replacement, since the output of this
// replacement is intended for an e-mail message, not a web browser.
return token_replace(config('user.mail')->get($key), $variables, array('langcode' => $langcode, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE));
// Get configuration objects customized for the user specified in $params as
// this user is not necessarily the same as the one triggering the mail. This
// allows the configuration objects to be localized for the user's language if
// the locale module is enabled.
$user_config_context = config_context_enter("Drupal\\user\\UserConfigContext");
$user_config_context->setAccount($params['account']);
$mail_config = config('user.mail');
// We do not sanitize the token replacement, since the output of this
// replacement is intended for an e-mail message, not a web browser.
$token_options = array('langcode' => $langcode, 'callback' => 'user_mail_tokens', 'sanitize' => FALSE, 'clear' => TRUE);
$message['subject'] .= token_replace($mail_config->get($key . '.subject'), $variables, $token_options);
$message['body'][] = token_replace($mail_config->get($key . '.body'), $variables, $token_options);
// Return the previous config context.
config_context_leave();
}
/**
* Token callback to add unsafe tokens for user mails.
*
* This function is used by the token_replace() call at the end of
* _user_mail_text() to set up some additional tokens that can be
* used in email messages generated by user_mail().
* This function is used by the token_replace() to set up some additional
* tokens that can be used in email messages generated by user_mail().
*
* @param $replacements
* An associative array variable containing mappings from token names to