Commit 0612c664 authored by catch's avatar catch

Issue #1763640 by beejeebus, alexpott, Jose Reyero, c31ck, das-peter, YesCT,...

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.
parent 3714bed1
<?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 @@
* 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]);
}
}
......
......@@ -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');
......
......@@ -9,7 +9,7 @@
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 @@ public function getStorage() {
* 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);
}
/**
......
......@@ -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
*/
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(Config $config) {
public function __construct(ContextInterface $context, Config $config = NULL) {
$this->config = $config;
$this->context = $context;
}
/**
......@@ -26,4 +40,14 @@ public function __construct(Config $config) {
public function getConfig() {
return $this->config;
}
/**
* Get configuration context object.
*
* @return \Drupal\Core\Config\Context\ContextInterface
* Configuration context.
*/
public function getContext() {
return $this->context;
}
}
......@@ -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 @@
* 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 @@ public function get($name) {
* @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 @@ public function reset($name = NULL) {
$config->init();
}
}
return $this;
}
/**
......@@ -108,14 +117,84 @@ public function reset($name = NULL) {
* @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 );
});
}
}
<?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));
}
}
<?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;
}
}
<?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.