diff --git a/core/lib/Drupal/Core/Config/ConfigFactory.php b/core/lib/Drupal/Core/Config/ConfigFactory.php index 5d3756e..f4e44d1 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactory.php +++ b/core/lib/Drupal/Core/Config/ConfigFactory.php @@ -312,4 +312,11 @@ public function addOverride(ConfigFactoryOverrideInterface $config_factory_overr $this->configFactoryOverrides[] = $config_factory_override; } + /** + * {@inheritdoc} + */ + public function getOverrides() { + return $this->configFactoryOverrides; + } + } diff --git a/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php b/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php index 55eb6f6..cafd3fd 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigFactoryInterface.php @@ -134,4 +134,11 @@ public function listAll($prefix = ''); */ public function addOverride(ConfigFactoryOverrideInterface $config_factory_override); + /** + * Get all added configuration factory override instances. + * + * @return ConfigFactoryOverrideInterface[] + * The configuration factory override instances. + */ + public function getOverrides(); } diff --git a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php index 06e1f3f..96c8d48 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideInterface.php @@ -32,4 +32,24 @@ public function loadOverrides($names); */ public function getCacheSuffix(); + /** + * Reacts to default configuration installation during extension install. + * + * @param string $type + * The type of extension being installed. Either 'module' or 'theme'. + * @param string $name + * The name of the extension. + */ + public function install($type, $name); + + /** + * Reacts to configuration removal during extension uninstallation. + * + * @param string $type + * The type of extension being uninstalled. Either 'module' or 'theme'. + * @param string $name + * The name of the extension. + */ + public function uninstall($type, $name); + } diff --git a/core/lib/Drupal/Core/Config/ConfigFactoryOverrideSyncInterface.php b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideSyncInterface.php new file mode 100644 index 0000000..340564c --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigFactoryOverrideSyncInterface.php @@ -0,0 +1,31 @@ +moduleHandler->alter('config_import_steps', $sync_steps, $this); @@ -602,6 +604,38 @@ public function processConfigurations(array &$context) { } } + public function processOverrides(array &$context) { + if (!isset($context['sandbox']['overrides'])) { + // Allow overriders to process staged configuration. + $context['sandbox']['overrides'] = array(); + $context['sandbox']['override_count'] = 0; + $context['sandbox']['override_progress_count'] = 0; + foreach ($this->configManager->getConfigFactory()->getOverrides() as $overrider) { + if ($overrider instanceof ConfigFactoryOverrideSyncInterface) { + $context['sandbox']['overrides'][] = array(get_class($overrider), 'configSyncStep'); + $context['sandbox']['override_count']++; + } + } + } + if (empty($context['sandbox']['overrides'])) { + $context['finished'] = 1; + } + else { + $callable = current($context['sandbox']['overrides']); + call_user_func_array($callable, array(&$context, $this)); + // Have to change $context['finished'] to account for the fact there maybe + // more that one overrider. + $new_finished = ($context['sandbox']['override_progress_count'] + $context['finished']) / $context['sandbox']['override_count']; + if ($context['finished'] == 1) { + // We've complete the current process - remove it. + array_shift($context['sandbox']['overrides']); + + $context['sandbox']['override_progress_count']++; + } + $context['finished'] = $new_finished; + } + } + /** * Finishes the batch. * @@ -997,7 +1031,7 @@ public function getId() { */ protected function reInjectMe() { $this->eventDispatcher = \Drupal::service('event_dispatcher'); - $this->configFactory = \Drupal::configFactory(); + $this->configManager = \Drupal::service('config.manager'); $this->entityManager = \Drupal::entityManager(); $this->lock = \Drupal::lock(); $this->typedConfigManager = \Drupal::service('config.typed'); diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php index a22907b..b9e72e0 100644 --- a/core/lib/Drupal/Core/Config/ConfigInstaller.php +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -138,12 +138,12 @@ public function installDefaultConfig($type, $name) { // Remove configuration that already exists in the active storage. $sorted_config = array_diff($sorted_config, $this->activeStorage->listAll()); - foreach ($sorted_config as $name) { - $new_config = new Config($name, $this->activeStorage, $this->eventDispatcher, $this->typedConfig); - if ($data[$name] !== FALSE) { - $new_config->setData($data[$name]); + foreach ($sorted_config as $config_name) { + $new_config = new Config($config_name, $this->activeStorage, $this->eventDispatcher, $this->typedConfig); + if ($data[$config_name] !== FALSE) { + $new_config->setData($data[$config_name]); } - if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { + if ($entity_type = $this->configManager->getEntityTypeIdByName($config_name)) { // If we are syncing do not create configuration entities. Pluggable // configuration entities can have dependencies on modules that are @@ -159,8 +159,8 @@ public function installDefaultConfig($type, $name) { ->getStorage($entity_type); // It is possible that secondary writes can occur during configuration // creation. Updates of such configuration are allowed. - if ($this->activeStorage->exists($name)) { - $id = $entity_storage->getIDFromConfigName($name, $entity_storage->getEntityType()->getConfigPrefix()); + if ($this->activeStorage->exists($config_name)) { + $id = $entity_storage->getIDFromConfigName($config_name, $entity_storage->getEntityType()->getConfigPrefix()); $entity = $entity_storage->load($id); foreach ($new_config->get() as $property => $value) { $entity->set($property, $value); @@ -179,6 +179,12 @@ public function installDefaultConfig($type, $name) { } $this->configFactory->setOverrideState($old_state); } + + // Allow configuration factory overrides to respond to installation. + foreach ($this->configFactory->getOverrides() as $config_override) { + $config_override->install($type, $name); + } + // Reset all the static caches and list caches. $this->configFactory->reset(); } diff --git a/core/lib/Drupal/Core/Config/ConfigManager.php b/core/lib/Drupal/Core/Config/ConfigManager.php index 6faa23e..0fe9a72 100644 --- a/core/lib/Drupal/Core/Config/ConfigManager.php +++ b/core/lib/Drupal/Core/Config/ConfigManager.php @@ -156,6 +156,11 @@ public function uninstall($type, $name) { foreach ($config_names as $config_name) { $this->configFactory->get($config_name)->delete(); } + // Allow configuration factory overrides to respond to uninstallation. + foreach ($this->configFactory->getOverrides() as $config_override) { + $config_override->uninstall($type, $name); + } + $schema_dir = drupal_get_path($type, $name) . '/' . InstallStorage::CONFIG_SCHEMA_DIRECTORY; if (is_dir($schema_dir)) { // Refresh the schema cache if uninstalling an extension that provides diff --git a/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php index 11ab691..6509e00 100644 --- a/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -73,18 +73,13 @@ public function exists($name) { * {@inheritdoc} */ public function read($name) { - $data = FALSE; - try { - $raw = $this->connection->query('SELECT data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name', array(':name' => $name), $this->options)->fetchField(); - if ($raw !== FALSE) { - $data = $this->decode($raw); - } + $data = $this->readMultiple(array($name)); + if (isset($data[$name])) { + return $data[$name]; } - catch (\Exception $e) { - // If we attempt a read without actually having the database or the table - // available, just return FALSE so the caller can handle it. - } - return $data; + // @todo https://drupal.org/node/2260961 change return NULL when no + // configuration object found to be consistent with other parts of Drupal. + return FALSE; } /** diff --git a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php index 88cdeab..90a3dc9 100644 --- a/core/modules/config/lib/Drupal/config/Controller/ConfigController.php +++ b/core/modules/config/lib/Drupal/config/Controller/ConfigController.php @@ -9,6 +9,7 @@ use Drupal\Core\Archiver\ArchiveTar; use Drupal\Component\Serialization\Yaml; +use Drupal\Core\Config\ConfigFactoryOverrideSyncInterface; use Drupal\Core\Config\ConfigManagerInterface; use Drupal\Core\Config\StorageInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; @@ -89,6 +90,19 @@ public function downloadExport() { $archiver->addString("$name.yml", Yaml::encode(\Drupal::config($name)->get())); } + // Allow overriders to add files to the export. + foreach (\Drupal::configFactory()->getOverrides() as $overrider) { + if ($overrider instanceof ConfigFactoryOverrideSyncInterface) { + foreach ($overrider->getStorages() as $storage) { + // @todo consider adding getExportDir to interface. But where? + $overrider_dir = $storage->getExportDir(); + foreach ($storage->listAll() as $name) { + $archiver->addString($overrider_dir . '/' . $name . '.yml', Yaml::encode($storage->read($name))); + } + } + } + } + $request = new Request(array('file' => 'config.tar.gz')); return $this->fileDownloadController->download($request, 'temporary'); } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index a9eaac7..b13d9d4 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -8,6 +8,7 @@ namespace Drupal\config\Form; use Drupal\Component\Uuid\UuidInterface; +use Drupal\Core\Config\ConfigFactoryOverrideSyncInterface; use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Entity\EntityManagerInterface; diff --git a/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php b/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php index 8a5d022..d5e6fdf 100644 --- a/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/DefaultConfigTest.php @@ -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; } diff --git a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php index 106df3d..355869f 100644 --- a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php +++ b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverrider.php @@ -40,5 +40,17 @@ public function getCacheSuffix() { return 'ConfigOverrider'; } + /** + * {@inheritdoc} + */ + public function install($type, $name) { + } + + /** + * {@inheritdoc} + */ + public function uninstall($type, $name) { + } + } diff --git a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php index a9a435e..19ea21e 100644 --- a/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php +++ b/core/modules/config/tests/config_override/lib/Drupal/config_override/ConfigOverriderLowPriority.php @@ -41,5 +41,17 @@ public function getCacheSuffix() { return 'ConfigOverriderLowPriority'; } + /** + * {@inheritdoc} + */ + public function install($type, $name) { + } + + /** + * {@inheritdoc} + */ + public function uninstall($type, $name) { + } + } diff --git a/core/modules/config/tests/config_test/config/install/language.config.de.config_test.system.yml b/core/modules/config/tests/config_test/config/install/language/de/config_test.system.yml similarity index 100% rename from core/modules/config/tests/config_test/config/install/language.config.de.config_test.system.yml rename to core/modules/config/tests/config_test/config/install/language/de/config_test.system.yml diff --git a/core/modules/config/tests/config_test/config/install/language.config.en.config_test.system.yml b/core/modules/config/tests/config_test/config/install/language/en/config_test.system.yml similarity index 100% rename from core/modules/config/tests/config_test/config/install/language.config.en.config_test.system.yml rename to core/modules/config/tests/config_test/config/install/language/en/config_test.system.yml diff --git a/core/modules/config/tests/config_test/config/install/language.config.fr.config_test.system.yml b/core/modules/config/tests/config_test/config/install/language/fr/config_test.system.yml similarity index 100% rename from core/modules/config/tests/config_test/config/install/language.config.fr.config_test.system.yml rename to core/modules/config/tests/config_test/config/install/language/fr/config_test.system.yml diff --git a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php index fb3ba88..13311d9 100644 --- a/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php +++ b/core/modules/config_translation/lib/Drupal/config_translation/Form/ConfigTranslationFormBase.php @@ -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. diff --git a/core/modules/language/language.install b/core/modules/language/language.install new file mode 100644 index 0000000..7f529d5 --- /dev/null +++ b/core/modules/language/language.install @@ -0,0 +1,34 @@ +ensureTableExists(); + } + + // Get all the existing modules that are enabled and install any language + // configuration. + $language_config_override = \Drupal::service('language.config_factory_override'); + foreach (array_keys(\Drupal::moduleHandler()->getModuleList()) as $module) { + $language_config_override->install('module', $module); + } + /** @var \Drupal\Core\Extension\ThemeHandler $theme_handler */ + $theme_handler = \Drupal::service('theme_handler'); + foreach ($theme_handler->listInfo() as $theme) { + if ($theme->status) { + $language_config_override->install('theme', $theme->name); + } + } + \Drupal::configFactory()->reset(); +} diff --git a/core/modules/language/language.services.yml b/core/modules/language/language.services.yml index 7396d57..6aa5b2f 100644 --- a/core/modules/language/language.services.yml +++ b/core/modules/language/language.services.yml @@ -11,8 +11,18 @@ services: class: Drupal\language\EventSubscriber\ConfigSubscriber tags: - { name: event_subscriber } + language.cache_config: + class: Drupal\Core\Cache\CacheBackendInterface + tags: + - { name: cache.bin } + factory_method: get + factory_service: cache_factory + arguments: [language_config] + language.config_storage: + class: Drupal\language\Config\LanguageOverrideDatabaseStorage + arguments: ['@database', 'config_language'] language.config_factory_override: class: Drupal\language\Config\LanguageConfigFactoryOverride - arguments: ['@config.storage', '@event_dispatcher', '@config.typed'] + arguments: ['@language.config_storage', '@event_dispatcher', '@config.typed'] tags: - { name: config.factory.override, priority: -254 } diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php index bfadec0..8b2c81e 100644 --- a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php @@ -8,6 +8,9 @@ namespace Drupal\language\Config; use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\FileStorage; +use Drupal\Core\Config\StorageComparer; use Drupal\Core\Config\StorageInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Language\Language; @@ -22,9 +25,19 @@ class LanguageConfigFactoryOverride implements LanguageConfigFactoryOverrideInte /** * The configuration storage. * - * @var \Drupal\Core\Config\StorageInterface + * Do not access this directly. Should be accessed through self::getStorage() + * so that the cache of storages per langcode is used. + * + * @var \Drupal\language\Config\LanguageOverrideStorageInterface + */ + protected $baseStorage; + + /** + * An array of configuration storages keyed by langcode. + * + * @var \Drupal\language\Config\LanguageOverrideStorageInterface[] */ - protected $storage; + protected $storages; /** * The typed config manager. @@ -50,15 +63,15 @@ class LanguageConfigFactoryOverride implements LanguageConfigFactoryOverrideInte /** * Constructs the LanguageConfigFactoryOverride object. * - * @param \Drupal\Core\Config\StorageInterface $storage + * @param \Drupal\language\Config\LanguageOverrideStorageInterface $storage * The configuration storage engine. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * An event dispatcher instance to use for configuration events. * @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config * The typed configuration manager. */ - public function __construct(StorageInterface $storage, EventDispatcherInterface $event_dispatcher, TypedConfigManagerInterface $typed_config) { - $this->storage = $storage; + public function __construct(LanguageOverrideStorageInterface $storage, EventDispatcherInterface $event_dispatcher, TypedConfigManagerInterface $typed_config) { + $this->baseStorage = $storage; $this->eventDispatcher = $event_dispatcher; $this->typedConfigManager = $typed_config; } @@ -67,72 +80,40 @@ 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 = NULL) { + if (!isset($langcode)) { + $langcode = $this->language ? $this->language->id : NULL; } - return static::LANGUAGE_CONFIG_PREFIX . '.' . $langcode . '.' . $name; + if (!isset($this->storages[$langcode])) { + // Clone the language override storage so a process could compare language + // overrides if it wanted to. + $storage = clone $this->baseStorage; + $this->storages[$langcode] = $storage->setLangcode($langcode); + } + return $this->storages[$langcode]; } /** @@ -165,4 +146,141 @@ public function setLanguageFromDefault(LanguageDefault $language_default = NULL) return $this; } + /** + * {@inheritdoc} + */ + public function install($type, $name) { + // Work out if this extension provides default language overrides. + $config_dir = drupal_get_path($type, $name) . '/config/install/language'; + if (is_dir($config_dir)) { + // List all the directories. + // \DirectoryIterator on Windows requires an absolute path. + $it = new \DirectoryIterator(realpath($config_dir)); + foreach ($it as $dir) { + if (!$dir->isDot() && $dir->isDir() ) { + $default_language_config = new FileStorage($dir->getPathname()); + $storage = $this->getStorage($dir->getFilename()); + foreach ($default_language_config->listAll() as $config_name) { + $data = $default_language_config->read($config_name); + $config = new LanguageConfigOverride($config_name, $storage, $this->typedConfigManager); + $config->setData($data)->save(); + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function uninstall($type, $name) { + $this->baseStorage->deleteAllOverrides($name . '.'); + } + + /** + * {@inheritdoc} + */ + public function getStorages() { + $storages = array(); + foreach (\Drupal::languageManager()->getLanguages() as $language) { + $storages[] = $this->getStorage($language->getId()); + } + return $storages; + } + + /** + * {@inheritdoc} + */ + public static function configSyncStep(&$context, ConfigImporter $config_importer) { + // First time get create list of stuff to import. + if (!isset($context['sandbox']['language'])) { + static::initializeSandbox($context, $config_importer); + } + + if ($to_process = static::getNextOperation($context)) { + list($config_name, $langcode, $op, $comparer) = $to_process; + + if ($op == 'delete') { + $comparer->getTargetStorage()->delete($config_name); + } + else { + $comparer->getTargetStorage()->write($config_name, $comparer->getSourceStorage()->read($config_name)); + } + $context['sandbox']['language']['data'][$langcode]['processed'][$op][] = $config_name; + $context['sandbox']['language']['processed']++; + $context['finished'] = $context['sandbox']['language']['processed'] / $context['sandbox']['language']['total_to_process']; + $context['message'] = \Drupal::translation()->translate('Processing @name language configuration override for langcode @langcode.', array('@name' => $config_name, '@langcode' => $langcode)); + } + else { + $context['finished'] = 1; + } + + } + + /** + * Initializes the batch context with what to do. + * + * @param array $context + * The batch context. + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The config importer. + */ + protected static function initializeSandbox(&$context, $config_importer) { + $context['sandbox']['language']['total_to_process'] = 0; + $context['sandbox']['language']['processed'] = 0; + $context['sandbox']['language']['current_langcode'] = NULL; + $context['sandbox']['language']['data'] = array(); + // All languages are synced by now. + foreach (\Drupal::languageManager()->getLanguages() as $language) { + // Get the active store for the language + $active = \Drupal::languageManager()->getLanguageConfigOverrideStorage($language->getId()); + // So we are tying ourselves into files unlike the current ConfigSync + // which is just using config.storage.staging. If we don't want to do this + // we are going to have to expand the staging storage functionality to + // deal with overrides. Which might be a good idea anyway. + $stage = new FileStorage(config_get_config_directory(CONFIG_STAGING_DIRECTORY) . '/language/' . $language->getId()); + $comparer = new StorageComparer($stage, $active); + $comparer->createChangelist(); + $count = 0; + foreach (array('delete', 'create', 'rename', 'update') as $op) { + $count += count($comparer->getChangelist($op)); + } + if ($count > 0) { + $context['sandbox']['language']['data'][$language->getId()]['comparer'] = $comparer; + $context['sandbox']['language']['data'][$language->getId()]['processed'] = $comparer->getEmptyChangelist(); + $context['sandbox']['language']['data'][$language->getId()]['finished'] = FALSE; + $context['sandbox']['language']['total_to_process'] += $count; + } + } + } + + /** + * Gets the language code to process. + * + * @param $context + * The batch context. + * + * @return array + * An array containing data need to process the next config override. + */ + protected static function getNextOperation(&$context) { + foreach ($context['sandbox']['language']['data'] as $langcode => $data) { + if (!$data['finished']) { + $comparer = $context['sandbox']['language']['data'][$langcode]['comparer']; + foreach (array('delete', 'create', 'update') as $op) { + $todo = array_diff($comparer->getChangelist($op), $context['sandbox']['language']['data'][$langcode]['processed'][$op]); + if (!empty($todo)) { + $return[] = array_shift($todo); + $return[] = $langcode; + $return[] = $op; + $return[] = $comparer; + return $return; + } + } + $context['sandbox']['language']['data'][$langcode]['finished'] = TRUE; + } + } + return FALSE; + } + } diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php index 1c3de55..86510d4 100644 --- a/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php @@ -8,13 +8,14 @@ namespace Drupal\language\Config; use Drupal\Core\Config\ConfigFactoryOverrideInterface; +use Drupal\Core\Config\ConfigFactoryOverrideSyncInterface; use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageDefault; /** * Defines the interface for a configuration factory language override object. */ -interface LanguageConfigFactoryOverrideInterface extends ConfigFactoryOverrideInterface { +interface LanguageConfigFactoryOverrideInterface extends ConfigFactoryOverrideInterface, ConfigFactoryOverrideSyncInterface { /** * Prefix for all language configuration files. @@ -62,4 +63,15 @@ public function setLanguageFromDefault(LanguageDefault $language_default = NULL) */ public function getOverride($langcode, $name); + /** + * Returns the storage instance for a particular langcode. + * + * @param string $langcode + * Language code. + * + * @return \Drupal\language\Config\LanguageOverrideStorageInterface + * The language override storage object. + */ + public function getStorage($langcode); + } diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageConfigOverride.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigOverride.php new file mode 100644 index 0000000..6ed2b11 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigOverride.php @@ -0,0 +1,71 @@ +name = $name; + $this->storage = $storage; + $this->typedConfigManager = $typed_config; + } + + /** + * {@inheritdoc} + */ + public function save() { + // Validate the configuration object name before saving. + static::validateName($this->name); + + // If there is a schema for this configuration object, cast all values to + // conform to the schema. + if ($this->typedConfigManager->hasConfigSchema($this->name)) { + // Ensure that the schema wrapper has the latest data. + $this->schemaWrapper = NULL; + foreach ($this->data as $key => $value) { + $this->data[$key] = $this->castValue($key, $value); + } + } + + $this->storage->write($this->name, $this->data); + $this->isNew = FALSE; + $this->originalData = $this->data; + return $this; + } + + /** + * {@inheritdoc} + */ + 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; + $this->originalData = $this->data; + return $this; + } + +} diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageOverrideDatabaseStorage.php b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideDatabaseStorage.php new file mode 100644 index 0000000..f4f8853 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideDatabaseStorage.php @@ -0,0 +1,187 @@ +connection->queryRange('SELECT 1 FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name = :name AND langcode = :langcode', 0, 1, array( + ':name' => $name, + ':langcode' => $this->langcode, + ), $this->options)->fetchField(); + } + catch (\Exception $e) { + // If we attempt a read without actually having the database or the table + // available, just return FALSE so the caller can handle it. + return FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function readMultiple(array $names) { + $list = array(); + try { + $list = $this->connection->query('SELECT name, data FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name IN (:names) AND langcode = :langcode', array(':names' => $names, ':langcode' => $this->langcode), $this->options)->fetchAllKeyed(); + foreach ($list as &$data) { + $data = $this->decode($data); + } + } + catch (\Exception $e) { + // If we attempt a read without actually having the database or the table + // available, just return an empty array so the caller can handle it. + } + return $list; + } + + /** + * {@inheritdoc} + */ + protected function doWrite($name, $data) { + $options = array('return' => Database::RETURN_AFFECTED) + $this->options; + return (bool) $this->connection->merge($this->table, $options) + ->keys(array('name' => $name, 'langcode' => $this->langcode)) + ->fields(array('data' => $data)) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function ensureTableExists() { + return parent::ensureTableExists(); + } + + /** + * {@inheritdoc} + */ + protected static function schemaDefinition() { + $schema = parent::schemaDefinition(); + $schema['fields']['langcode'] = array( + 'description' => 'The {language}.langcode of this configuration data.', + 'type' => 'varchar', + 'length' => 12, + 'not null' => TRUE, + ); + $schema['primary key'] = array('name', 'langcode'); + return $schema; + } + + /** + * {@inheritdoc} + */ + public function delete($name) { + $options = array('return' => Database::RETURN_AFFECTED) + $this->options; + return (bool) $this->connection->delete($this->table, $options) + ->condition('name', $name) + ->condition('langcode', $this->langcode) + ->execute(); + } + + + /** + * {@inheritdoc} + */ + public function rename($name, $new_name) { + $options = array('return' => Database::RETURN_AFFECTED) + $this->options; + return (bool) $this->connection->update($this->table, $options) + ->fields(array('name' => $new_name)) + ->condition('name', $name) + ->execute(); + } + + /** + * {@inheritdoc} + */ + public function listAll($prefix = '') { + try { + return $this->connection->query('SELECT name FROM {' . $this->connection->escapeTable($this->table) . '} WHERE name LIKE :name AND langcode = :langcode', array( + ':name' => $this->connection->escapeLike($prefix) . '%', + ':langcode' => $this->langcode, + ), $this->options)->fetchCol(); + } + catch (\Exception $e) { + return array(); + } + } + + /** + * {@inheritdoc} + */ + public function deleteAll($prefix = '') { + try { + $options = array('return' => Database::RETURN_AFFECTED) + $this->options; + return (bool) $this->connection->delete($this->table, $options) + ->condition('name', $prefix . '%', 'LIKE') + ->condition('langcode', $this->langcode) + ->execute(); + } + catch (\Exception $e) { + return FALSE; + } + } + + + /** + * {@inheritdoc} + */ + public function deleteAllOverrides($prefix) { + try { + $options = array('return' => Database::RETURN_AFFECTED) + $this->options; + return (bool) $this->connection->delete($this->table, $options) + ->condition('name', $prefix . '%', 'LIKE') + ->execute(); + } + catch (\Exception $e) { + return FALSE; + } + } + + /** + * {@inheritdoc} + */ + public function setLangcode($langcode) { + // Mitigate side effects by preventing the storage's langcode changing once + // set. The configuration override objects returned from + // \Drupal\language\Config\LanguageConfigFactoryOverride::getOverride() + // have a storage object injected so allowing the langcode to be changed + // could create a mismatch between the language of the data and the storage. + if (isset($this->langcode) && $langcode !== $this->langcode) { + throw new LanguageOverrideStorageLockedException('Cannot switch language override storage langcode once it has been set.'); + } + $this->langcode = $langcode; + return $this; + } + + public function getExportDir() { + if (!isset($this->langcode)) { + throw new \Exception('Wha?'); + } + return 'language/' . $this->langcode; + } + +} diff --git a/core/modules/language/lib/Drupal/language/Config/LanguageOverrideStorageInterface.php b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideStorageInterface.php new file mode 100644 index 0000000..7360903 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideStorageInterface.php @@ -0,0 +1,46 @@ +configFactoryOverride->getStorage($langcode); + } + + /** + * {@inheritdoc} + */ public function getUnusedPredefinedList() { $languages = $this->getLanguages(); $predefined = $this->getStandardLanguageList(); diff --git a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php index 085a170..fe6923e4 100644 --- a/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php +++ b/core/modules/language/lib/Drupal/language/ConfigurableLanguageManagerInterface.php @@ -91,12 +91,24 @@ public function updateLockedLanguageWeights(); * @param string $name * The language configuration object name. * - * @return \Drupal\Core\Config\Config + * @return \Drupal\language\Config\LanguageConfigOverride * The language config override object. */ public function getLanguageConfigOverride($langcode, $name); /** + * Gets a language config override storage object. + * + * @param string $langcode + * The language code for the override. + * + * @param \Drupal\Core\Config\StorageInterface $storage + * A storage object to use for reading and writing the + * configuration override. + */ + public function getLanguageConfigOverrideStorage($langcode); + + /** * Prepare a language code list for unused predefined languages. * * @return array diff --git a/core/modules/language/lib/Drupal/language/Exception/LanguageOverrideStorageLockedException.php b/core/modules/language/lib/Drupal/language/Exception/LanguageOverrideStorageLockedException.php new file mode 100644 index 0000000..2dec626 --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Exception/LanguageOverrideStorageLockedException.php @@ -0,0 +1,15 @@ + 'Language config override synchronize', + 'description' => 'Ensures the language config overrides can be synchronized.', + 'group' => 'Language', + ); + } + + public function setUp() { + parent::setUp(); + $this->adminUser = $this->drupalCreateUser(array('translate configuration')); + $this->drupalLogin($this->adminUser); + } + + /** + * + */ + public function testConfigOverrideImport() { + language_save(new Language(array( + 'name' => 'French', + 'id' => 'fr', + ))); + /* @var \Drupal\Core\Config\StorageInterface $staging */ + $staging = \Drupal::service('config.storage.staging'); + $this->copyConfig(\Drupal::service('config.storage'), $staging); + + \Drupal::moduleHandler()->uninstall(array('language')); + // Ensure that the current site has no overrides registered to the + // ConfigFactory. + $this->rebuildContainer(); + + /* @var \Drupal\Core\Config\StorageInterface $override_staging */ + $override_staging = new FileStorage(config_get_config_directory(CONFIG_STAGING_DIRECTORY) . '/language/fr'); + // Create some overrides in staging. + $override_staging->write('system.site', array('name' => 'FR default site name')); + $override_staging->write('system.maintenance', array('message' => 'FR message: @site is currently under maintenance. We should be back shortly. Thank you for your patience')); + + $this->configImporter()->import(); + $this->rebuildContainer(); + + $override = \Drupal::languageManager()->getLanguageConfigOverride('fr', 'system.site'); + $this->assertEqual('FR default site name', $override->get('name')); + $this->drupalGet('fr'); + $this->assertText('FR default site name'); + + $this->drupalGet('admin/config/development/maintenance/translate/fr/edit'); + $this->assertText('FR message: @site is currently under maintenance. We should be back shortly. Thank you for your patience'); + } +} diff --git a/core/modules/language/tests/Drupal/language/Tests/Config/LanguageOverrideDatabaseStorageTest.php b/core/modules/language/tests/Drupal/language/Tests/Config/LanguageOverrideDatabaseStorageTest.php new file mode 100644 index 0000000..531e721 --- /dev/null +++ b/core/modules/language/tests/Drupal/language/Tests/Config/LanguageOverrideDatabaseStorageTest.php @@ -0,0 +1,77 @@ + 'Language override database storage test', + 'description' => 'Tests the language override database storage', + 'group' => 'Language', + ); + } + + /** + * Sets necessary mock objects for testing LanguageOverrideDatabaseStorage. + */ + public function setUp() { + $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * Tests that the langcode can be set on the storage. + * + * This set exists to provide ensure that the exception is not being thrown + * on the first call to + * \Drupal\language\Config\LanguageOverrideDatabaseStorage::setLangcode(). + * + * @covers ::setLangcode + */ + public function testSetLangcode() { + $storage = new LanguageOverrideDatabaseStorage($this->connection, 'language_config'); + $storage->setLangcode('en'); + $this->assertTrue(TRUE, 'The langcode can be set.'); + } + + /** + * Tests that the langcode can only be set once on the storage. + * + * @expectedException \Drupal\language\Exception\LanguageOverrideStorageLockedException + * + * @covers ::setLangcode + */ + public function testSetLangcodeException() { + $storage = new LanguageOverrideDatabaseStorage($this->connection, 'language_config'); + $storage->setLangcode('en'); + $storage->setLangcode('fr'); + } +} diff --git a/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php b/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php index 3c0dc57..f731b13 100644 --- a/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php +++ b/core/modules/locale/lib/Drupal/locale/LocaleConfigManager.php @@ -230,9 +230,9 @@ public function getStringNames(array $lids) { * Language code to delete. */ public function deleteLanguageTranslations($langcode) { - $locale_name = LanguageConfigFactoryOverrideInterface::LANGUAGE_CONFIG_PREFIX . '.' . $langcode . '.'; - foreach ($this->configStorage->listAll($locale_name) as $name) { - $this->configStorage->delete($name); + $storage = $this->languageManager->getLanguageConfigOverrideStorage($langcode); + foreach ($storage->listAll() as $name) { + $this->languageManager->getLanguageConfigOverride($langcode, $name)->delete(); } } diff --git a/core/modules/locale/tests/modules/locale_test/config/install/language.config.de.locale_test.translation.yml b/core/modules/locale/tests/modules/locale_test/config/install/language/de/locale_test.translation.yml similarity index 100% rename from core/modules/locale/tests/modules/locale_test/config/install/language.config.de.locale_test.translation.yml rename to core/modules/locale/tests/modules/locale_test/config/install/language/de/locale_test.translation.yml diff --git a/core/modules/system/tests/modules/menu_test/config/install/language.config.nl.menu_test.menu_item.yml b/core/modules/system/tests/modules/menu_test/config/install/language/nl/menu_test.menu_item.yml similarity index 100% rename from core/modules/system/tests/modules/menu_test/config/install/language.config.nl.menu_test.menu_item.yml rename to core/modules/system/tests/modules/menu_test/config/install/language/nl/menu_test.menu_item.yml diff --git a/core/modules/tour/tests/tour_test/config/install/language.config.it.tour.tour.tour-test.yml b/core/modules/tour/tests/tour_test/config/install/language/it/tour.tour.tour-test.yml similarity index 100% rename from core/modules/tour/tests/tour_test/config/install/language.config.it.tour.tour.tour-test.yml rename to core/modules/tour/tests/tour_test/config/install/language/it/tour.tour.tour-test.yml