Issue #1890784 by alexpott, YesCT, heyrocker, tim.plunkett, EllaTheHarpy, beejeebus: Refactor configuration import and sync functions.

8.0.x
Nathaniel Catchpole 2013-05-17 15:16:16 +01:00
parent 3a8e8c6082
commit 4eaa775b10
31 changed files with 1218 additions and 323 deletions

View File

@ -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:

View File

@ -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 @@ use Symfony\Component\Yaml\Dumper;
* 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 @@ const CONFIG_IMPORT_LOCK = 'config_import';
* 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.
*

View File

@ -96,15 +96,13 @@ class ConfigFactory {
*/
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;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 {}

View File

@ -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';
}

View File

@ -521,7 +521,7 @@ class ConfigStorageController implements EntityStorageControllerInterface {
}
/**
* 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 @@ class ConfigStorageController implements EntityStorageControllerInterface {
* @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];

View File

@ -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;
}
}

View File

@ -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'));
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 @@ use Drupal\Core\Ajax\OpenModalDialogCommand;
* 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');
}
}

View File

@ -14,6 +14,14 @@ use Drupal\simpletest\DrupalUnitTestBase;
* 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 @@ class ConfigCRUDTest extends DrupalUnitTestBase {
$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);
}
}
}

View File

@ -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 @@ class ConfigImportTest extends DrupalUnitTestBase {
// 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 @@ class ConfigImportTest extends DrupalUnitTestBase {
// 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 @@ class ConfigImportTest extends DrupalUnitTestBase {
$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 @@ class ConfigImportTest extends DrupalUnitTestBase {
$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 @@ class ConfigImportTest extends DrupalUnitTestBase {
$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 @@ class ConfigImportTest extends DrupalUnitTestBase {
$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 @@ class ConfigImportTest extends DrupalUnitTestBase {
$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());
}
}

View File

@ -112,7 +112,8 @@ class ConfigImportUITest extends WebTestBase {
$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 @@ class ConfigImportUITest extends WebTestBase {
$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'));

View File

@ -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 @@ class ConfigOverrideTest extends DrupalUnitTestBase {
$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

View File

@ -7,6 +7,7 @@
namespace Drupal\config\Tests;
use Drupal\Core\Config\StorageComparer;
use Drupal\simpletest\DrupalUnitTestBase;
/**
@ -45,21 +46,24 @@ class ConfigSnapshotTest extends DrupalUnitTestBase {
$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 @@ class ConfigSnapshotTest extends DrupalUnitTestBase {
$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());
}
}

View File

@ -26,13 +26,13 @@ class ConfigTestStorageController extends ConfigStorageController {
}
/**
* 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);
}
/**

View File

@ -54,7 +54,7 @@ class FieldImportChangeTest extends FieldUnitTestBase {
$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);

View File

@ -69,7 +69,7 @@ class FieldImportCreateTest extends FieldUnitTestBase {
$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);

View File

@ -66,7 +66,7 @@ class FieldImportDeleteTest extends FieldUnitTestBase {
$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);

View File

@ -38,8 +38,8 @@ abstract class FieldUnitTestBase extends DrupalUnitTestBase {
$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'));
}
/**

View File

@ -367,7 +367,7 @@ class ImageAdminStylesTest extends ImageFieldTestBase {
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.');

View File

@ -9,6 +9,8 @@ namespace Drupal\simpletest;
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 @@ abstract class TestBase {
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();
}
}

View File

@ -12,6 +12,13 @@ namespace Drupal\system\Tests\Database;
*/
class RegressionTest extends DatabaseTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('node');
public static function getInfo() {
return array(
'name' => 'Regression tests',

View File

@ -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 @@ class TextPlainUnitTest extends DrupalUnitTestBase {
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.

View File

@ -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';
}

View File

@ -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 @@ class ViewTestData {
$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 @@ class ViewTestData {
}
$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();
}
}
}

View File

@ -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 @@ abstract class ViewUnitTestBase extends DrupalUnitTestBase {
}
$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'));
}