diff --git a/core/core.services.yml b/core/core.services.yml index 42b56145d3a33d99956b581123508d8b16eb5983..9a361399bc41e42d1bad8f74cf0d0b23797d2d85 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -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: diff --git a/core/includes/config.inc b/core/includes/config.inc index f4148456627e421814ff111f4f014c03aa43f7e7..31f6e5b07a0ef97667c6fb0ad504546be3b68d52 100644 --- a/core/includes/config.inc +++ b/core/includes/config.inc @@ -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. * diff --git a/core/lib/Drupal/Core/Config/ConfigFactory.php b/core/lib/Drupal/Core/Config/ConfigFactory.php index 0c3785cd3ee4a6b0ba0b871c44272f59dfabfe7e..dd5df1e6ca0d0cd5cd36f75f26bdec59c158cb95 100644 --- a/core/lib/Drupal/Core/Config/ConfigFactory.php +++ b/core/lib/Drupal/Core/Config/ConfigFactory.php @@ -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; } diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php new file mode 100644 index 0000000000000000000000000000000000000000..d496fe78fd487526cf2d2a71f9fb6a93d4d65031 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -0,0 +1,338 @@ +<?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') { + $config->delete(); + } + else { + $data = $this->storageComparer->getSourceStorage()->read($name); + $config->setData($data ? $data : array()); + $config->save(); + } + $this->setProcessed($op, $name); + } + } + } + + /** + * Invokes import* methods on configuration entity storage controllers. + * + * Allow modules to take over configuration change operations for higher-level + * configuration data. + * + * @todo Add support for other extension types; e.g., themes etc. + */ + protected function importInvokeOwner() { + // First pass deleted, then new, and lastly changed configuration, in order + // to handle dependencies correctly. + foreach (array('delete', 'create', 'update') as $op) { + foreach ($this->getUnprocessed($op) as $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, $this->storageComparer->getTargetStorage(), $this->context); + $old_config->load(); + + $data = $this->storageComparer->getSourceStorage()->read($name); + $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->context); + if ($data !== FALSE) { + $new_config->setData($data); + } + + $method = 'import' . ucfirst($op); + $handled_by_module = $this->entityManager->getStorageController($entity_type)->$method($name, $new_config, $old_config); + } + if (!empty($handled_by_module)) { + $this->setProcessed($op, $name); + } + } + } + } + + /** + * Dispatches a config importer event. + * + * @param string $event_name + * The name of the config importer event to dispatch. + */ + protected function notify($event_name) { + $this->eventDispatcher->dispatch(static::ID . '.' . $event_name, new ConfigImporterEvent($this)); + } + + /** + * Determines if a import is already running. + * + * @return bool + * TRUE if an import is already running, FALSE if not. + */ + public function alreadyImporting() { + return !$this->lock->lockMayBeAvailable(static::ID); + } + + /** + * Returns the identifier for events and locks. + * + * @return string + * The identifier for events and locks. + */ + public function getId() { + return static::ID; + } + +} diff --git a/core/lib/Drupal/Core/Config/ConfigImporterEvent.php b/core/lib/Drupal/Core/Config/ConfigImporterEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..caef42e930f16150fcf1bd3e7532fe5c1ac07e70 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigImporterEvent.php @@ -0,0 +1,40 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Config\ConfigImporterEvent. + */ + +namespace Drupal\Core\Config; + +use Symfony\Component\EventDispatcher\Event; + +class ConfigImporterEvent extends Event { + /** + * Configuration import object. + * + * @var \Drupal\Core\Config\ConfigImporter + */ + protected $configImporter; + + /** + * Constructs ConfigImporterEvent. + * + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * A config import object to notify listeners about. + */ + public function __construct(ConfigImporter $config_importer) { + $this->configImporter = $config_importer; + } + + /** + * Gets the config import object. + * + * @return \Drupal\Core\Config\ConfigImporter + * The ConfigImporter object. + */ + public function getConfigImporter() { + return $this->configImporter; + } + +} diff --git a/core/lib/Drupal/Core/Config/ConfigImporterException.php b/core/lib/Drupal/Core/Config/ConfigImporterException.php new file mode 100644 index 0000000000000000000000000000000000000000..fd18c4d10163b4ec51d28fc3826eaf4bb898d572 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigImporterException.php @@ -0,0 +1,13 @@ +<?php + +/** +* @file +* Contains \Drupal\Core\Config\ConfigImporterException. +*/ + +namespace Drupal\Core\Config; + +/** +* Exception thrown when a config import fails. +*/ +class ConfigImporterException extends ConfigException {} diff --git a/core/lib/Drupal/Core/Config/ConfigInstaller.php b/core/lib/Drupal/Core/Config/ConfigInstaller.php new file mode 100644 index 0000000000000000000000000000000000000000..11622b2dc5774615d99f4eac042b86143e51ccad --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigInstaller.php @@ -0,0 +1,33 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Config\ConfigInstaller. + */ + +namespace Drupal\Core\Config; + +/** + * Defines a configuration installer. + * + * A config installer imports the changes into the configuration system during + * module installs. + * + * The ConfigInstaller has a identifier which is used to construct event names. + * The events fired during an import are: + * - 'config.installer.validate': Events listening can throw a + * \Drupal\Core\Config\ConfigImporterException to prevent an import from + * occurring. + * @see \Drupal\Core\EventSubscriber\ConfigImportSubscriber + * - 'config.installer.import': Events listening can react to a successful import. + * + * @see \Drupal\Core\Config\ConfigImporter + */ +class ConfigInstaller extends ConfigImporter { + + /** + * The name used to identify events and the lock. + */ + const ID = 'config.installer'; + +} diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php index 5487e04d0a62c54437a057bbe11b98773feab40d..fbf4fdcb08dc33e0f1fdf249e5252ecb5147ca5c 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigStorageController.php @@ -521,7 +521,7 @@ public function importCreate($name, Config $new_config, Config $old_config) { } /** - * Update configuration upon synchronizing configuration changes. + * Updates configuration upon synchronizing configuration changes. * * This callback is invoked when configuration is synchronized between storages * and allows a module to take over the synchronization of configuration data. @@ -533,7 +533,7 @@ public function importCreate($name, Config $new_config, Config $old_config) { * @param \Drupal\Core\Config\Config $old_config * A configuration object containing the old configuration data. */ - public function importChange($name, Config $new_config, Config $old_config) { + public function importUpdate($name, Config $new_config, Config $old_config) { $id = static::getIDFromConfigName($name, $this->entityInfo['config_prefix']); $entities = $this->load(array($id)); $entity = $entities[$id]; diff --git a/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php new file mode 100644 index 0000000000000000000000000000000000000000..c7fa957bccb52ada8052e68e69e77e95880b55a4 --- /dev/null +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -0,0 +1,201 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Config\StorageComparer. + */ + +namespace Drupal\Core\Config; + +/** + * Defines a config storage comparer. + */ +class StorageComparer implements StorageComparerInterface { + + /** + * The source storage used to discover configuration changes. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $sourceStorage; + + /** + * The target storage used to write configuration changes. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $targetStorage; + + /** + * List of changes to between the source storage and the target storage. + * + * @var array + */ + protected $changelist; + + /** + * Lists all the configuration object names in the source storage. + * + * @see \Drupal\Core\Config\StorageComparer::getSourceNames() + * + * @var array + */ + protected $sourceNames = array(); + + /** + * Lists all the configuration object names in the target storage. + * + * @see \Drupal\Core\Config\StorageComparer::getTargetNames() + * + * @var array + */ + protected $targetNames = array(); + + /** + * Constructs the Configuration storage comparer. + * + * @param \Drupal\Core\Config\StorageInterface $source_storage + * Storage controller object used to read configuration. + * @param \Drupal\Core\Config\StorageInterface $target_storage + * Storage controller object used to write configuration. + */ + public function __construct(StorageInterface $source_storage, StorageInterface $target_storage) { + $this->sourceStorage = $source_storage; + $this->targetStorage = $target_storage; + $this->changelist = $this->getEmptyChangelist(); + } + + /** + * {@inheritdoc} + */ + public function getSourceStorage() { + return $this->sourceStorage; + } + + /** + * {@inheritdoc} + */ + public function getTargetStorage() { + return $this->targetStorage; + } + + /** + * {@inheritdoc} + */ + public function getEmptyChangelist() { + return array( + 'create' => array(), + 'update' => array(), + 'delete' => array(), + ); + } + + /** + * {@inheritdoc} + */ + public function getChangelist($op = NULL) { + if ($op) { + return $this->changelist[$op]; + } + return $this->changelist; + } + + /** + * {@inheritdoc} + */ + public function addChangeList($op, array $changes) { + // Only add changes that aren't already listed. + $changes = array_diff($changes, $this->changelist[$op]); + $this->changelist[$op] = array_merge($this->changelist[$op], $changes); + return $this; + } + + /** + * {@inheritdoc} + */ + public function createChangelist() { + return $this + ->addChangelistCreate() + ->addChangelistUpdate() + ->addChangelistDelete(); + } + + /** + * {@inheritdoc} + */ + public function addChangelistDelete() { + return $this->addChangeList('delete', array_diff($this->getTargetNames(), $this->getSourceNames())); + } + + /** + * {@inheritdoc} + */ + public function addChangelistCreate() { + return $this->addChangeList('create', array_diff($this->getSourceNames(), $this->getTargetNames())); + } + + /** + * {@inheritdoc} + */ + public function addChangelistUpdate() { + foreach (array_intersect($this->getSourceNames(), $this->getTargetNames()) as $name) { + // Ignore manifest files. + if (substr($name, 0, 9) != 'manifest.') { + $source_config_data = $this->sourceStorage->read($name); + $target_config_data = $this->targetStorage->read($name); + if ($source_config_data !== $target_config_data) { + $this->addChangeList('update', array($name)); + } + } + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->changelist = $this->getEmptyChangelist(); + $this->sourceNames = $this->targetNames = array(); + return $this->createChangelist(); + } + + /** + * {@inheritdoc} + */ + public function hasChanges($ops = array('delete', 'create', 'update')) { + foreach ($ops as $op) { + if (!empty($this->changelist[$op])) { + return TRUE; + } + } + return FALSE; + } + + /** + * Gets all the configuration names in the source storage. + * + * @return array + * List of all the configuration names in the source storage. + */ + protected function getSourceNames() { + if (empty($this->sourceNames)) { + $this->sourceNames = $this->sourceStorage->listAll(); + } + return $this->sourceNames; + } + + /** + * Gets all the configuration names in the target storage. + * + * @return array + * List of all the configuration names in the target storage. + */ + protected function getTargetNames() { + if (empty($this->targetNames)) { + $this->targetNames = $this->targetStorage->listAll(); + } + return $this->targetNames; + } + +} diff --git a/core/lib/Drupal/Core/Config/StorageComparerInterface.php b/core/lib/Drupal/Core/Config/StorageComparerInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..5ca0f3e043705aa27090deac2df4edb65626961f --- /dev/null +++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php @@ -0,0 +1,120 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Config\StorageComparerInterface. + */ + +namespace Drupal\Core\Config; + +/** + * Defines an interface for comparison of configuration storage objects. + */ +interface StorageComparerInterface { + + /** + * Gets the configuration source storage. + * + * @return \Drupal\Core\Config\StorageInterface + * Storage controller object used to read configuration. + */ + public function getSourceStorage(); + + /** + * Gets the configuration target storage. + * + * @return \Drupal\Core\Config\StorageInterface + * Storage controller object used to write configuration. + */ + public function getTargetStorage(); + + /** + * Gets an empty changelist. + * + * @return array + * An empty changelist array. + */ + public function getEmptyChangelist(); + + /** + * Gets the list of differences to import. + * + * @param string $op + * (optional) A change operation. Either delete, create or update. If + * supplied the returned list will be limited to this operation. + * + * @return array + * An array of config changes that are yet to be imported. + */ + public function getChangelist($op = NULL); + + /** + * Adds changes to the changelist. + * + * @param string $op + * The change operation performed. Either delete, create or update. + * @param array $changes + * Array of changes to add the changelist. + * + * @return \Drupal\Core\Config\StorageComparerInterface + * An object which implements the StorageComparerInterface. + */ + public function addChangeList($op, array $changes); + + /** + * Add differences between source and target configuration storage to changelist. + * + * @return \Drupal\Core\Config\StorageComparerInterface + * An object which implements the StorageComparerInterface. + */ + public function createChangelist(); + + /** + * Creates the delete changelist. + * + * @return \Drupal\Core\Config\StorageComparerInterface + * An object which implements the StorageComparerInterface. + */ + public function addChangelistDelete(); + + /** + * Creates the create changelist. + * + * @return \Drupal\Core\Config\StorageComparerInterface + * An object which implements the StorageComparerInterface. + */ + public function addChangelistCreate(); + + /** + * Creates the update changelist. + * + * @return \Drupal\Core\Config\StorageComparerInterface + * An object which implements the StorageComparerInterface. + */ + public function addChangelistUpdate(); + + /** + * Recalculates the differences. + * + * @return \Drupal\Core\Config\StorageComparerInterface + * An object which implements the StorageComparerInterface. + */ + public function reset(); + + /** + * Checks if there are any operations with changes to process. + * + * Until the changelist has been calculated this will always be FALSE. + * + * @see \Drupal\Core\Config\StorageComparerInterface::createChangelist(). + * + * @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 hasChanges($ops = array('delete', 'create', 'update')); + +} diff --git a/core/lib/Drupal/Core/Config/StorageComparerManifest.php b/core/lib/Drupal/Core/Config/StorageComparerManifest.php new file mode 100644 index 0000000000000000000000000000000000000000..2f8836d200b787c970ba711f81a0f8e120c09fca --- /dev/null +++ b/core/lib/Drupal/Core/Config/StorageComparerManifest.php @@ -0,0 +1,105 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Config\StorageComparerManifest. + */ + +namespace Drupal\Core\Config; + +/** + * Defines a config storage comparer that uses config entity manifests. + * + * 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. + */ +class StorageComparerManifest extends StorageComparer { + + /** + * List of config entities managed by manifests in the source storage. + * + * @see \Drupal\Core\Config\StorageComparerManifest::getSourceManifestData() + * + * @var array + */ + protected $sourceManifestData = array(); + + /** + * List of config entities managed by manifests in the target storage. + * + * @see \Drupal\Core\Config\StorageComparerManifest::getTargetManifestData() + * + * @var array + */ + protected $targetManifestData = array(); + + /** + * {@inheritdoc} + */ + public function addChangelistDelete() { + foreach (array_diff_key($this->getTargetManifestData(), $this->getSourceManifestData()) as $value) { + $this->addChangeList('delete', array($value['name'])); + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function addChangelistCreate() { + foreach (array_diff_key($this->getSourceManifestData(), $this->getTargetManifestData()) as $value) { + $this->addChangeList('create', array($value['name'])); + } + return $this; + } + + /** + * Gets the list of config entities from the source storage's manifest files. + * + * @return array + * The list of config entities in the source storage whose entity type has a + * manifest in the source storage. + */ + protected function getSourceManifestData() { + if (empty($this->sourceManifestData)) { + foreach ($this->getSourceStorage()->listAll('manifest') as $name) { + if ($source_manifest_data = $this->getSourceStorage()->read($name)) { + $this->sourceManifestData = array_merge($this->sourceManifestData, $source_manifest_data); + } + } + } + return $this->sourceManifestData; + } + + /** + * Gets the list of config entities from the target storage's manifest files. + * + * @see \Drupal\Core\Config\ConfigImporter::getSourceManifestData() + * + * @return array + * The list of config entities in the target storage whose entity type has a + * manifest in the source storage. + */ + protected function getTargetManifestData() { + if (empty($this->targetManifestData)) { + foreach ($this->getSourceStorage()->listAll('manifest') as $name) { + if ($target_manifest_data = $this->targetStorage->read($name)) { + $this->targetManifestData = array_merge($this->targetManifestData, $target_manifest_data); + } + } + } + return $this->targetManifestData; + } + + /** + * {@inheritdoc} + */ + public function reset() { + $this->sourceManifestData = $this->targetManifestData = array(); + return parent::reset(); + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..ba54de3e29276c822b8fda618e5940be84a6a2b1 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigImportSubscriber.php @@ -0,0 +1,48 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\EventSubscriber\ConfigImportSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigImporterEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + + +/** + * Config import subscriber for config import events. + */ +class ConfigImportSubscriber implements EventSubscriberInterface { + + /** + * Validates the configuration to be imported. + * + * @param \Drupal\Core\Config\ConfigImporterEvent $event + * The Event to process. + * + * @throws \Drupal\Core\Config\ConfigNameException + */ + public function onConfigImporterValidate(ConfigImporterEvent $event) { + foreach (array('delete', 'create', 'update') as $op) { + foreach ($event->getConfigImporter()->getUnprocessed($op) as $name) { + Config::validateName($name); + } + } + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events['config.importer.validate'][] = array('onConfigImporterValidate', 40); + $events['config.installer.validate'][] = array('onConfigImporterValidate', 40); + return $events; + } + +} diff --git a/core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..54c60dc1ba49215061d65911b644b42a3c0edc10 --- /dev/null +++ b/core/lib/Drupal/Core/EventSubscriber/ConfigSnapshotSubscriber.php @@ -0,0 +1,68 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\EventSubscriber\ConfigSnapshotSubscriber. + */ + +namespace Drupal\Core\EventSubscriber; + +use Drupal\Core\Config\Config; +use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\ConfigImporterEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Create a snapshot when config is imported. + */ +class ConfigSnapshotSubscriber implements EventSubscriberInterface { + + /** + * The source storage used to discover configuration changes. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $sourceStorage; + + /** + * The snapshot storage used to write configuration changes. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $snapshotStorage; + + /** + * Constructs the ConfigSnapshotSubscriber object. + * + * @param StorageInterface $source_storage + * The source storage used to discover configuration changes. + * @param StorageInterface $snapshot_storage + * The snapshot storage used to write configuration changes. + */ + public function __construct(StorageInterface $source_storage, StorageInterface $snapshot_storage) { + $this->sourceStorage = $source_storage; + $this->snapshotStorage = $snapshot_storage; + } + + /** + * Creates a config snapshot. + * + * @param \Drupal\Core\Config\ConfigImporterEvent $event + * The Event to process. + */ + public function onConfigImporterImport(ConfigImporterEvent $event) { + config_import_create_snapshot($this->sourceStorage, $this->snapshotStorage); + } + + /** + * Registers the methods in this class that should be listeners. + * + * @return array + * An array of event listener definitions. + */ + static function getSubscribedEvents() { + $events['config.importer.import'][] = array('onConfigImporterImport', 40); + return $events; + } + +} diff --git a/core/modules/config/config.admin.inc b/core/modules/config/config.admin.inc index 57704de8e9ab27d51b6351c3374094abda45be82..a9612861b8ba7f1ddf73ffe51ffe574bb90bce77 100644 --- a/core/modules/config/config.admin.inc +++ b/core/modules/config/config.admin.inc @@ -5,9 +5,12 @@ * Admin page callbacks for the config module. */ -use Drupal\Core\Config\StorageInterface; use Drupal\Core\Ajax\AjaxResponse; use Drupal\Core\Ajax\OpenModalDialogCommand; +use Drupal\Core\Config\ConfigException; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\StorageComparerManifest; +use Drupal\Core\Config\StorageInterface; /** * Helper function to construct the storage changes in a configuration synchronization form. @@ -16,12 +19,13 @@ * The form structure to add to. Passed by reference. * @param array $form_state * The current state of the form. Passed by reference. - * @param Drupal\Core\Config\StorageInterface $source_storage + * @param \Drupal\Core\Config\StorageInterface $source_storage * The source storage to retrieve differences from. - * @param Drupal\Core\Config\StorageInterface $target_storage - * The target storage to compare differences to. + * + * @return array + * The form with the configuration storage changes. */ -function config_admin_sync_form(array &$form, array &$form_state, StorageInterface $source_storage, StorageInterface $target_storage) { +function config_admin_sync_form(array &$form, array &$form_state, StorageInterface $source_storage) { $source_list = $source_storage->listAll(); if (empty($source_list)) { $form['no_changes'] = array( @@ -31,18 +35,23 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa return $form; } - $config_changes = config_sync_get_changes($source_storage, $target_storage); - if (empty($config_changes)) { + $config_comparer = new StorageComparerManifest(Drupal::service('config.storage.staging'), Drupal::service('config.storage')); + if (!$config_comparer->createChangelist()->hasChanges()) { $form['no_changes'] = array( '#markup' => t('There are no configuration changes.'), ); + $form['actions']['#access'] = FALSE; return $form; } + else { + // Store the comparer for use in the submit. + $form_state['storage_comparer'] = $config_comparer; + } // Add the AJAX library to the form for dialog support. $form['#attached']['library'][] = array('system', 'drupal.ajax'); - foreach ($config_changes as $config_change_type => $config_files) { + foreach ($config_comparer->getChangelist() as $config_change_type => $config_files) { if (empty($config_files)) { continue; } @@ -58,7 +67,7 @@ function config_admin_sync_form(array &$form, array &$form_state, StorageInterfa $form[$config_change_type]['heading']['#value'] = format_plural(count($config_files), '@count new', '@count new'); break; - case 'change': + case 'update': $form[$config_change_type]['heading']['#value'] = format_plural(count($config_files), '@count changed', '@count changed'); break; @@ -122,23 +131,37 @@ function config_admin_import_form($form, &$form_state) { * Form submission handler for config_admin_import_form(). */ function config_admin_import_form_submit($form, &$form_state) { - if (!lock()->lockMayBeAvailable(CONFIG_IMPORT_LOCK)) { + $config_importer = new ConfigImporter( + $form_state['storage_comparer'], + Drupal::service('event_dispatcher'), + Drupal::service('config.factory'), + Drupal::entityManager(), + Drupal::lock() + ); + if ($config_importer->alreadyImporting()) { drupal_set_message(t('Another request may be synchronizing configuration already.')); } - else if (config_import()) { - // Once a sync completes, we empty the staging directory. This prevents - // changes from being accidentally overwritten by stray files getting - // imported later. - $source_storage = drupal_container()->get('config.storage.staging'); - foreach ($source_storage->listAll() as $name) { - $source_storage->delete($name); + else{ + try { + $config_importer->import(); + drupal_flush_all_caches(); + drupal_set_message(t('The configuration was imported successfully.')); + + // Once a sync completes, we empty the staging directory. This prevents + // changes from being accidentally overwritten by stray files getting + // imported later. + $source_storage = $config_importer->getStorageComparer()->getSourceStorage(); + foreach ($source_storage->listAll() as $name) { + $source_storage->delete($name); + } + } + catch (ConfigException $e) { + // 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. + watchdog_exception('config_import', $e); + drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error'); } - - drupal_flush_all_caches(); - - drupal_set_message(t('The configuration was imported successfully.')); - } - else { - drupal_set_message(t('The import failed due to an error. Any errors have been logged.'), 'error'); } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php index 497fd525b9bbbbd711d3d459d254fb74e80c29e6..7322324bd27a37b46ff5d457c0d352152d6bc9b6 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigCRUDTest.php @@ -14,6 +14,14 @@ * Tests CRUD operations on configuration objects. */ class ConfigCRUDTest extends DrupalUnitTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('system'); + public static function getInfo() { return array( 'name' => 'CRUD operations', @@ -193,8 +201,15 @@ function testNameValidation() { $manifest_data['new']['name'] = 'invalid'; $staging->write('manifest.invalid_object_name', $manifest_data); - // Assert that config_import returns false indicating a failure. - $this->assertFalse(config_import(), "Config import failed when trying to importing an object with an invalid name"); + // Verify that an exception is thrown when importing. + $message = 'Expected ConfigNameException was thrown when attempting to sync invalid configuration.'; + try { + $this->configImporter()->import(); + $this->fail($message); + } + catch (ConfigNameException $e) { + $this->pass($message); + } } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php index 03fda2a77c70f693173364c067ca4c5f7fa4391a..9d1e6be66b0463ba91e06db432eec77a1da7e8af 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportTest.php @@ -2,17 +2,26 @@ /** * @file - * Definition of Drupal\config\Tests\ConfigImportTest. + * Contains \Drupal\config\Tests\ConfigImporterTest. */ namespace Drupal\config\Tests; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\StorageComparerManifest; use Drupal\simpletest\DrupalUnitTestBase; /** * Tests importing configuration from files into active configuration. */ -class ConfigImportTest extends DrupalUnitTestBase { +class ConfigImporterTest extends DrupalUnitTestBase { + + /** + * Config Importer object used for testing. + * + * @var \Drupal\Core\Config\ConfigImporter + */ + protected $configImporter; /** * Modules to enable. @@ -39,6 +48,18 @@ function setUp() { // variable being used for recording hook invocations by this test already, // so it has to be cleared out manually. unset($GLOBALS['hook_config_test']); + + // Set up the ConfigImporter object for testing. + $config_comparer = new StorageComparerManifest( + $this->container->get('config.storage.staging'), + $this->container->get('config.storage')); + $this->configImporter = new ConfigImporter( + $config_comparer->createChangelist(), + $this->container->get('event_dispatcher'), + $this->container->get('config.factory'), + $this->container->get('plugin.manager.entity'), + $this->container->get('lock') + ); } /** @@ -70,7 +91,7 @@ function testDeleted() { // Create an empty manifest to delete the configuration object. $staging->write('manifest.config_test.dynamic', array()); // Import. - config_import(); + $this->configImporter->reset()->import(); // Verify the values have disappeared. $this->assertIdentical($storage->read($dynamic_name), FALSE); @@ -87,7 +108,7 @@ function testDeleted() { $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); // Verify that there is nothing more to import. - $this->assertFalse(config_sync_get_changes($staging, $storage)); + $this->assertFalse($this->configImporter->hasUnprocessedChanges()); } /** @@ -123,7 +144,7 @@ function testNew() { $this->assertIdentical($staging->exists($dynamic_name), TRUE, $dynamic_name . ' found.'); // Import. - config_import(); + $this->configImporter->reset()->import(); // Verify the values appeared. $config = config($dynamic_name); @@ -138,7 +159,7 @@ function testNew() { $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); // Verify that there is nothing more to import. - $this->assertFalse(config_sync_get_changes($staging, $storage)); + $this->assertFalse($this->configImporter->hasUnprocessedChanges()); } /** @@ -174,7 +195,7 @@ function testUpdated() { $this->assertIdentical($config->get('label'), 'Default'); // Import. - config_import(); + $this->configImporter->reset()->import(); // Verify the values were updated. $config = config($name); @@ -195,7 +216,7 @@ function testUpdated() { $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); // Verify that there is nothing more to import. - $this->assertFalse(config_sync_get_changes($staging, $storage)); + $this->assertFalse($this->configImporter->hasUnprocessedChanges()); } } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index ad6ac3208084e7e3d54b4cf23405d8463b5c96fd..61c6e473e914ee29716c7804098e6ec4bdeb0876 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -112,7 +112,8 @@ function testImportLock() { $this->assertNoText(t('There are no configuration changes.')); // Acquire a fake-lock on the import mechanism. - lock()->acquire('config_import'); + $config_importer_lock = $this->configImporter()->getId(); + $this->container->get('lock')->acquire($config_importer_lock); // Attempt to import configuration and verify that an error message appears. $this->drupalPost(NULL, array(), t('Import all')); @@ -120,7 +121,7 @@ function testImportLock() { $this->assertText(t('Another request may be synchronizing configuration already.')); // Release the lock, just to keep testing sane. - lock()->release('config_import'); + $this->container->get('lock')->release($config_importer_lock); // Verify site name has not changed. $this->assertNotEqual($new_site_name, config('system.site')->get('name')); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php index 6dfcf4edef927419578e716b74308421a427e16c..e0748a7207b306fb9f623e66f3633dab77d54292 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigOverrideTest.php @@ -8,7 +8,6 @@ namespace Drupal\config\Tests; use Drupal\simpletest\DrupalUnitTestBase; -use Drupal\Core\Config\Context\ConfigContext; /** * Tests configuration overrides via $conf in settings.php. @@ -118,7 +117,7 @@ function testConfOverride() { $staging->write('config_test.system', $expected_new_data); // Import changed data from staging to active. - config_import(); + $this->configImporter()->import(); $data = $active->read('config_test.system'); // Verify that the new configuration data exists. Have to read storage diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php index 439f1916ad9afeb333a3237e82d7bd89e8461c1b..7315c53cb3551cfb1e871d8bb9df0073ed9d9f50 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigSnapshotTest.php @@ -7,6 +7,7 @@ namespace Drupal\config\Tests; +use Drupal\Core\Config\StorageComparer; use Drupal\simpletest\DrupalUnitTestBase; /** @@ -45,21 +46,24 @@ function testSnapshot() { $config_key = 'foo'; $new_data = 'foobar'; + $active_snapshot_comparer = new StorageComparer($active, $snapshot); + $staging_snapshot_comparer = new StorageComparer($staging, $snapshot); + // Verify that we have an initial snapshot that matches the active // configuration. This has to be true as no config should be installed. - $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE)); + $this->assertFalse($active_snapshot_comparer->createChangelist()->hasChanges()); // Install the default config. config_install_default_config('module', 'config_test'); // Although we have imported config this has not affected the snapshot. - $this->assertTrue(config_sync_get_changes($snapshot, $active, FALSE)); + $this->assertTrue($active_snapshot_comparer->reset()->hasChanges()); // Update the config snapshot. config_import_create_snapshot($active, $snapshot); // The snapshot and active config should now contain the same config // objects. - $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE)); + $this->assertFalse($active_snapshot_comparer->reset()->hasChanges()); // Change a configuration value in staging. $staging_data = config($config_name)->get(); @@ -67,20 +71,19 @@ function testSnapshot() { $staging->write($config_name, $staging_data); // Verify that active and snapshot match, and that staging doesn't match - // either of them. - $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE)); - $this->assertTrue(config_sync_get_changes($snapshot, $staging, FALSE)); - $this->assertTrue(config_sync_get_changes($staging, $active, FALSE)); + // active. + $this->assertFalse($active_snapshot_comparer->reset()->hasChanges()); + $this->assertTrue($staging_snapshot_comparer->createChangelist()->hasChanges()); // Import changed data from staging to active. - config_import(); + $this->configImporter()->import(); // Verify changed config was properly imported. $this->assertIdentical(config($config_name)->get($config_key), $new_data); // Verify that a new snapshot was created which and that it matches // the active config. - $this->assertFalse(config_sync_get_changes($snapshot, $active, FALSE)); + $this->assertFalse($active_snapshot_comparer->reset()->hasChanges()); } } diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php index e1fe4fe025e00ed0aeb24d74c8b5a0c56cfeb768..a73cffb551e48a6a969b858c982a7db6fed1f2cb 100644 --- a/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/ConfigTestStorageController.php @@ -26,13 +26,13 @@ public function importCreate($name, Config $new_config, Config $old_config) { } /** - * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importChange(). + * Overrides \Drupal\Core\Config\Entity\ConfigStorageController::importUpdate(). */ - public function importChange($name, Config $new_config, Config $old_config) { + public function importUpdate($name, Config $new_config, Config $old_config) { // Set a global value we can check in test code. $GLOBALS['hook_config_import'] = __METHOD__; - return parent::importChange($name, $new_config, $old_config); + return parent::importUpdate($name, $new_config, $old_config); } /** diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php index aaf648d2771181bbb9fda8d9038fa2e42f99f3ac..70e7286b0286f6ef76a568b76a4ec40616d7ddb6 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportChangeTest.php @@ -54,7 +54,7 @@ function testImportChange() { $staging->write($instance_config_name, $instance); // Import the content of the staging directory. - config_import(); + $this->configImporter()->import(); // Check that the updated config was correctly imported. $instance = entity_load('field_instance', $instance_id); diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php index ff21fac97bda5fcb8e703dd6de4e24cc1541919b..577de45f44a9318969c8518a0a7f4c0b2a560627 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportCreateTest.php @@ -69,7 +69,7 @@ function testImportCreate() { $staging->write($instance_manifest_name, $instance_manifest); // Import the content of the staging directory. - config_import(); + $this->configImporter()->import(); // Check that the field and instance were created. $field = entity_load('field_entity', $field_id); diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php index 0db6455947f61a4c077b8acc905feb4769db3792..c1ac59a5a241e3a92e16a1f30c187fe6026ecd8a 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php @@ -66,7 +66,7 @@ function testImportDelete() { $staging->write($instance_manifest_name, $instance_manifest); // Import the content of the staging directory. - config_import(); + $this->configImporter()->import(); // Check that the field and instance are gone. $field = entity_load('field_entity', $field_id, TRUE); diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php index 223e0acc6f8ad36dfeb95c24d6e92987bd9a7b50..23f5269ff6b3967bd4fff37d4d6c142879ffee7a 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldUnitTestBase.php @@ -38,8 +38,8 @@ function setUp() { $this->installSchema('entity_test', 'entity_test'); $this->installSchema('field_test', array('test_entity', 'test_entity_revision', 'test_entity_bundle')); - // Set default storage backend. - $this->installConfig(array('field')); + // Set default storage backend and configure the theme system. + $this->installConfig(array('field', 'system')); } /** diff --git a/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php b/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php index 4cb1dca043cee7242d8c94545ec70ebf7776d026..f667a671ad09fdacb46e8e207c91d7da0090b497 100644 --- a/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php +++ b/core/modules/image/lib/Drupal/image/Tests/ImageAdminStylesTest.php @@ -367,7 +367,7 @@ function testConfigImport() { unset($manifest_data[$style_name]); $staging = $this->container->get('config.storage.staging'); $staging->write('manifest.image.style', $manifest_data); - config_import(); + $this->configImporter()->import(); $this->assertFalse(entity_load('image_style', $style_name), 'Style deleted after config import.'); $this->assertEqual($this->getImageCount($style), 0, 'Image style was flushed after being deleted by config import.'); diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 259d2750990a162c8579a03f89491a413b209339..6e791b3fc64ebd9b109032c52642e0da2cbdedc4 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -9,6 +9,8 @@ use Drupal\Core\Database\Database; use Drupal\Component\Utility\Settings; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\StorageComparerManifest; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Database\ConnectionNotDefinedException; use Drupal\Core\DrupalKernel; @@ -167,6 +169,13 @@ abstract class TestBase { */ protected $container; + /** + * The config importer that can used in a test. + * + * @var \Drupal\Core\Config\ConfigImporter + */ + protected $configImporter; + /** * Constructor for Test. * @@ -1272,4 +1281,28 @@ public static function generatePermutations($parameters) { public static function filePreDeleteCallback($path) { chmod($path, 0700); } + + /** + * Returns a ConfigImporter object to import test importing of configuration. + * + * @return \Drupal\Core\Config\ConfigImporter + * The ConfigImporter object. + */ + public function configImporter() { + if (!$this->configImporter) { + // Set up the ConfigImporter object for testing. + $config_comparer = new StorageComparerManifest( + $this->container->get('config.storage.staging'), + $this->container->get('config.storage')); + $this->configImporter = new ConfigImporter( + $config_comparer, + $this->container->get('event_dispatcher'), + $this->container->get('config.factory'), + $this->container->get('plugin.manager.entity'), + $this->container->get('lock') + ); + } + // Always recalculate the changelist when called. + return $this->configImporter->reset(); + } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php b/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php index 4ad4417f9c4a31daa68f992b75b0a67763840037..f38b49f0640fc3affeb70753c4ae84b521619814 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Database/RegressionTest.php @@ -12,6 +12,13 @@ */ class RegressionTest extends DatabaseTestBase { + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('node'); + public static function getInfo() { return array( 'name' => 'Regression tests', diff --git a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php index 712638430acfa9d6fc26be91e955f2aec9abacb3..9530fd8d0e1730c6876bc8121e9dfa6909f4d895 100644 --- a/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php +++ b/core/modules/text/lib/Drupal/text/Tests/Formatter/TextPlainUnitTest.php @@ -25,7 +25,7 @@ class TextPlainUnitTest extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('entity', 'field', 'field_sql_storage', 'text', 'field_test'); + public static $modules = array('system', 'entity', 'field', 'field_sql_storage', 'text', 'field_test'); /** * Contains rendered content. @@ -45,7 +45,8 @@ public static function getInfo() { function setUp() { parent::setUp(); - $this->installConfig(array('field')); + // Configure the theme system. + $this->installConfig(array('system', 'field')); // @todo Add helper methods for all of the following. diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewTestConfigInstaller.php b/core/modules/views/lib/Drupal/views/Tests/ViewTestConfigInstaller.php new file mode 100644 index 0000000000000000000000000000000000000000..c514ebdd65b096ef2cbc9ff33ae0d26fdbab8540 --- /dev/null +++ b/core/modules/views/lib/Drupal/views/Tests/ViewTestConfigInstaller.php @@ -0,0 +1,27 @@ +<?php + +/** + * @file + * Contains \Drupal\views\Tests\ViewTestConfigInstaller. + */ + +namespace Drupal\views\Tests; + +use Drupal\Core\Config\ConfigImporter; + +/** + * Defines a configuration installer. + * + * A config installer imports test views for views testing. + * + * @see \Drupal\Core\Config\ConfigImporter + * @see \Drupal\views\Tests\ViewTestData + */ +class ViewTestConfigInstaller extends ConfigImporter { + + /** + * The name used to identify events and the lock. + */ + const ID = 'views.test.installer'; + +} diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php b/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php index cf2c0d8df3ee54e6053e4560d883ea121a5f3399..3f2bfd1dd396f657283ac93307f642d1db130aa1 100644 --- a/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php +++ b/core/modules/views/lib/Drupal/views/Tests/ViewTestData.php @@ -8,6 +8,7 @@ namespace Drupal\views\Tests; use Drupal\Core\Config\FileStorage; +use Drupal\Core\Config\StorageComparer; /** * Provides tests view data and the base test schema with sample data records. @@ -39,13 +40,6 @@ public static function importTestViews($class, $modules = array()) { $class = get_parent_class($class); } if (!empty($views)) { - $target_storage = drupal_container()->get('config.storage'); - $config_changes = array( - 'delete' => array(), - 'create' => array(), - 'change' => array(), - ); - $module_handler = \Drupal::moduleHandler(); foreach ($modules as $module) { $config_dir = drupal_get_path('module', $module) . '/test_views'; @@ -54,16 +48,27 @@ public static function importTestViews($class, $modules = array()) { } $source_storage = new FileStorage($config_dir); + // Only import views used by test. + $views_to_import = array(); foreach ($source_storage->listAll('views.view.') as $config_name) { $id = str_replace('views.view.', '', $config_name); if (in_array($id, $views)) { - $config_changes['create'][] = $config_name; + $views_to_import[] = $config_name; } } - } - if (!empty($config_changes['create'])) { - $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') + ); + $storage_comparer->addChangelist('create', $views_to_import); + $installer = new ViewTestConfigInstaller( + $storage_comparer, + \Drupal::service('event_dispatcher'), + \Drupal::service('config.factory'), + \Drupal::entityManager(), + \Drupal::lock() + ); + $installer->import(); } } } diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php b/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php index 80b659cdfa76a145457a868c431bcba6d6197b96..134b97afd59208d6a6dcca1a06edb334d335d5e7 100644 --- a/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php +++ b/core/modules/views/lib/Drupal/views/Tests/ViewUnitTestBase.php @@ -29,7 +29,7 @@ abstract class ViewUnitTestBase extends DrupalUnitTestBase { * * @var array */ - public static $modules = array('views', 'views_test_config', 'views_test_data'); + public static $modules = array('system', 'views', 'views_test_config', 'views_test_data'); protected function setUp() { parent::setUp(); @@ -62,6 +62,9 @@ protected function setUpFixtures() { } $query->execute(); + // Tests implementing ViewUnitTestBase depend on the theme system being + // properly configured. + $this->installConfig(array('system')); ViewTestData::importTestViews(get_class($this), array('views_test_config')); }