diff -u b/core/lib/Drupal/Core/Config/DatabaseStorage.php b/core/lib/Drupal/Core/Config/DatabaseStorage.php --- b/core/lib/Drupal/Core/Config/DatabaseStorage.php +++ b/core/lib/Drupal/Core/Config/DatabaseStorage.php @@ -77,8 +77,8 @@ if (isset($data[$name])) { return $data[$name]; } - // @todo change to return to be consistent with other parts of Drupal, for - // example, loading an entity returns NULL if it does not exist. + // @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 -u b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php --- b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php +++ b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverride.php @@ -8,7 +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; @@ -177,2 +179,108 @@ + /** + * {@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 -u b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php b/core/modules/language/lib/Drupal/language/Config/LanguageConfigFactoryOverrideInterface.php --- b/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. diff -u b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideDatabaseStorage.php b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideDatabaseStorage.php --- b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideDatabaseStorage.php +++ b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideDatabaseStorage.php @@ -165,12 +165,11 @@ * {@inheritdoc} */ public function setLangcode($langcode) { - // Prevent side effects from switching changing a storage's langcode. The - // configuration override objects returned from + // 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 - // once set could create a mismatch between the language of the data and the - // storage. + // 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.'); } @@ -180,2 +179,9 @@ + public function getExportDir() { + if (!isset($this->langcode)) { + throw new \Exception('Wha?'); + } + return 'language/' . $this->langcode; + } + } diff -u b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideStorageInterface.php b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideStorageInterface.php --- b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideStorageInterface.php +++ b/core/modules/language/lib/Drupal/language/Config/LanguageOverrideStorageInterface.php @@ -14,7 +14,9 @@ /** * Sets the langcode to determine the override configuration directory to use. * - * Implementations + * Implementations should prevent the langcode from changing once set and + * throw a \Drupal\language\Exception\LanguageOverrideStorageLockedException + * if this occurs. * * @param string $langcode * The language langcode to get the directory for. only in patch2: unchanged: --- /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'); only in patch2: unchanged: --- 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'); } only in patch2: unchanged: --- 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; only in patch2: unchanged: --- /dev/null +++ b/core/modules/language/lib/Drupal/language/Tests/LanguageConfigOverrideImportTest.php @@ -0,0 +1,71 @@ + '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'); + } +}