Commit 4eaa775b authored by catch's avatar catch

Issue #1890784 by alexpott, YesCT, heyrocker, tim.plunkett, EllaTheHarpy,...

Issue #1890784 by alexpott, YesCT, heyrocker, tim.plunkett, EllaTheHarpy, beejeebus: Refactor configuration import and sync functions.
parent 3a8e8c60
......@@ -356,6 +356,15 @@ services:
class: Drupal\Core\EventSubscriber\ConfigGlobalOverrideSubscriber
tags:
- { name: event_subscriber }
config_import_subscriber:
class: Drupal\Core\EventSubscriber\ConfigImportSubscriber
tags:
- { name: event_subscriber }
config_snapshot_subscriber:
class: Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
tags:
- { name: event_subscriber }
arguments: ['@config.storage', '@config.storage.snapshot']
language_request_subscriber:
class: Drupal\Core\EventSubscriber\LanguageRequestSubscriber
tags:
......
......@@ -2,8 +2,10 @@
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigException;
use Drupal\Core\Config\ConfigInstaller;
use Drupal\Core\Config\FileStorage;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Config\StorageComparer;
use Symfony\Component\Yaml\Dumper;
/**
......@@ -11,11 +13,6 @@
* This is the API for configuration storage.
*/
/**
* Config import lock name used to prevent concurrent synchronizations.
*/
const CONFIG_IMPORT_LOCK = 'config_import';
/**
* Installs the default configuration of a given extension.
*
......@@ -25,10 +22,6 @@
* 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) {
......@@ -38,22 +31,19 @@ function config_install_default_config($type, $name) {
$config_dir = drupal_get_path($type, $name) . '/config';
if (is_dir($config_dir)) {
$source_storage = new FileStorage($config_dir);
$target_storage = drupal_container()->get('config.storage');
// Ignore manifest files.
$config_changes = config_sync_get_changes($source_storage, $target_storage, FALSE);
if (empty($config_changes['create'])) {
return;
}
// Do not overwrite or delete pre-existing configuration.
$config_changes['change'] = array();
$config_changes['delete'] = array();
$remaining_changes = config_import_invoke_owner($config_changes, $source_storage, $target_storage);
config_sync_changes($remaining_changes, $source_storage, $target_storage);
$storage_comparer = new StorageComparer($source_storage, Drupal::service('config.storage'));
// Only import new config. Changed config is from previous enables and
// should not be overwritten.
$storage_comparer->addChangelistCreate();
$installer = new ConfigInstaller(
$storage_comparer,
Drupal::service('event_dispatcher'),
Drupal::service('config.factory'),
Drupal::entityManager(),
Drupal::lock()
);
$installer->import();
}
// Exit the override free context.
config_context_leave();
}
/**
......@@ -154,227 +144,6 @@ function config_context_leave() {
->leaveContext();
}
/**
* Returns a list of differences between configuration storages.
*
* @param Drupal\Core\Config\StorageInterface $source_storage
* The storage to synchronize configuration from.
* @param Drupal\Core\Config\StorageInterface $target_storage
* The storage to synchronize configuration to.
* @param bool $use_manifest
* (optional) Whether to determine changes based on manifest files. Defaults
* to TRUE.
*
* @return array|bool
* An associative array containing the differences between source and target
* storage, or FALSE if there are no differences.
*/
function config_sync_get_changes(StorageInterface $source_storage, StorageInterface $target_storage, $use_manifest = TRUE) {
$config_changes = array(
'create' => array(),
'change' => array(),
'delete' => array(),
);
$all_source_names = $source_storage->listAll();
$all_target_names = $target_storage->listAll();
// Config entities maintain 'manifest' files that list the objects they
// are currently handling. Each file is a simple indexed array of config
// object names. In order to generate a list of objects that have been
// created or deleted we need to open these files in both the source and
// target storage, generate an array of the objects, and compare them.
if ($use_manifest) {
$source_config_data = array();
$target_config_data = array();
foreach ($source_storage->listAll('manifest') as $name) {
if ($source_manifest_data = $source_storage->read($name)) {
$source_config_data = array_merge($source_config_data, $source_manifest_data);
}
if ($target_manifest_data = $target_storage->read($name)) {
$target_config_data = array_merge($target_config_data, $target_manifest_data);
}
}
foreach (array_diff_key($target_config_data, $source_config_data) as $name => $value) {
$config_changes['delete'][] = $value['name'];
}
foreach (array_diff_key($source_config_data, $target_config_data) as $name => $value) {
$config_changes['create'][] = $value['name'];
}
}
else {
$config_changes['delete'] = array_diff($all_target_names, $all_source_names);
$config_changes['create'] = array_diff($all_source_names, $all_target_names);
}
foreach (array_intersect($all_source_names, $all_target_names) as $name) {
// Ignore manifest files
if (substr($name, 0, 9) != 'manifest.') {
$source_config_data = $source_storage->read($name);
$target_config_data = $target_storage->read($name);
if ($source_config_data !== $target_config_data) {
$config_changes['change'][] = $name;
}
}
}
// Do not trigger subsequent synchronization operations if there are no
// changes in any category.
if (empty($config_changes['create']) && empty($config_changes['change']) && empty($config_changes['delete'])) {
return FALSE;
}
return $config_changes;
}
/**
* Writes an array of config file changes from a source storage to a target storage.
*
* @param array $config_changes
* An array of changes to be written.
* @param Drupal\Core\Config\StorageInterface $source_storage
* The storage to synchronize configuration from.
* @param Drupal\Core\Config\StorageInterface $target_storage
* 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, $target_context);
if ($op == 'delete') {
$config->delete();
}
else {
$data = $source_storage->read($name);
$config->setData($data ? $data : array());
$config->save();
}
$factory->reset($name);
}
}
}
/**
* Imports configuration into the active configuration.
*
* @return bool|null
* TRUE if configuration was imported successfully, FALSE in case of a
* synchronization error, or NULL if there are no changes to synchronize.
*/
function config_import() {
// Retrieve a list of differences between staging and the active configuration.
$source_storage = drupal_container()->get('config.storage.staging');
$snapshot_storage = drupal_container()->get('config.storage.snapshot');
$target_storage = drupal_container()->get('config.storage');
$config_changes = config_sync_get_changes($source_storage, $target_storage);
if (empty($config_changes)) {
return;
}
if (!lock()->acquire(CONFIG_IMPORT_LOCK)) {
// Another request is synchronizing configuration.
// Return a negative result for UI purposes. We do not differentiate between
// an actual synchronization error and a failed lock, because concurrent
// synchronizations are an edge-case happening only when multiple developers
// or site builders attempt to do it without coordinating.
return FALSE;
}
$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);
$success = FALSE;
}
lock()->release(CONFIG_IMPORT_LOCK);
return $success;
}
/**
* Creates a configuration snapshot following a successful import.
*
* @param Drupal\Core\Config\StorageInterface $source_storage
* The storage to synchronize configuration from.
* @param Drupal\Core\Config\StorageInterface $target_storage
* The storage to synchronize configuration to.
*/
function config_import_create_snapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
$snapshot_storage->deleteAll();
foreach ($source_storage->listAll() as $name) {
$snapshot_storage->write($name, $source_storage->read($name));
}
}
/**
* Invokes MODULE_config_import() callbacks for configuration changes.
*
* @param array $config_changes
* An array of changes to be loaded.
* @param Drupal\Core\Config\StorageInterface $source_storage
* The storage to synchronize configuration from.
* @param Drupal\Core\Config\StorageInterface $target_storage
* The storage to synchronize configuration to.
*
* @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
// handle dependencies correctly.
$manager = Drupal::entityManager();
foreach (array('delete', 'create', 'change') as $op) {
foreach ($config_changes[$op] as $key => $name) {
// Call to the configuration entity's storage controller to handle the
// configuration change.
$handled_by_module = FALSE;
// 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, $free_context);
$old_config->load();
$data = $source_storage->read($name);
$new_config = new Config($name, $source_storage, $free_context);
if ($data !== FALSE) {
$new_config->setData($data);
}
$method = 'import' . ucfirst($op);
$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::entityManager()->getDefinition($entity_type);
$factory->reset('manifest.' . $entity_info['config_prefix']);
unset($config_changes[$op][$key]);
}
}
}
return $config_changes;
}
/**
* Return a list of all config entity types provided by a module.
*
......@@ -424,6 +193,21 @@ function config_typed() {
return drupal_container()->get('config.typed');
}
/**
* Creates a configuration snapshot following a successful import.
*
* @param Drupal\Core\Config\StorageInterface $source_storage
* The storage to synchronize configuration from.
* @param Drupal\Core\Config\StorageInterface $snapshot_storage
* The storage to synchronize configuration to.
*/
function config_import_create_snapshot(StorageInterface $source_storage, StorageInterface $snapshot_storage) {
$snapshot_storage->deleteAll();
foreach ($source_storage->listAll() as $name) {
$snapshot_storage->write($name, $source_storage->read($name));
}
}
/**
* Return a formatted diff of a named config between two storages.
*
......
......@@ -96,15 +96,13 @@ public function get($name) {
*/
public function reset($name = NULL) {
if ($name) {
// Reinitialize the configuration object in all contexts.
// Clear the cached configuration object in all contexts.
foreach ($this->getCacheKeys($name) as $cache_key) {
$this->cache[$cache_key]->init();
unset($this->cache[$cache_key]);
}
}
else {
foreach ($this->cache as $config) {
$config->init();
}
$this->cache = array();
}
return $this;
}
......
<?php
/**
* @file
* Contains \Drupal\Core\Config\ConfigImporter.
*/
namespace Drupal\Core\Config;
use Drupal\Core\Config\Context\FreeConfigContext;
use Drupal\Core\Entity\EntityManager;
use Drupal\Core\Lock\LockBackendInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Defines a configuration importer.
*
* A config importer imports the changes into the configuration system. To
* determine which changes to import a StorageComparer in used.
*
* @see \Drupal\Core\Config\StorageComparerInterface
*
* The ConfigImporter has a identifier which is used to construct event names.
* The events fired during an import are:
* - 'config.importer.validate': Events listening can throw a
* \Drupal\Core\Config\ConfigImporterException to prevent an import from
* occurring.
* @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber
* - 'config.importer.import': Events listening can react to a successful import.
* @see \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber
*
* @see \Drupal\Core\Config\ConfigImporterEvent
*/
class ConfigImporter {
/**
* The name used to identify events and the lock.
*/
const ID = 'config.importer';
/**
* The storage comparer used to discover configuration changes.
*
* @var \Drupal\Core\Config\StorageComparerInterface
*/
protected $storageComparer;
/**
* The event dispatcher used to notify subscribers.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcher
*/
protected $eventDispatcher;
/**
* The configuration context.
*
* @var \Drupal\Core\Config\Context\ContextInterface
*/
protected $context;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactory
*/
protected $configFactory;
/**
* The plugin manager for entities.
*
* @var \Drupal\Core\Entity\EntityManager
*/
protected $entityManager;
/**
* The used lock backend instance.
*
* @var \Drupal\Core\Lock\LockBackendInterface
*/
protected $lock;
/**
* List of changes processed by the import().
*
* @var array
*/
protected $processed;
/**
* Indicates changes to import have been validated.
*
* @var bool
*/
protected $validated;
/**
* Constructs a configuration import object.
*
* @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer
* A storage comparer object used to determin configuration changes and
* access the source and target storage objects.
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher used to notify subscribers of config import events.
* @param \Drupal\Core\Config\ConfigFactory $config_factory
* The config factory that statically caches config objects.
* @param \Drupal\Core\Entity\EntityManager $entity_manager
* The entity manager used to import config entities.
* @param \Drupal\Core\Lock\LockBackendInterface
* The lock backend to ensure multiple imports do not occur at the same time.
*/
public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigFactory $config_factory, EntityManager $entity_manager, LockBackendInterface $lock) {
$this->storageComparer = $storage_comparer;
$this->eventDispatcher = $event_dispatcher;
$this->configFactory = $config_factory;
$this->entityManager = $entity_manager;
$this->lock = $lock;
$this->processed = $this->storageComparer->getEmptyChangelist();
// Use an override free context for importing so that overrides to do not
// pollute the imported data. The context is hard coded to ensure this is
// the case.
$this->context = new FreeConfigContext($this->eventDispatcher);
}
/**
* Gets the configuration storage comparer.
*
* @return \Drupal\Core\Config\StorageComparerInterface
* Storage comparer object used to calculate configuration changes.
*/
public function getStorageComparer() {
return $this->storageComparer;
}
/**
* Resets the storage comparer and processed list.
*
* @return \Drupal\Core\Config\ConfigImporter
* The ConfigImporter instance.
*/
public function reset() {
$this->storageComparer->reset();
$this->processed = $this->storageComparer->getEmptyChangelist();
$this->validated = FALSE;
return $this;
}
/**
* Checks if there are any unprocessed changes.
*
* @param array $ops
* The operations to check for changes. Defaults to all operations, i.e.
* array('delete', 'create', 'update').
*
* @return bool
* TRUE if there are changes to process and FALSE if not.
*/
public function hasUnprocessedChanges($ops = array('delete', 'create', 'update')) {
foreach ($ops as $op) {
if (count($this->getUnprocessed($op))) {
return TRUE;
}
}
return FALSE;
}
/**
* Gets list of processed changes.
*
* @return array
* An array containing a list of processed changes.
*/
public function getProcessed() {
return $this->processed;
}
/**
* Sets a change as processed.
*
* @param string $op
* The change operation performed, either delete, create or update.
* @param string $name
* The name of the configuration processed.
*/
protected function setProcessed($op, $name) {
$this->processed[$op][] = $name;
}
/**
* Gets a list of unprocessed changes for a given operation.
*
* @param string $op
* The change operation to get the unprocessed list for, either delete,
* create or update.
*
* @return array
* An array of configuration names.
*/
public function getUnprocessed($op) {
return array_diff($this->storageComparer->getChangelist($op), $this->processed[$op]);
}
/**
* Imports the changelist to the target storage.
*
* @throws \Drupal\Core\Config\ConfigException
*
* @return \Drupal\Core\Config\ConfigImporter
* The ConfigImporter instance.
*/
public function import() {
if ($this->hasUnprocessedChanges()) {
// Ensure that the changes have been validated.
$this->validate();
$this->configFactory->enterContext($this->context);
if (!$this->lock->acquire(static::ID)) {
// Another process is synchronizing configuration.
throw new ConfigImporterException(sprintf('%s is already importing', static::ID));
}
$this->importInvokeOwner();
$this->importConfig();
// Allow modules to react to a import.
$this->notify('import');
// The import is now complete.
$this->lock->release(static::ID);
$this->reset();
// Leave the context used during import and clear the ConfigFactory's
// static cache.
$this->configFactory->leaveContext()->reset();
}
return $this;
}
/**
* Dispatches validate event for a ConfigImporter object.
*
* Events should throw a \Drupal\Core\Config\ConfigImporterException to
* prevent an import from occurring.
*/
public function validate() {
if (!$this->validated) {
$this->notify('validate');
$this->validated = TRUE;
}
return $this;
}
/**
* Writes an array of config changes from the source to the target storage.
*/
protected function importConfig() {
foreach (array('delete', 'create', 'update') as $op) {
foreach ($this->getUnprocessed($op) as $name) {
$config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context);
if ($op == 'delete') {