Commit 68d6316b authored by catch's avatar catch

Issue #2224887 by alexpott, Berdir, Gábor Hojtsy, Jalandhar: Language...

Issue #2224887 by alexpott, Berdir, Gábor Hojtsy, Jalandhar: Language configuration overrides should have their own storage.
parent d98e9cb1
......@@ -77,7 +77,7 @@ services:
arguments: [discovery]
config.manager:
class: Drupal\Core\Config\ConfigManager
arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage']
arguments: ['@entity.manager', '@config.factory', '@config.typed', '@string_translation', '@config.storage', '@event_dispatcher']
config.factory:
class: Drupal\Core\Config\ConfigFactory
tags:
......
......@@ -237,7 +237,6 @@ public function save() {
* The configuration object.
*/
public function delete() {
// @todo Consider to remove the pruning of data for Config::delete().
$this->data = array();
$this->storage->delete($this->name);
$this->isNew = TRUE;
......
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigCollectionInfo.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\Event;
/**
* Gets information on all the possible configuration collections.
*/
class ConfigCollectionInfo extends Event {
/**
* Configuration collection information keyed by collection name.
*
* The value is either the configuration factory override that is responsible
* for the collection or null if there is not one.
*
* @var array
*/
protected $collections = array();
/**
* Adds a collection to the list of possible collections.
*
* @param string $collection
* Collection name to add.
* @param \Drupal\Core\Config\ConfigFactoryOverrideInterface
* (optional) The configuration factory override service responsible for the
* collection.
*
* @throws \InvalidArgumentException
* Exception thrown if $collection is equal to
* \Drupal\Core\Config\StorageInterface::DEFAULT_COLLECTION
*/
public function addCollection($collection, ConfigFactoryOverrideInterface $override_service = NULL) {
if ($collection == StorageInterface::DEFAULT_COLLECTION) {
throw new \InvalidArgumentException('Can not add the default collection to the ConfigCollectionInfo object');
}
$this->collections[$collection] = $override_service;
}
/**
* Gets the list of possible collection names.
*
* @param bool $include_default
* (Optional) Include the default collection. Defaults to TRUE.
*
* @return array
* The list of possible collection names.
*/
public function getCollectionNames($include_default = TRUE) {
$collection_names = array_keys($this->collections);
sort($collection_names);
if ($include_default) {
array_unshift($collection_names, StorageInterface::DEFAULT_COLLECTION);
}
return $collection_names;
}
/**
* Gets the config factory override service responsible for the collection.
*
* @param string $collection
* The configuration collection.
*
* @return \Drupal\Core\Config\ConfigFactoryOverrideInterface|NULL
* The override service responsible for the collection if one exists. NULL
* if not.
*/
public function getOverrideService($collection) {
return isset($this->collections[$collection]) ? $this->collections[$collection] : NULL;
}
}
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigCollectionNamesEvent.
*/
namespace Drupal\Core\Config;
use Symfony\Component\EventDispatcher\Event;
/**
* Wraps a configuration event for event listeners.
*/
class ConfigCollectionNamesEvent extends Event {
/**
* Configuration collection names.
*
* @var array
*/
protected $collections = array();
/**
* Adds names to the list of possible collections.
*
* @param array $collections
* Collection names to add.
*/
public function addCollectionNames(array $collections) {
$this->collections = array_merge($this->collections, $collections);
}
/**
* Adds a name to the list of possible collections.
*
* @param string $collection
* Collection name to add.
*/
public function addCollectionName($collection) {
$this->addCollectionNames(array($collection));
}
/**
* Gets the list of possible collection names.
*
* @return array
* The list of possible collection names.
*/
public function getCollectionNames($include_default = TRUE) {
sort($this->collections);
$collections = array_unique($this->collections);
if ($include_default) {
array_unshift($collections, StorageInterface::DEFAULT_COLLECTION);
}
return $collections;
}
}
......@@ -51,10 +51,11 @@ final class ConfigEvents {
const IMPORT = 'config.importer.import';
/**
* Name of event fired to discover all the possible configuration collections.
* Name of event fired to collect information on all collections.
*
* @see \Drupal\Core\Config\ConfigInstaller::installDefaultConfig()
* @see \Drupal\Core\Config\ConfigManager::getConfigCollectionInfo()
* @see \Drupal\Core\Config\ConfigCollectionInfo
*/
const COLLECTION_NAMES = 'config.collection_names';
const COLLECTION_INFO = 'config.collection_info';
}
......@@ -32,4 +32,30 @@ public function loadOverrides($names);
*/
public function getCacheSuffix();
/**
* Creates a configuration object for use during install and synchronization.
*
* If the overrider stores it's overrides in configuration collections then
* it can have its own implementation of
* \Drupal\Core\Config\StorableConfigBase. Configuration overriders can link
* themselves to a configuration collection by listening to the
* \Drupal\Core\Config\ConfigEvents::COLLECTION_INFO event and adding the
* collections they are responsible for. Doing this will allow installation
* and synchronization to use the overrider's implementation of
* StorableConfigBase.
*
* @see \Drupal\Core\Config\ConfigCollectionInfo
* @see \Drupal\Core\Config\ConfigImporter::importConfig()
* @see \Drupal\Core\Config\ConfigInstaller::createConfiguration()
*
* @param string $name
* The configuration object name.
* @param string $collection
* The configuration collection.
*
* @return \Drupal\Core\Config\StorableConfigBase
* The configuration object for the provided name and collection.
*/
public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION);
}
......@@ -896,7 +896,15 @@ protected function checkOp($collection, $op, $name) {
* The name of the configuration to process.
*/
protected function importConfig($collection, $op, $name) {
// Allow config factory overriders to use a custom configuration object if
// they are responsible for the collection.
$overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
if ($overrider) {
$config = $overrider->createConfigObject($name, $collection);
}
else {
$config = new Config($name, $this->storageComparer->getTargetStorage($collection), $this->eventDispatcher, $this->typedConfigManager);
}
if ($op == 'delete') {
$config->delete();
}
......
......@@ -96,10 +96,8 @@ public function installDefaultConfig($type, $name) {
$this->typedConfig->clearCachedDefinitions();
}
// Gather all the supported collection names.
$event = new ConfigCollectionNamesEvent();
$this->eventDispatcher->dispatch(ConfigEvents::COLLECTION_NAMES, $event);
$collections = $event->getCollectionNames();
// Gather information about all the supported collections.
$collection_info = $this->configManager->getConfigCollectionInfo();
$old_state = $this->configFactory->getOverrideState();
$this->configFactory->setOverrideState(FALSE);
......@@ -110,7 +108,7 @@ public function installDefaultConfig($type, $name) {
$enabled_extensions = array_keys((array) $extension_config->get('module'));
$enabled_extensions += array_keys((array) $extension_config->get('theme'));
foreach ($collections as $collection) {
foreach ($collection_info->getCollectionNames(TRUE) as $collection) {
$config_to_install = $this->listDefaultConfigCollection($collection, $type, $name, $enabled_extensions);
if (!empty($config_to_install)) {
$this->createConfiguration($collection, $config_to_install);
......@@ -185,7 +183,15 @@ protected function createConfiguration($collection, array $config_to_install) {
$config_to_install = array_diff($config_to_install, $this->getActiveStorage($collection)->listAll());
foreach ($config_to_install as $name) {
// Allow config factory overriders to use a custom configuration object if
// they are responsible for the collection.
$overrider = $this->configManager->getConfigCollectionInfo()->getOverrideService($collection);
if ($overrider) {
$new_config = $overrider->createConfigObject($name, $collection);
}
else {
$new_config = new Config($name, $this->getActiveStorage($collection), $this->eventDispatcher, $this->typedConfig);
}
if ($data[$name] !== FALSE) {
$new_config->setData($data[$name]);
}
......
......@@ -12,6 +12,7 @@
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\TranslationManager;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* The ConfigManager provides helper functions for the configuration system.
......@@ -53,6 +54,27 @@ class ConfigManager implements ConfigManagerInterface {
*/
protected $activeStorage;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The configuration collection info.
*
* @var \Drupal\Core\Config\ConfigCollectionInfo
*/
protected $configCollectionInfo;
/**
* The configuration storages keyed by collection name.
*
* @var \Drupal\Core\Config\StorageInterface[]
*/
protected $storages;
/**
* Creates ConfigManager objects.
*
......@@ -64,13 +86,18 @@ class ConfigManager implements ConfigManagerInterface {
* The typed config manager.
* @param \Drupal\Core\StringTranslation\TranslationManager $string_translation
* The string translation service.
* @param \Drupal\Core\Config\StorageInterface $active_storage
* The active configuration storage.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
*/
public function __construct(EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, TypedConfigManager $typed_config_manager, TranslationManager $string_translation, StorageInterface $active_storage) {
public function __construct(EntityManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, TypedConfigManager $typed_config_manager, TranslationManager $string_translation, StorageInterface $active_storage, EventDispatcherInterface $event_dispatcher) {
$this->entityManager = $entity_manager;
$this->configFactory = $config_factory;
$this->typedConfigManager = $typed_config_manager;
$this->stringTranslation = $string_translation;
$this->activeStorage = $active_storage;
$this->eventDispatcher = $event_dispatcher;
}
/**
......@@ -246,4 +273,16 @@ public function findConfigEntityDependentsAsEntities($type, array $names) {
public function supportsConfigurationEntities($collection) {
return $collection == StorageInterface::DEFAULT_COLLECTION;
}
/**
* {@inheritdoc}
*/
public function getConfigCollectionInfo() {
if (!isset($this->configCollectionInfo)) {
$this->configCollectionInfo = new ConfigCollectionInfo();
$this->eventDispatcher->dispatch(ConfigEvents::COLLECTION_INFO, $this->configCollectionInfo);
}
return $this->configCollectionInfo;
}
}
......@@ -123,4 +123,12 @@ public function findConfigEntityDependentsAsEntities($type, array $names);
*/
public function supportsConfigurationEntities($collection);
/**
* Gets available collection information using the event system.
*
* @return \Drupal\Core\Config\ConfigCollectionInfo
* The object which contains information about the available collections.
*/
public function getConfigCollectionInfo();
}
......@@ -51,9 +51,9 @@ public function testDefaultConfig() {
$default_config_storage = new TestInstallStorage();
foreach ($default_config_storage->listAll() as $config_name) {
// @todo: remove once migration (https://drupal.org/node/2183957) and
// translation (https://drupal.org/node/2168609) schemas are in.
if (strpos($config_name, 'migrate.migration') === 0 || strpos($config_name, 'language.config') === 0) {
// @todo: remove once migration (https://drupal.org/node/2183957) schemas
// are in.
if (strpos($config_name, 'migrate.migration') === 0) {
continue;
}
......
......@@ -7,7 +7,7 @@
namespace Drupal\config_collection_install_test;
use Drupal\Core\Config\ConfigCollectionNamesEvent;
use Drupal\Core\Config\ConfigCollectionInfo;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\State\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
......@@ -34,18 +34,21 @@ public function __construct(StateInterface $state) {
/**
* Reacts to the ConfigEvents::COLLECTION_NAMES event.
*
* @param \Drupal\Core\Config\ConfigCollectionNamesEvent $event
* @param \Drupal\Core\Config\ConfigCollectionInfo $collection_info
* The configuration collection names event.
*/
public function addCollectionNames(ConfigCollectionNamesEvent $event) {
$event->addCollectionNames($this->state->get('config_collection_install_test.collection_names', array()));
public function addCollections(ConfigCollectionInfo $collection_info) {
$collections = $this->state->get('config_collection_install_test.collection_names', array());
foreach ($collections as $collection) {
$collection_info->addCollection($collection);
}
}
/**
* {@inheritdoc}
*/
static function getSubscribedEvents() {
$events[ConfigEvents::COLLECTION_NAMES][] = array('addCollectionNames');
$events[ConfigEvents::COLLECTION_INFO][] = array('addCollections');
return $events;
}
......
......@@ -10,6 +10,7 @@
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigFactoryOverrideInterface;
use Drupal\Core\Config\ConfigModuleOverridesEvent;
use Drupal\Core\Config\StorageInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
......@@ -40,5 +41,12 @@ public function getCacheSuffix() {
return 'ConfigOverrider';
}
/**
* {@inheritdoc}
*/
public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) {
return NULL;
}
}
......@@ -8,6 +8,7 @@
namespace Drupal\config_override;
use Drupal\Core\Config\ConfigFactoryOverrideInterface;
use Drupal\Core\Config\StorageInterface;
/**
* Tests module overrides for configuration.
......@@ -41,5 +42,12 @@ public function getCacheSuffix() {
return 'ConfigOverriderLowPriority';
}
/**
* {@inheritdoc}
*/
public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) {
return NULL;
}
}
......@@ -15,6 +15,7 @@
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Language\Language;
use Drupal\language\Config\LanguageConfigOverride;
use Drupal\language\ConfigurableLanguageManagerInterface;
use Drupal\locale\StringStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -352,7 +353,7 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d
* Set the configuration in this language.
* @param \Drupal\Core\Config\Config $base_config
* Base configuration values, in the source language.
* @param \Drupal\Core\Config\Config $config_translation
* @param \Drupal\language\Config\LanguageConfigOverride $config_translation
* Translation configuration override data.
* @param array $config_values
* A simple one dimensional or recursive array:
......@@ -372,7 +373,7 @@ protected function buildConfigForm(Element $schema, $config_data, $base_config_d
* @return array
* Translation configuration override data.
*/
protected function setConfig(Language $language, Config $base_config, Config $config_translation, array $config_values, $shipped_config = FALSE) {
protected function setConfig(Language $language, Config $base_config, LanguageConfigOverride $config_translation, array $config_values, $shipped_config = FALSE) {
foreach ($config_values as $key => $value) {
if (is_array($value) && !isset($value['translation'])) {
// Traverse into this level in the configuration.
......
......@@ -425,6 +425,8 @@ function language_save($language) {
$language_entity->save();
$t_args = array('%language' => $language->name, '%langcode' => $language->id);
if ($language->is_new) {
// Install any available language configuration overrides for the language.
\Drupal::service('language.config_factory_override')->installLanguageOverrides($language->getId());
watchdog('language', 'The %language (%langcode) language has been created.', $t_args);
}
else {
......
......@@ -16,3 +16,4 @@ services:
arguments: ['@config.storage', '@event_dispatcher', '@config.typed']
tags:
- { name: config.factory.override, priority: -254 }
- { name: event_subscriber }
......@@ -7,24 +7,37 @@
namespace Drupal\language\Config;
use Drupal\Core\Config\Config;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\ConfigCollectionInfo;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\TypedConfigManagerInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageDefault;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Provides language overrides for the configuration factory.
*/
class LanguageConfigFactoryOverride implements LanguageConfigFactoryOverrideInterface {
class LanguageConfigFactoryOverride implements LanguageConfigFactoryOverrideInterface, EventSubscriberInterface {
/**
* The configuration storage.
*
* Do not access this directly. Should be accessed through self::getStorage()
* so that the cache of storages per langcode is used.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $storage;
protected $baseStorage;
/**
* An array of configuration storages keyed by langcode.
*
* @var \Drupal\Core\Config\StorageInterface[]
*/
protected $storages;
/**
* The typed config manager.
......@@ -58,7 +71,7 @@ class LanguageConfigFactoryOverride implements LanguageConfigFactoryOverrideInte
* The typed configuration manager.
*/
public function __construct(StorageInterface $storage, EventDispatcherInterface $event_dispatcher, TypedConfigManagerInterface $typed_config) {
$this->storage = $storage;
$this->baseStorage = $storage;
$this->eventDispatcher = $event_dispatcher;
$this->typedConfigManager = $typed_config;
}
......@@ -67,72 +80,34 @@ public function __construct(StorageInterface $storage, EventDispatcherInterface
* {@inheritdoc}
*/
public function loadOverrides($names) {
$data = array();
$language_names = $this->getLanguageConfigNames($names);
if ($language_names) {
$data = $this->storage->readMultiple(array_values($language_names));
// Re-key the data array to use configuration names rather than override
// names.
$prefix_length = strlen(static::LANGUAGE_CONFIG_PREFIX . '.' . $this->language->id) + 1;
foreach ($data as $key => $value) {
unset($data[$key]);
$key = substr($key, $prefix_length);
$data[$key] = $value;
}
if ($this->language) {
$storage = $this->getStorage($this->language->getId());
return $storage->readMultiple($names);
}
return $data;
return array();
}
/**
* {@inheritdoc}
*/
public function getOverride($langcode, $name) {
$override_name = $this->getLanguageConfigName($langcode, $name);
$overrides = $this->storage->read($override_name);
$config = new Config($override_name, $this->storage, $this->eventDispatcher, $this->typedConfigManager);
if (!empty($overrides)) {
$config->initWithData($overrides);
}
return $config;
}
/**
* Generate a list of configuration names based on base names.
*
* @param array $names
* List of configuration names.
*
* @return array
* List of configuration names for language override files if applicable.
*/
protected function getLanguageConfigNames(array $names) {
$language_names = array();
if (isset($this->language)) {
foreach ($names as $name) {
if ($language_name = $this->getLanguageConfigName($this->language->id, $name)) {
$language_names[$name] = $language_name;
}
$storage = $this->getStorage($langcode);
$data = $storage->read($name);
$override = new LanguageConfigOverride($name, $storage, $this->typedConfigManager);
if (!empty($data)) {
$override->initWithData($data);
}
}
return $language_names;
return $override;
}
/**
* Get language override name for given language and configuration name.
*
* @param string $langcode
* Language code.
* @param string $name
* Configuration name.
*
* @return bool|string
* Configuration name or FALSE if not applicable.
* {@inheritdoc}
*/
protected function getLanguageConfigName($langcode, $name) {
if (strpos($name, static::LANGUAGE_CONFIG_PREFIX) === 0) {
return FALSE;
public function getStorage($langcode) {
if (!isset($this->storages[$langcode])) {
$this->storages[$langcode] = $this->baseStorage->createCollection($this->createConfigCollectionName($langcode));
}
return static::LANGUAGE_CONFIG_PREFIX . '.' . $langcode . '.' . $name;
return $this->storages[$langcode];
}
/**
......@@ -165,4 +140,77 @@ public function setLanguageFromDefault(LanguageDefault $language_default = NULL)
return $this;
}
/**
* {@inheritdoc}
*/
public function installLanguageOverrides($langcode) {
/** @var \Drupal\Core\Config\ConfigInstallerInterface $config_installer */
$config_installer = \Drupal::service('config.installer');
$config_installer->installCollectionDefaultConfig($this->createConfigCollectionName($langcode));
}
/**
* {@inheritdoc}
*/
public function createConfigObject($name, $collection = StorageInterface::DEFAULT_COLLECTION) {
$langcode = $this->getLangcodeFromCollectionName($collection);
return $this->getOverride($langcode, $name);
}
/**
* Creates a configuration collection name based on a langcode.
*
* @param string $langcode
* The langcode.
*
* @return string
* The configuration collection name for a langcode.
*/
protected function createConfigCollectionName($langcode) {
return 'language.' . $langcode;
}
/**
* Converts a configuration collection name to a langcode.
*
* @param string $collection
* The configuration collection name.
*
* @return string
* The langcode of the collection.
*
* @throws \InvalidArgumentException
* Exception thrown if the provided collection name is not in the format
* "language.LANGCODE".
*
* @see self::createConfigCollectionName()
*/
protected function getLangcodeFromCollectionName($collection) {
preg_match('/^language\.(.*)$/', $collection, $matches);
if (!isset($matches[1])) {
throw new \InvalidArgumentException(String::format('!collection is not a valid language override collection', array('!collection' => $collection)));
}
return $matches[1];
}
/**
* Reacts to the ConfigEvents::COLLECTION_INFO event.
*
* @param \Drupal\Core\Config\ConfigCollectionInfo $collection_info
* The configuration collection names event.
*/
public function addCollections(ConfigCollectionInfo $collection_info) {
foreach (\Drupal::languageManager()->getLanguages() as $language) {
$collection_info->addCollection($this->createConfigCollectionName($language->getId()), $this);
}
}
/**
* {@inheritdoc}