Issue #1808248 by alexpott, beejeebus, tayzlor, Nitesh Sethia: Add a separate module install/uninstall step to the config import process.

8.0.x
Nathaniel Catchpole 2014-04-04 14:49:13 +01:00
parent 8866760fca
commit ae702dc56d
38 changed files with 1232 additions and 168 deletions

View File

@ -612,4 +612,14 @@ class Drupal {
return static::$container->get('form_builder');
}
/**
* Gets the syncing state.
*
* @return bool
* Returns TRUE is syncing flag set.
*/
public function isConfigSyncing() {
return static::$container->get('config.installer')->isSyncing();
}
}

View File

@ -14,10 +14,34 @@ namespace Drupal\Core\Config;
*/
class BatchConfigImporter extends ConfigImporter {
/**
* The total number of extensions to process.
*
* @var int
*/
protected $totalExtensionsToProcess = 0;
/**
* The total number of configuration objects to process.
*
* @var int
*/
protected $totalConfigurationToProcess = 0;
/**
* Initializes the config importer in preparation for processing a batch.
*
* @return array
* An array of method names that to be called by the batch. If there are
* modules or themes to process then an extra step is added.
*
* @throws ConfigImporterException
* If the configuration is already importing.
*/
public function initialize() {
$batch_operations = array();
$this->createExtensionChangelist();
// Ensure that the changes have been validated.
$this->validate();
@ -25,61 +49,137 @@ class BatchConfigImporter extends ConfigImporter {
// Another process is synchronizing configuration.
throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID));
}
$this->totalToProcess = 0;
foreach(array('create', 'delete', 'update') as $op) {
$this->totalToProcess += count($this->getUnprocessed($op));
$modules = $this->getUnprocessedExtensions('module');
foreach (array('install', 'uninstall') as $op) {
$this->totalExtensionsToProcess += count($modules[$op]);
}
$themes = $this->getUnprocessedExtensions('theme');
foreach (array('enable', 'disable') as $op) {
$this->totalExtensionsToProcess += count($themes[$op]);
}
// We have extensions to process.
if ($this->totalExtensionsToProcess > 0) {
$batch_operations[] = 'processExtensionBatch';
}
$batch_operations[] = 'processConfigurationBatch';
$batch_operations[] = 'finishBatch';
return $batch_operations;
}
/**
* Processes batch.
* Processes extensions as a batch operation.
*
* @param array $context.
* The batch context.
*/
public function processBatch(array &$context) {
$operation = $this->getNextOperation();
public function processExtensionBatch(array &$context) {
$operation = $this->getNextExtensionOperation();
if (!empty($operation)) {
$this->process($operation['op'], $operation['name']);
$context['message'] = t('Synchronizing @name.', array('@name' => $operation['name']));
$context['finished'] = $this->batchProgress();
$this->processExtension($operation['type'], $operation['op'], $operation['name']);
$context['message'] = t('Synchronising extensions: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name']));
$processed_count = count($this->processedExtensions['module']['install']) + count($this->processedExtensions['module']['uninstall']);
$processed_count += count($this->processedExtensions['theme']['disable']) + count($this->processedExtensions['theme']['enable']);
$context['finished'] = $processed_count / $this->totalExtensionsToProcess;
}
else {
$context['finished'] = 1;
}
if ($context['finished'] >= 1) {
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
// The import is now complete.
$this->lock->release(static::LOCK_ID);
$this->reset();
}
/**
* Processes configuration as a batch operation.
*
* @param array $context.
* The batch context.
*/
public function processConfigurationBatch(array &$context) {
// The first time this is called we need to calculate the total to process.
// This involves recalculating the changelist which will ensure that if
// extensions have been processed any configuration affected will be taken
// into account.
if ($this->totalConfigurationToProcess == 0) {
$this->storageComparer->reset();
foreach (array('delete', 'create', 'update') as $op) {
$this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op));
}
}
$operation = $this->getNextConfigurationOperation();
if (!empty($operation)) {
$this->processConfiguration($operation['op'], $operation['name']);
$context['message'] = t('Synchronizing configuration: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name']));
$processed_count = count($this->processedConfiguration['create']) + count($this->processedConfiguration['delete']) + count($this->processedConfiguration['update']);
$context['finished'] = $processed_count / $this->totalConfigurationToProcess;
}
else {
$context['finished'] = 1;
}
}
/**
* Gets percentage of progress made.
* Finishes the batch.
*
* @return float
* The percentage of progress made expressed as a float between 0 and 1.
* @param array $context.
* The batch context.
*/
protected function batchProgress() {
$processed_count = count($this->processed['create']) + count($this->processed['delete']) + count($this->processed['update']);
return $processed_count / $this->totalToProcess;
public function finishBatch(array &$context) {
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
// The import is now complete.
$this->lock->release(static::LOCK_ID);
$this->reset();
$context['message'] = t('Finalising configuration synchronisation.');
$context['finished'] = 1;
}
/**
* Gets the next operation to perform.
* Gets the next extension operation to perform.
*
* @return array|bool
* An array containing the next operation and extension name to perform it
* on. If there is nothing left to do returns FALSE;
*/
protected function getNextExtensionOperation() {
foreach (array('install', 'uninstall') as $op) {
$modules = $this->getUnprocessedExtensions('module');
if (!empty($modules[$op])) {
return array(
'op' => $op,
'type' => 'module',
'name' => array_shift($modules[$op]),
);
}
}
foreach (array('enable', 'disable') as $op) {
$themes = $this->getUnprocessedExtensions('theme');
if (!empty($themes[$op])) {
return array(
'op' => $op,
'type' => 'theme',
'name' => array_shift($themes[$op]),
);
}
}
return FALSE;
}
/**
* Gets the next configuration operation to perform.
*
* @return array|bool
* An array containing the next operation and configuration name to perform
* it on. If there is nothing left to do returns FALSE;
*/
protected function getNextOperation() {
foreach(array('create', 'delete', 'update') as $op) {
$names = $this->getUnprocessed($op);
if (!empty($names)) {
protected function getNextConfigurationOperation() {
// The order configuration operations is processed is important. Deletes
// have to come first so that recreates can work.
foreach (array('delete', 'create', 'update') as $op) {
$config_names = $this->getUnprocessedConfiguration($op);
if (!empty($config_names)) {
return array(
'op' => $op,
'name' => array_shift($names),
'name' => array_shift($config_names),
);
}
}

View File

@ -7,6 +7,8 @@
namespace Drupal\Core\Config;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\Entity\ImportableEntityStorageInterface;
use Drupal\Core\DependencyInjection\DependencySerialization;
@ -71,16 +73,30 @@ class ConfigImporter extends DependencySerialization {
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManager
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* List of changes processed by the import().
* List of configuration file changes processed by the import().
*
* @var array
*/
protected $processed;
protected $processedConfiguration;
/**
* List of extension changes processed by the import().
*
* @var array
*/
protected $processedExtensions;
/**
* List of extension changes to be processed by the import().
*
* @var array
*/
protected $extensionChangelist;
/**
* Indicates changes to import have been validated.
@ -89,6 +105,27 @@ class ConfigImporter extends DependencySerialization {
*/
protected $validated;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Flag set to import system.theme during processing theme enable and disables.
*
* @var bool
*/
protected $processedSystemTheme = FALSE;
/**
* Constructs a configuration import object.
*
@ -103,14 +140,21 @@ class ConfigImporter extends DependencySerialization {
* The lock backend to ensure multiple imports do not occur at the same time.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config
* The typed configuration manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler
*/
public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManager $typed_config) {
public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
$this->storageComparer = $storage_comparer;
$this->eventDispatcher = $event_dispatcher;
$this->configManager = $config_manager;
$this->lock = $lock;
$this->typedConfigManager = $typed_config;
$this->processed = $this->storageComparer->getEmptyChangelist();
$this->moduleHandler = $module_handler;
$this->themeHandler = $theme_handler;
$this->processedConfiguration = $this->storageComparer->getEmptyChangelist();
$this->processedExtensions = $this->getEmptyExtensionsProcessedList();
}
/**
@ -131,13 +175,35 @@ class ConfigImporter extends DependencySerialization {
*/
public function reset() {
$this->storageComparer->reset();
$this->processed = $this->storageComparer->getEmptyChangelist();
$this->processedConfiguration = $this->storageComparer->getEmptyChangelist();
$this->processedExtensions = $this->getEmptyExtensionsProcessedList();
$this->createExtensionChangelist();
$this->validated = FALSE;
$this->processedSystemTheme = FALSE;
return $this;
}
/**
* Checks if there are any unprocessed changes.
* Gets an empty list of extensions to process.
*
* @return array
* An empty list of extensions to process.
*/
protected function getEmptyExtensionsProcessedList() {
return array(
'module' => array(
'install' => array(),
'uninstall' => array(),
),
'theme' => array(
'enable' => array(),
'disable' => array(),
),
);
}
/**
* Checks if there are any unprocessed configuration changes.
*
* @param array $ops
* The operations to check for changes. Defaults to all operations, i.e.
@ -146,9 +212,9 @@ class ConfigImporter extends DependencySerialization {
* @return bool
* TRUE if there are changes to process and FALSE if not.
*/
public function hasUnprocessedChanges($ops = array('delete', 'create', 'update')) {
public function hasUnprocessedConfigurationChanges($ops = array('delete', 'create', 'update')) {
foreach ($ops as $op) {
if (count($this->getUnprocessed($op))) {
if (count($this->getUnprocessedConfiguration($op))) {
return TRUE;
}
}
@ -161,8 +227,8 @@ class ConfigImporter extends DependencySerialization {
* @return array
* An array containing a list of processed changes.
*/
public function getProcessed() {
return $this->processed;
public function getProcessedConfiguration() {
return $this->processedConfiguration;
}
/**
@ -173,8 +239,8 @@ class ConfigImporter extends DependencySerialization {
* @param string $name
* The name of the configuration processed.
*/
protected function setProcessed($op, $name) {
$this->processed[$op][] = $name;
protected function setProcessedConfiguration($op, $name) {
$this->processedConfiguration[$op][] = $name;
}
/**
@ -187,8 +253,139 @@ class ConfigImporter extends DependencySerialization {
* @return array
* An array of configuration names.
*/
public function getUnprocessed($op) {
return array_diff($this->storageComparer->getChangelist($op), $this->processed[$op]);
public function getUnprocessedConfiguration($op) {
return array_diff($this->storageComparer->getChangelist($op), $this->processedConfiguration[$op]);
}
/**
* Gets list of processed extension changes.
*
* @return array
* An array containing a list of processed extension changes.
*/
public function getProcessedExtensions() {
return $this->processedExtensions;
}
/**
* Determines if the current import has processed extensions.
*
* @return bool
* TRUE if the ConfigImporter has processed extensions.
*/
protected function hasProcessedExtensions() {
$compare = array_diff($this->processedExtensions, getEmptyExtensionsProcessedList());
return !empty($compare);
}
/**
* Sets an extension change as processed.
*
* @param string $type
* The type of extension, either 'theme' or 'module'.
* @param string $op
* The change operation performed, either install or uninstall.
* @param string $name
* The name of the extension processed.
*/
protected function setProcessedExtension($type, $op, $name) {
$this->processedExtensions[$type][$op][] = $name;
}
/**
* Populates the extension change list.
*/
protected function createExtensionChangelist() {
// Read the extensions information to determine changes.
$current_extensions = $this->storageComparer->getTargetStorage()->read('core.extension');
$new_extensions = $this->storageComparer->getSourceStorage()->read('core.extension');
// If there is no extension information in staging then exit. This is
// probably due to an empty staging directory.
if (!$new_extensions) {
return;
}
// Get a list of modules with dependency weights as values.
$module_data = system_rebuild_module_data();
// Set the actual module weights.
$module_list = array_combine(array_keys($module_data), array_keys($module_data));
$module_list = array_map(function ($module) use ($module_data) {
return $module_data[$module]->sort;
}, $module_list);
// Work out what modules to install and uninstall.
$uninstall = array_diff(array_keys($current_extensions['module']), array_keys($new_extensions['module']));
$install = array_diff(array_keys($new_extensions['module']), array_keys($current_extensions['module']));
// Sort the module list by their weights. So that dependencies
// are uninstalled last.
asort($module_list);
$uninstall = array_intersect(array_keys($module_list), $uninstall);
// Sort the module list by their weights (reverse). So that dependencies
// are installed first.
arsort($module_list);
$install = array_intersect(array_keys($module_list), $install);
// Work out what themes to enable and to disable.
$enable = array_diff(array_keys($new_extensions['theme']), array_keys($current_extensions['theme']));
$disable = array_diff(array_keys($current_extensions['theme']), array_keys($new_extensions['theme']));
$this->extensionChangelist = array(
'module' => array(
'uninstall' => $uninstall,
'install' => $install,
),
'theme' => array(
'enable' => $enable,
'disable' => $disable,
),
);
}
/**
* Gets a list changes for extensions.
*
* @param string $type
* The type of extension, either 'theme' or 'module'.
* @param string $op
* The change operation to get the unprocessed list for, either install
* or uninstall.
*
* @return array
* An array of extension names.
*/
protected function getExtensionChangelist($type, $op = NULL) {
if ($op) {
return $this->extensionChangelist[$type][$op];
}
return $this->extensionChangelist[$type];
}
/**
* Gets a list of unprocessed changes for extensions.
*
* @param string $type
* The type of extension, either 'theme' or 'module'.
*
* @return array
* An array of extension names.
*/
public function getUnprocessedExtensions($type) {
$changelist = $this->getExtensionChangelist($type);
if ($type == 'theme') {
$unprocessed = array(
'enable' => array_diff($changelist['enable'], $this->processedExtensions[$type]['enable']),
'disable' => array_diff($changelist['disable'], $this->processedExtensions[$type]['disable']),
);
}
else {
$unprocessed = array(
'install' => array_diff($changelist['install'], $this->processedExtensions[$type]['install']),
'uninstall' => array_diff($changelist['uninstall'], $this->processedExtensions[$type]['uninstall']),
);
}
return $unprocessed;
}
/**
@ -200,7 +397,9 @@ class ConfigImporter extends DependencySerialization {
* The ConfigImporter instance.
*/
public function import() {
if ($this->hasUnprocessedChanges()) {
if ($this->hasUnprocessedConfigurationChanges()) {
$this->createExtensionChangelist();
// Ensure that the changes have been validated.
$this->validate();
@ -208,19 +407,20 @@ class ConfigImporter extends DependencySerialization {
// Another process is synchronizing configuration.
throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID));
}
// Process any extension changes before importing configuration.
$this->handleExtensions();
// First pass deleted, then new, and lastly changed configuration, in order
// to handle dependencies correctly.
// @todo Implement proper dependency ordering using
// https://drupal.org/node/2080823
foreach (array('delete', 'create', 'update') as $op) {
foreach ($this->getUnprocessed($op) as $name) {
$this->process($op, $name);
foreach ($this->getUnprocessedConfiguration($op) as $name) {
$this->processConfiguration($op, $name);
}
}
// Allow modules to react to a import.
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
// The import is now complete.
$this->lock->release(static::LOCK_ID);
$this->reset();
@ -253,12 +453,58 @@ class ConfigImporter extends DependencySerialization {
* @param string $name
* The name of the configuration to process.
*/
protected function process($op, $name) {
protected function processConfiguration($op, $name) {
if (!$this->importInvokeOwner($op, $name)) {
$this->importConfig($op, $name);
}
}
/**
* Processes an extension change.
*
* @param string $type
* The type of extension, either 'module' or 'theme'.
* @param string $op
* The change operation.
* @param string $name
* The name of the extension to process.
*/
protected function processExtension($type, $op, $name) {
// Set the config installer to use the staging directory instead of the
// extensions own default config directories.
\Drupal::service('config.installer')
->setSyncing(TRUE)
->setSourceStorage($this->storageComparer->getSourceStorage());
if ($type == 'module') {
$this->moduleHandler->$op(array($name), FALSE);
// Installing a module can cause a kernel boot therefore reinject all the
// services.
$this->reInjectMe();
// During a module install or uninstall the container is rebuilt and the
// module handler is called from drupal_get_complete_schema(). This causes
// the container's instance of the module handler not to have loaded all
// the enabled modules.
$this->moduleHandler->loadAll();
}
if ($type == 'theme') {
// Theme disables possible remove default or admin themes therefore we
// need to import this before doing any. If there are no disables and
// the default or admin theme is change this will be picked up whilst
// processing configuration.
if ($op == 'disable' && $this->processedSystemTheme === FALSE) {
$this->importConfig('update', 'system.theme');
$this->configManager->getConfigFactory()->reset('system.theme');
$this->processedSystemTheme = TRUE;
}
$this->themeHandler->$op(array($name));
}
$this->setProcessedExtension($type, $op, $name);
\Drupal::service('config.installer')
->setSyncing(FALSE)
->resetSourceStorage();
}
/**
* Writes a configuration change from the source to the target storage.
*
@ -277,7 +523,7 @@ class ConfigImporter extends DependencySerialization {
$config->setData($data ? $data : array());
$config->save();
}
$this->setProcessed($op, $name);
$this->setProcessedConfiguration($op, $name);
}
/**
@ -325,7 +571,7 @@ class ConfigImporter extends DependencySerialization {
throw new EntityStorageException(String::format('The entity storage "@storage" for the "@entity_type" entity type does not support imports', array('@storage' => get_class($entity_storage), '@entity_type' => $entity_type)));
}
$entity_storage->$method($name, $new_config, $old_config);
$this->setProcessed($op, $name);
$this->setProcessedConfiguration($op, $name);
return TRUE;
}
return FALSE;
@ -341,4 +587,74 @@ class ConfigImporter extends DependencySerialization {
return !$this->lock->lockMayBeAvailable(static::LOCK_ID);
}
/**
* Returns the identifier for events and locks.
*
* @return string
* The identifier for events and locks.
*/
public function getId() {
return static::LOCK_ID;
}
/**
* Checks if a configuration object will be updated by the import.
*
* @param string $config_name
* The configuration object name.
*
* @return bool
* TRUE if the configuration object will be updated.
*/
protected function hasUpdate($config_name) {
return in_array($config_name, $this->getUnprocessedConfiguration('update'));
}
/**
* Handle changes to installed modules and themes.
*/
protected function handleExtensions() {
$processed_extension = FALSE;
foreach (array('install', 'uninstall') as $op) {
$modules = $this->getUnprocessedExtensions('module');
foreach($modules[$op] as $module) {
$processed_extension = TRUE;
$this->processExtension('module', $op, $module);
}
}
foreach (array('enable', 'disable') as $op) {
$themes = $this->getUnprocessedExtensions('theme');
foreach($themes[$op] as $theme) {
$processed_extension = TRUE;
$this->processExtension('theme', $op, $theme);
}
}
if ($processed_extension) {
// Recalculate differences as default config could have been imported.
$this->storageComparer->reset();
$this->processed = $this->storageComparer->getEmptyChangelist();
// Modules have been updated. Services etc might have changed.
// We don't reinject storage comparer because swapping out the active
// store during config import is a complete nonsense.
$this->recalculateChangelist = TRUE;
}
}
/**
* Gets all the service dependencies from \Drupal.
*
* Since the ConfigImporter handles module installation the kernel and the
* container can be rebuilt and altered during processing. It is necessary to
* keep the services used by the importer in sync.
*/
protected function reInjectMe() {
$this->eventDispatcher = \Drupal::service('event_dispatcher');
$this->configFactory = \Drupal::configFactory();
$this->entityManager = \Drupal::entityManager();
$this->lock = \Drupal::lock();
$this->typedConfigManager = \Drupal::service('config.typed');
$this->moduleHandler = \Drupal::moduleHandler();
$this->themeHandler = \Drupal::service('theme_handler');
}
}

View File

@ -48,6 +48,20 @@ class ConfigInstaller implements ConfigInstallerInterface {
*/
protected $eventDispatcher;
/**
* The configuration storage that provides the default configuration.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $sourceStorage;
/**
* Is configuration being created as part of a configuration sync.
*
* @var bool
*/
protected $isSyncing = FALSE;
/**
* Constructs the configuration installer.
*
@ -75,7 +89,7 @@ class ConfigInstaller implements ConfigInstallerInterface {
*/
public function installDefaultConfig($type, $name) {
// Get all default configuration owned by this extension.
$source_storage = new ExtensionInstallStorage($this->activeStorage);
$source_storage = $this->getSourceStorage();
$config_to_install = $source_storage->listAll($name . '.');
// Work out if this extension provides default configuration for any other
@ -130,6 +144,16 @@ class ConfigInstaller implements ConfigInstallerInterface {
$new_config->setData($data[$name]);
}
if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) {
// If we are syncing do not create configuration entities. Pluggable
// configuration entities can have dependencies on modules that are
// not yet enabled. This approach means that any code that expects
// default configuration entities to exist will be unstable after the
// module has been enabled and before the config entity has been
// imported.
if ($this->isSyncing) {
continue;
}
$entity_storage = $this->configManager
->getEntityManager()
->getStorage($entity_type);
@ -159,4 +183,49 @@ class ConfigInstaller implements ConfigInstallerInterface {
$this->configFactory->reset();
}
/**
* {@inheritdoc}
*/
public function setSourceStorage(StorageInterface $storage) {
$this->sourceStorage = $storage;
return $this;
}
/**
* {@inheritdoc}
*/
public function resetSourceStorage() {
$this->sourceStorage = null;
return $this;
}
/**
* Gets the configuration storage that provides the default configuration.
*
* @return \Drupal\Core\Config\StorageInterface
* The configuration storage that provides the default configuration.
*/
public function getSourceStorage() {
if (!isset($this->sourceStorage)) {
// Default to using the ExtensionInstallStorage which searches extension's
// config directories for default configuration.
$this->sourceStorage = new ExtensionInstallStorage($this->activeStorage);
}
return $this->sourceStorage;
}
/**
* {@inheritdoc}
*/
public function setSyncing($status) {
$this->isSyncing = $status;
return $this;
}
/**
* {@inheritdoc}
*/
public function isSyncing() {
return $this->isSyncing;
}
}

View File

@ -37,4 +37,38 @@ interface ConfigInstallerInterface {
*/
public function installDefaultConfig($type, $name);
/**
* Sets the configuration storage that provides the default configuration.
*
* @param \Drupal\Core\Config\StorageInterface $storage
*
* @return self
* The configuration installer.
*/
public function setSourceStorage(StorageInterface $storage);
/**
* Resets the configuration storage that provides the default configuration.
*
* @return self
* The configuration installer.
*/
public function resetSourceStorage();
/**
* Sets the status of the isSyncing flag.
*
* @param bool $status
* The status of the sync flag.
*/
public function setSyncing($status);
/**
* Gets the syncing state.
*
* @return bool
* Returns TRUE is syncing flag set.
*/
public function isSyncing();
}

View File

@ -90,6 +90,13 @@ class ConfigManager implements ConfigManagerInterface {
return $this->entityManager;
}
/**
* {@inheritdoc}
*/
public function getConfigFactory() {
return $this->configFactory;
}
/**
* {@inheritdoc}
*/

View File

@ -31,6 +31,14 @@ interface ConfigManagerInterface {
*/
public function getEntityManager();
/**
* Gets the config factory.
*
* @return \Drupal\Core\Config\ConfigFactoryInterface
* The entity manager.
*/
public function getConfigFactory();
/**
* Return a formatted diff of a named config between two storages.
*

View File

@ -6,6 +6,8 @@
*/
namespace Drupal\Core\Config;
use Drupal\Component\Utility\String;
use Drupal\Core\Config\Entity\ConfigDependencyManager;
/**
@ -118,11 +120,23 @@ class StorageComparer implements StorageComparerInterface {
* The change operation performed. Either delete, create or update.
* @param array $changes
* Array of changes to add to the changelist.
* @param array $sort_order
* Array to sort that can be used to sort the changelist. This array must
* contain all the items that are in the change list.
*/
protected function addChangeList($op, array $changes) {
protected function addChangeList($op, array $changes, array $sort_order = NULL) {
// Only add changes that aren't already listed.
$changes = array_diff($changes, $this->changelist[$op]);
$this->changelist[$op] = array_merge($this->changelist[$op], $changes);
if (isset($sort_order)) {
$count = count($this->changelist[$op]);
// Sort the changlist in the same order as the $sort_order array and
// ensure the array is keyed from 0.
$this->changelist[$op] = array_values(array_intersect($sort_order, $this->changelist[$op]));
if ($count != count($this->changelist[$op])) {
throw new \InvalidArgumentException(String::format('Sorting the @op changelist should not change its length.', array('@op' => $op)));
}
}
}
/**
@ -188,8 +202,9 @@ class StorageComparer implements StorageComparerInterface {
if (!empty($recreates)) {
// Recreates should become deletes and creates. Deletes should be ordered
// so that dependencies are deleted first.
$this->addChangeList('create', $recreates);
$this->addChangeList('delete', array_reverse($recreates));
$this->addChangeList('create', $recreates, $this->sourceNames);
$this->addChangeList('delete', $recreates, array_reverse($this->targetNames));
}
}

View File

@ -28,7 +28,7 @@ class ConfigImportSubscriber implements EventSubscriberInterface {
*/
public function onConfigImporterValidate(ConfigImporterEvent $event) {
foreach (array('delete', 'create', 'update') as $op) {
foreach ($event->getConfigImporter()->getUnprocessed($op) as $name) {
foreach ($event->getConfigImporter()->getUnprocessedConfiguration($op) as $name) {
Config::validateName($name);
}
}

View File

@ -582,6 +582,12 @@ class ModuleHandler implements ModuleHandlerInterface {
// Required for module installation checks.
include_once DRUPAL_ROOT . '/core/includes/install.inc';
/** @var \Drupal\Core\Config\ConfigInstaller $config_installer */
$config_installer = \Drupal::service('config.installer');
$sync_status = $config_installer->isSyncing();
if ($sync_status) {
$source_storage = $config_installer->getSourceStorage();
}
$modules_installed = array();
foreach ($module_list as $module) {
$enabled = $extension_config->get("module.$module") !== NULL;
@ -671,6 +677,18 @@ class ModuleHandler implements ModuleHandlerInterface {
}
// Install default configuration of the module.
$config_installer = \Drupal::service('config.installer');
if ($sync_status) {
$config_installer
->setSyncing(TRUE)
->setSourceStorage($source_storage);
}
else {
// If we're not in a config synchronisation reset the source storage
// so that the extension install storage will pick up the new
// configuration.
$config_installer->resetSourceStorage();
}
\Drupal::service('config.installer')->installDefaultConfig('module', $module);
// If the module has no current updates, but has some that were
@ -732,7 +750,7 @@ class ModuleHandler implements ModuleHandlerInterface {
// Skip already uninstalled modules.
if (isset($installed_modules[$dependent]) && !isset($module_list[$dependent]) && $dependent != $profile) {
$module_list[$dependent] = TRUE;
$module_list[$dependent] = $dependent;
}
}
}

View File

@ -146,6 +146,15 @@ class ThemeHandler implements ThemeHandlerInterface {
// Refresh the theme list as installation of default configuration needs
// an updated list to work.
$this->reset();
// The default config installation storage only knows about the currently
// enabled list of themes, so it has to be reset in order to pick up the
// default config of the newly installed theme. However, do not reset the
// source storage when synchronizing configuration, since that would
// needlessly trigger a reload of the whole configuration to be imported.
if (!$this->configInstaller->isSyncing()) {
$this->configInstaller->resetSourceStorage();
}
// Install default configuration of the theme.
$this->configInstaller->installDefaultConfig('theme', $key);
}

View File

@ -8,6 +8,7 @@
use Drupal\block\BlockInterface;
use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\language\Entity\Language;
use Drupal\system\Entity\Menu;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
/**
@ -422,10 +423,12 @@ function block_user_role_delete($role) {
/**
* Implements hook_menu_delete().
*/
function block_menu_delete($menu) {
foreach (entity_load_multiple('block') as $block) {
if ($block->get('plugin') == 'system_menu_block:' . $menu->id()) {
$block->delete();
function block_menu_delete(Menu $menu) {
if (!$menu->isSyncing()) {
foreach (entity_load_multiple('block') as $block) {
if ($block->get('plugin') == 'system_menu_block:' . $menu->id()) {
$block->delete();
}
}
}
}

View File

@ -184,6 +184,14 @@ class CommentManager implements CommentManagerInterface {
))
->save();
// The comment field should be hidden in all other form displays.
foreach ($this->entityManager->getFormModes($entity_type) as $id => $form_mode) {
$display = entity_get_form_display($entity_type, $bundle, $id);
// Only update existing displays.
if ($display && !$display->isNew()) {
$display->removeComponent($field_name)->save();
}
}
// Set default to display comment list.
entity_get_display($entity_type, $bundle, 'default')
->setComponent($field_name, array(
@ -192,6 +200,15 @@ class CommentManager implements CommentManagerInterface {
'weight' => 20,
))
->save();
// The comment field should be hidden in all other view displays.
foreach ($this->entityManager->getViewModes($entity_type) as $id => $view_mode) {
$display = entity_get_display($entity_type, $bundle, $id);
// Only update existing displays.
if ($display && !$display->isNew()) {
$display->removeComponent($field_name)->save();
}
}
}
$this->addBodyField($entity_type, $field_name);
}

View File

@ -7,6 +7,10 @@
namespace Drupal\config\Form;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Config\ConfigManagerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Config\StorageInterface;
@ -72,6 +76,20 @@ class ConfigSync extends FormBase {
*/
protected $typedConfigManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* Constructs the object.
*
@ -89,8 +107,12 @@ class ConfigSync extends FormBase {
* The url generator service.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config
* The typed configuration manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler
*/
public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config) {
public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) {
$this->sourceStorage = $sourceStorage;
$this->targetStorage = $targetStorage;
$this->lock = $lock;
@ -98,6 +120,8 @@ class ConfigSync extends FormBase {
$this->configManager = $config_manager;
$this->urlGenerator = $url_generator;
$this->typedConfigManager = $typed_config;
$this->moduleHandler = $module_handler;
$this->themeHandler = $theme_handler;
}
/**
@ -111,7 +135,9 @@ class ConfigSync extends FormBase {
$container->get('event_dispatcher'),
$container->get('config.manager'),
$container->get('url_generator'),
$container->get('config.typed')
$container->get('config.typed'),
$container->get('module_handler'),
$container->get('theme_handler')
);
}
@ -222,24 +248,27 @@ class ConfigSync extends FormBase {
$this->eventDispatcher,
$this->configManager,
$this->lock,
$this->typedConfigManager
$this->typedConfigManager,
$this->moduleHandler,
$this->themeHandler
);
if ($config_importer->alreadyImporting()) {
drupal_set_message($this->t('Another request may be synchronizing configuration already.'));
}
else{
$config_importer->initialize();
$operations = $config_importer->initialize();
$batch = array(
'operations' => array(
array(array(get_class($this), 'processBatch'), array($config_importer)),
),
'operations' => array(),
'finished' => array(get_class($this), 'finishBatch'),
'title' => t('Synchronizing configuration'),
'init_message' => t('Starting configuration synchronization.'),
'progress_message' => t('Synchronized @current configuration files out of @total.'),
'progress_message' => t('Completed @current step of @total.'),
'error_message' => t('Configuration synchronization has encountered an error.'),
'file' => drupal_get_path('module', 'config') . '/config.admin.inc',
);
foreach ($operations as $operation) {
$batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $operation));
}
batch_set($batch);
}
@ -253,13 +282,13 @@ class ConfigSync extends FormBase {
* @param $context
* The batch context.
*/
public static function processBatch(BatchConfigImporter $config_importer, &$context) {
public static function processBatch(BatchConfigImporter $config_importer, $operation, &$context) {
if (!isset($context['sandbox']['config_importer'])) {
$context['sandbox']['config_importer'] = $config_importer;
}
$config_importer = $context['sandbox']['config_importer'];
$config_importer->processBatch($context);
$config_importer->$operation($context);
}
/**

View File

@ -0,0 +1,112 @@
<?php
/**
* @file
* Contains \Drupal\config\Tests\ConfigImportAllTest.
*/
namespace Drupal\config\Tests;
use Drupal\Core\Config\StorageComparer;
use Drupal\system\Tests\Module\ModuleTestBase;
class ConfigImportAllTest extends ModuleTestBase {
/**
* The profile to install as a basis for testing.
*
* Using the standard profile as this has a lot of additional configuration.
*
* @var string
*/
protected $profile = 'standard';
public static function getInfo() {
return array(
'name' => 'Import configuration from all modules and the standard profile',
'description' => 'Tests the largest configuration import possible with the modules and profiles provided by core.',
'group' => 'Configuration',
);
}
/**
* Tests that a fixed set of modules can be installed and uninstalled.
*/
public function testInstallUninstall() {
// Get a list of modules to enable.
$all_modules = system_rebuild_module_data();
$all_modules = array_filter($all_modules, function ($module) {
// Filter hidden, already enabled modules and modules in the Testing
// package.
if (!empty($module->info['hidden']) || $module->status == TRUE || $module->info['package'] == 'Testing') {
return FALSE;
}
return TRUE;
});
// Install every module possible.
\Drupal::moduleHandler()->install(array_keys($all_modules));
$this->assertModules(array_keys($all_modules), TRUE);
foreach($all_modules as $module => $info) {
$this->assertModuleConfig($module);
$this->assertModuleTablesExist($module);
}
// Export active config to staging
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
system_list_reset();
$this->resetAll();
// Delete every field on the site so all modules can be uninstalled. For
// example, if a comment field exists then module becomes required and can
// not be uninstalled.
$fields = \Drupal::service('field.info')->getFields();
foreach ($fields as $field) {
entity_invoke_bundle_hook('delete', $field->entity_type, $field->entity_type . '__' . $field->name);
$field->delete();
}
// Purge the data.
field_purge_batch(1000);
system_list_reset();
$all_modules = system_rebuild_module_data();
$modules_to_uninstall = array_filter($all_modules, function ($module) {
// Filter required and not enabled modules.
if (!empty($module->info['required']) || $module->status == FALSE) {
return FALSE;
}
return TRUE;
});
$this->assertTrue(isset($modules_to_uninstall['comment']), 'The comment module will be disabled');
// Uninstall all modules that can be uninstalled.
\Drupal::moduleHandler()->uninstall(array_keys($modules_to_uninstall));
$this->assertModules(array_keys($modules_to_uninstall), FALSE);
foreach($modules_to_uninstall as $module => $info) {
$this->assertNoModuleConfig($module);
$this->assertModuleTablesDoNotExist($module);
}
// Import the configuration thereby re-installing all the modules.
$this->configImporter()->import();
// Check that all modules that were uninstalled are now reinstalled.
$this->assertModules(array_keys($modules_to_uninstall), TRUE);
foreach($modules_to_uninstall as $module => $info) {
$this->assertModuleConfig($module);
$this->assertModuleTablesExist($module);
}
// Ensure that we have no configuration changes to import.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.staging'),
$this->container->get('config.storage')
);
$this->assertIdentical($storage_comparer->createChangelist()->getChangelist(), $storage_comparer->getEmptyChangelist());
}
}

View File

@ -45,6 +45,8 @@ class ConfigImportRecreateTest extends DrupalUnitTestBase {
$this->installSchema('system', 'config_snapshot');
$this->installSchema('node', 'node');
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.staging'),
@ -55,9 +57,10 @@ class ConfigImportRecreateTest extends DrupalUnitTestBase {
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed')
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('theme_handler')
);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
public function testRecreateEntity() {
@ -89,21 +92,19 @@ class ConfigImportRecreateTest extends DrupalUnitTestBase {
$this->configImporter->reset();
// A node type, a field, a field instance an entity view display and an
// entity form display will be recreated.
$creates = $this->configImporter->getUnprocessed('create');
$deletes = $this->configImporter->getUnprocessed('delete');
$creates = $this->configImporter->getUnprocessedConfiguration('create');
$deletes = $this->configImporter->getUnprocessedConfiguration('delete');
$this->assertEqual(5, count($creates), 'There are 5 configuration items to create.');
$this->assertEqual(5, count($deletes), 'There are 5 configuration items to delete.');
$this->assertEqual(0, count($this->configImporter->getUnprocessed('update')), 'There are no configuration items to update.');
$this->assertEqual(0, count($this->configImporter->getUnprocessedConfiguration('update')), 'There are no configuration items to update.');
$this->assertIdentical($creates, array_reverse($deletes), 'Deletes and creates contain the same configuration names in opposite orders due to dependencies.');
$this->configImporter->import();
// Verify that there is nothing more to import.
$this->assertFalse($this->configImporter->reset()->hasUnprocessedChanges());
$this->assertFalse($this->configImporter->reset()->hasUnprocessedConfigurationChanges());
$content_type = entity_load('node_type', $type_name);
$this->assertEqual('Node type one', $content_type->label());
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\config\Tests;
use Drupal\Core\Config\InstallStorage;
use Drupal\simpletest\WebTestBase;
/**
@ -14,7 +15,9 @@ use Drupal\simpletest\WebTestBase;
*/
class ConfigImportUITest extends WebTestBase {
public static $modules = array('config', 'config_test');
// Enable the Options and Text modules to ensure dependencies are handled
// correctly.
public static $modules = array('config', 'config_test', 'config_import_test', 'text', 'options');
public static function getInfo() {
return array(
@ -38,6 +41,7 @@ class ConfigImportUITest extends WebTestBase {
function testImport() {
$name = 'system.site';
$dynamic_name = 'config_test.dynamic.new';
/** @var \Drupal\Core\Config\StorageInterface $staging */
$staging = $this->container->get('config.storage.staging');
$this->drupalGet('admin/config/development/configuration');
@ -65,16 +69,61 @@ class ConfigImportUITest extends WebTestBase {
$staging->write($dynamic_name, $original_dynamic_data);
$this->assertIdentical($staging->exists($dynamic_name), TRUE, $dynamic_name . ' found.');
// Enable the Action and Ban modules during import. The Ban
// module is used because it creates a table during the install. The Action
// module is used because it creates a single simple configuration file
// during the install.
$core_extension = \Drupal::config('core.extension')->get();
$core_extension['module']['action'] = 0;
$core_extension['module']['ban'] = 0;
$core_extension['module'] = module_config_sort($core_extension['module']);
$core_extension['theme']['bartik'] = 0;
$staging->write('core.extension', $core_extension);
// Use the install storage so that we can read configuration from modules
// and themes that are not installed.
$install_storage = new InstallStorage();
// Set the Bartik theme as default.
$system_theme = \Drupal::config('system.theme')->get();
$system_theme['default'] = 'bartik';
$staging->write('system.theme', $system_theme);
$staging->write('bartik.settings', $install_storage->read('bartik.settings'));
// Read the action config from module default config folder.
$action_settings = $install_storage->read('action.settings');
$action_settings['recursion_limit'] = 50;
$staging->write('action.settings', $action_settings);
// Uninstall the Options and Text modules to ensure that dependencies are
// handled correctly. Options depends on Text so Text should be installed
// first. Since they were enabled during the test setup the core.extension
// file in staging will already contain them.
\Drupal::moduleHandler()->uninstall(array('text', 'options'));
// Set the state system to record installations and uninstallations.
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_installed', array());
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_uninstalled', array());
// Verify that both appear as ready to import.
$this->drupalGet('admin/config/development/configuration');
$this->assertText($name);
$this->assertText($dynamic_name);
$this->assertText('core.extension');
$this->assertText('system.theme');
$this->assertText('action.settings');
$this->assertText('bartik.settings');
$this->assertFieldById('edit-submit', t('Import all'));
// Import and verify that both do not appear anymore.
$this->drupalPostForm(NULL, array(), t('Import all'));
$this->assertNoText($name);
$this->assertNoText($dynamic_name);
$this->assertNoText('core.extension');
$this->assertNoText('system.theme');
$this->assertNoText('action.settings');
$this->assertNoText('bartik.settings');
$this->assertNoFieldById('edit-submit', t('Import all'));
// Verify that there are no further changes to import.
@ -88,6 +137,85 @@ class ConfigImportUITest extends WebTestBase {
// Verify the cache got cleared.
$this->assertTrue(isset($GLOBALS['hook_cache_flush']));
$this->rebuildContainer();
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('ban'), 'Ban module installed during import.');
$this->assertTrue(\Drupal::database()->schema()->tableExists('ban_ip'), 'The database table ban_ip exists.');
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('action'), 'Action module installed during import.');
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('options'), 'Options module installed during import.');
$this->assertTrue(\Drupal::moduleHandler()->moduleExists('text'), 'Text module installed during import.');
$theme_info = \Drupal::service('theme_handler')->listInfo();
$this->assertTrue($theme_info['bartik']->status, 'Bartik theme enabled during import.');
// Ensure installations and uninstallation occur as expected.
$installed = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_installed', array());
$uninstalled = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_uninstalled', array());
$expected = array('ban', 'action', 'text', 'options');
$this->assertIdentical($expected, $installed, 'Ban, Action, Text and Options modules installed in the correct order.');
$this->assertTrue(empty($uninstalled), 'No modules uninstalled during import');
// Verify that the action.settings configuration object was only written
// once during the import process and only with the value set in the staged
// configuration. This verifies that the module's default configuration is
// used during configuration import and, additionally, that after installing
// a module, that configuration is not synced twice.
$recursion_limit_values = \Drupal::state()->get('ConfigImportUITest.action.settings.recursion_limit', array());
$this->assertIdentical($recursion_limit_values, array(50));
$core_extension = \Drupal::config('core.extension')->get();
unset($core_extension['module']['action']);
unset($core_extension['module']['ban']);
unset($core_extension['module']['options']);
unset($core_extension['module']['text']);
unset($core_extension['theme']['bartik']);
$core_extension['disabled']['theme']['bartik'] = 0;
$staging->write('core.extension', $core_extension);
$staging->delete('action.settings');
$staging->delete('text.settings');
$system_theme = \Drupal::config('system.theme')->get();
$system_theme['default'] = 'stark';
$system_theme['admin'] = 'stark';
$staging->write('system.theme', $system_theme);
// Set the state system to record installations and uninstallations.
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_installed', array());
\Drupal::state()->set('ConfigImportUITest.core.extension.modules_uninstalled', array());
// Verify that both appear as ready to import.
$this->drupalGet('admin/config/development/configuration');
$this->assertText('core.extension');
$this->assertText('system.theme');
$this->assertText('action.settings');
// Import and verify that both do not appear anymore.
$this->drupalPostForm(NULL, array(), t('Import all'));
$this->assertNoText('core.extension');
$this->assertNoText('system.theme');
$this->assertNoText('action.settings');
$this->rebuildContainer();
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('ban'), 'Ban module uninstalled during import.');
$this->assertFalse(\Drupal::database()->schema()->tableExists('ban_ip'), 'The database table ban_ip does not exist.');
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('action'), 'Action module uninstalled during import.');
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('options'), 'Options module uninstalled during import.');
$this->assertFalse(\Drupal::moduleHandler()->moduleExists('text'), 'Text module uninstalled during import.');
// Ensure installations and uninstallation occur as expected.
$installed = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_installed', array());
$uninstalled = \Drupal::state()->get('ConfigImportUITest.core.extension.modules_uninstalled', array());
$expected = array('options', 'text', 'ban', 'action');
$this->assertIdentical($expected, $uninstalled, 'Options, Text, Action and Ban modules uninstalled in the correct order.');
$this->assertTrue(empty($installed), 'No modules installed during import');
$theme_info = \Drupal::service('theme_handler')->listInfo();
$this->assertTrue(isset($theme_info['bartik']) && !$theme_info['bartik']->status, 'Bartik theme disabled during import.');
// Verify that the action.settings configuration object was only deleted
// once during the import process.
$delete_called = \Drupal::state()->get('ConfigImportUITest.action.settings.delete', 0);
$this->assertIdentical($delete_called, 1, "The action.settings configuration was deleted once during configuration import.");
}
/**

View File

@ -50,6 +50,8 @@ class ConfigImporterTest extends DrupalUnitTestBase {
// so it has to be cleared out manually.
unset($GLOBALS['hook_config_test']);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
// Set up the ConfigImporter object for testing.
$storage_comparer = new StorageComparer(
$this->container->get('config.storage.staging'),
@ -60,9 +62,10 @@ class ConfigImporterTest extends DrupalUnitTestBase {
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed')
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('theme_handler')
);
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
/**
@ -145,8 +148,7 @@ class ConfigImporterTest extends DrupalUnitTestBase {
$this->assertTrue(isset($GLOBALS['hook_config_test']['predelete']));
$this->assertTrue(isset($GLOBALS['hook_config_test']['delete']));
// Verify that there is nothing more to import.
$this->assertFalse($this->configImporter->hasUnprocessedChanges());
$this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges());
}
/**
@ -193,7 +195,7 @@ class ConfigImporterTest extends DrupalUnitTestBase {
$this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
// Verify that there is nothing more to import.
$this->assertFalse($this->configImporter->hasUnprocessedChanges());
$this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges());
}
/**
@ -248,7 +250,7 @@ class ConfigImporterTest extends DrupalUnitTestBase {
$this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
// Verify that there is nothing more to import.
$this->assertFalse($this->configImporter->hasUnprocessedChanges());
$this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges());
}
}

View File

@ -32,6 +32,7 @@ class ConfigOverrideTest extends DrupalUnitTestBase {
public function setUp() {
parent::setUp();
$this->installSchema('system', 'config_snapshot');
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
/**

View File

@ -36,6 +36,7 @@ class ConfigSnapshotTest extends DrupalUnitTestBase {
// Update the config snapshot. This allows the parent::setUp() to write
// configuration files.
\Drupal::service('config.manager')->createSnapshot(\Drupal::service('config.storage'), \Drupal::service('config.storage.snapshot'));
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging'));
}
/**

View File

@ -0,0 +1,6 @@
name: 'Configuration import test'
type: module
package: Testing
version: VERSION
core: 8.x
hidden: true

View File

@ -0,0 +1,6 @@
<?php
/**
* @file
* Provides configuration import test helpers.
*/

View File

@ -0,0 +1,6 @@
services:
config_import_test.event_subscriber:
class: Drupal\config_import_test\EventSubscriber
tags:
- { name: event_subscriber }
arguments: ['@state']

View File

@ -0,0 +1,107 @@
<?php
/**
* @file
* Contains \Drupal\config_import_test\EventSubscriber.
*/
namespace Drupal\config_import_test;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Config\ConfigImporterEvent;
use Drupal\Core\KeyValueStore\StateInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Config import subscriber for config import events.
*/
class EventSubscriber implements EventSubscriberInterface {
/**
* The key value store.
*
* @var \Drupal\Core\KeyValueStore\StateInterface
*/
protected $state;
/**
* Constructs the event subscriber.
*
* @param \Drupal\Core\KeyValueStore\StateInterface $state
* The key value store.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* 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) {
}
/**
* Reacts to a config save and records information in state for testing.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
*/
public function onConfigSave(ConfigCrudEvent $event) {
$config = $event->getConfig();
if ($config->getName() == 'action.settings') {
$values = $this->state->get('ConfigImportUITest.action.settings.recursion_limit', array());
$values[] = $config->get('recursion_limit');
$this->state->set('ConfigImportUITest.action.settings.recursion_limit', $values);
}
if ($config->getName() == 'core.extension') {
$installed = $this->state->get('ConfigImportUITest.core.extension.modules_installed', array());
$uninstalled = $this->state->get('ConfigImportUITest.core.extension.modules_uninstalled', array());
$original = $config->getOriginal('module');
$data = $config->get('module');
$install = array_diff_key($data, $original);
if (!empty($install)) {
$installed[] = key($install);
}
$uninstall = array_diff_key($original, $data);
if (!empty($uninstall)) {
$uninstalled[] = key($uninstall);
}
$this->state->set('ConfigImportUITest.core.extension.modules_installed', $installed);
$this->state->set('ConfigImportUITest.core.extension.modules_uninstalled', $uninstalled);
}
}
/**
* Reacts to a config delete and records information in state for testing.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
*/
public function onConfigDelete(ConfigCrudEvent $event) {
$config = $event->getConfig();
if ($config->getName() == 'action.settings') {
$value = $this->state->get('ConfigImportUITest.action.settings.delete', 0);
$this->state->set('ConfigImportUITest.action.settings.delete', $value + 1);
}
}
/**
* Registers the methods in this class that should be listeners.
*
* @return array
* An array of event listener definitions.
*/
static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = array('onConfigSave', 40);
$events[ConfigEvents::DELETE][] = array('onConfigDelete', 40);
return $events;
}
}

View File

@ -15,5 +15,11 @@ function contact_install() {
if (empty($site_mail)) {
$site_mail = ini_get('sendmail_from');
}
\Drupal::config('contact.category.feedback')->set('recipients', array($site_mail))->save();
$config = \Drupal::config('contact.category.feedback');
// Update the recipients if the default configuration entity has been created.
// We should never rely on default config entities as during enabling a module
// during config sync they will not exist.
if (!$config->isNew()) {
\Drupal::config('contact.category.feedback')->set('recipients', array($site_mail))->save();
}
}

View File

@ -88,6 +88,19 @@ function content_translation_install() {
// hook_module_implements_alter() is run among the last ones.
module_set_weight('content_translation', 10);
\Drupal::service('language_negotiator')->saveConfiguration(Language::TYPE_CONTENT, array(LanguageNegotiationUrl::METHOD_ID => 0));
$config_names = \Drupal::configFactory()->listAll('field.field.');
foreach ($config_names as $name) {
\Drupal::config($name)
->set('settings.translation_sync', FALSE)
->save();
}
$config_names = \Drupal::configFactory()->listAll('field.instance.');
foreach ($config_names as $name) {
\Drupal::config($name)
->set('settings.translation_sync', FALSE)
->save();
}
}
/**
@ -105,3 +118,21 @@ function content_translation_enable() {
$message = t('<a href="!settings_url">Enable translation</a> for <em>content types</em>, <em>taxonomy vocabularies</em>, <em>accounts</em>, or any other element you wish to translate.', $t_args);
drupal_set_message($message, 'warning');
}
/**
* Implements hook_uninstall().
*/
function content_translation_uninstall() {
$config_names = \Drupal::configFactory()->listAll('field.field.');
foreach ($config_names as $name) {
\Drupal::config($name)
->clear('settings.translation_sync')
->save();
}
$config_names = \Drupal::configFactory()->listAll('field.instance.');
foreach ($config_names as $name) {
\Drupal::config($name)
->clear('settings.translation_sync')
->save();
}
}

View File

@ -794,4 +794,11 @@ class FieldInstanceConfig extends ConfigEntityBase implements FieldInstanceConfi
return $this->field->hasCustomStorage();
}
/**
* {@inheritdoc}
*/
public function isDeleted() {
return $this->deleted;
}
}

View File

@ -40,4 +40,12 @@ interface FieldInstanceConfigInterface extends ConfigEntityInterface, FieldDefin
*/
public function targetBundle();
/**
* Gets the deleted flag of the field instance.
*
* @return bool
* Returns TRUE if the instance is deleted.
*/
public function isDeleted();
}

View File

@ -15,5 +15,5 @@ settings: { }
field_type: list_boolean
dependencies:
entity:
- field.field.forum.forum_container
- field.field.taxonomy_term.forum_container
- taxonomy.vocabulary.forums

View File

@ -16,72 +16,75 @@ function forum_install() {
$locked['forum'] = 'forum';
\Drupal::state()->set('node.type.locked', $locked);
// Create the 'taxonomy_forums' field if it doesn't already exist. If forum
// is being enabled at the same time as taxonomy after both modules have been
// enabled, the field might exist but still be marked inactive.
if (!field_info_field('node', 'taxonomy_forums')) {
entity_create('field_config', array(
'name' => 'taxonomy_forums',
'entity_type' => 'node',
'type' => 'taxonomy_term_reference',
'settings' => array(
'allowed_values' => array(
array(
'vocabulary' => 'forums',
'parent' => 0,
if (!\Drupal::service('config.installer')->isSyncing()) {
// Create the 'taxonomy_forums' field if it doesn't already exist. If forum
// is being enabled at the same time as taxonomy after both modules have been
// enabled, the field might exist but still be marked inactive.
if (!field_info_field('node', 'taxonomy_forums')) {
entity_create('field_config', array(
'name' => 'taxonomy_forums',
'entity_type' => 'node',
'type' => 'taxonomy_term_reference',
'settings' => array(
'allowed_values' => array(
array(
'vocabulary' => 'forums',
'parent' => 0,
),
),
),
),
))->save();
))->save();
// Create a default forum so forum posts can be created.
$term = entity_create('taxonomy_term', array(
'name' => t('General discussion'),
'description' => '',
'parent' => array(0),
'vid' => 'forums',
'forum_container' => 0,
// Create a default forum so forum posts can be created.
$term = entity_create('taxonomy_term', array(
'name' => t('General discussion'),
'description' => '',
'parent' => array(0),
'vid' => 'forums',
'forum_container' => 0,
));
$term->save();
// Create the instance on the bundle.
entity_create('field_instance_config', array(
'field_name' => 'taxonomy_forums',
'entity_type' => 'node',
'label' => 'Forums',
'bundle' => 'forum',
'required' => TRUE,
))->save();
// Assign form display settings for the 'default' form mode.
entity_get_form_display('node', 'forum', 'default')
->setComponent('taxonomy_forums', array(
'type' => 'options_select',
))
->save();
// Assign display settings for the 'default' and 'teaser' view modes.
entity_get_display('node', 'forum', 'default')
->setComponent('taxonomy_forums', array(
'type' => 'taxonomy_term_reference_link',
'weight' => 10,
))
->save();
entity_get_display('node', 'forum', 'teaser')
->setComponent('taxonomy_forums', array(
'type' => 'taxonomy_term_reference_link',
'weight' => 10,
))
->save();
}
// Add the comment field to the forum node type.
$fields = entity_load_multiple_by_properties('field_config', array(
'type' => 'comment',
'name' => 'comment_forum',
'include_deleted' => FALSE,
));
$term->save();
// Create the instance on the bundle.
entity_create('field_instance_config', array(
'field_name' => 'taxonomy_forums',
'entity_type' => 'node',
'label' => 'Forums',
'bundle' => 'forum',
'required' => TRUE,
))->save();
// Assign form display settings for the 'default' form mode.
entity_get_form_display('node', 'forum', 'default')
->setComponent('taxonomy_forums', array(
'type' => 'options_select',
))
->save();
// Assign display settings for the 'default' and 'teaser' view modes.
entity_get_display('node', 'forum', 'default')
->setComponent('taxonomy_forums', array(
'type' => 'taxonomy_term_reference_link',
'weight' => 10,
))
->save();
entity_get_display('node', 'forum', 'teaser')
->setComponent('taxonomy_forums', array(
'type' => 'taxonomy_term_reference_link',
'weight' => 10,
))
->save();
}
// Add the comment field to the forum node type.
$fields = entity_load_multiple_by_properties('field_config', array(
'type' => 'comment',
'name' => 'comment_forum',
'include_deleted' => FALSE,
));
if (empty($fields)) {
Drupal::service('comment.manager')->addDefaultField('node', 'forum', 'comment_forum');
if (empty($fields)) {
Drupal::service('comment.manager')->addDefaultField('node', 'forum', 'comment_forum');
}
}
}

View File

@ -459,7 +459,9 @@ function node_uninstall() {
$types = \Drupal::configFactory()->listAll('node.type.');
foreach ($types as $config_name) {
$type = \Drupal::config($config_name)->get('type');
\Drupal::config('language.settings')->clear('node. ' . $type . '.language.default_configuration')->save();
if (\Drupal::moduleHandler()->moduleExists('language')) {
\Drupal::config('language.settings')->clear('node. ' . $type . '.language.default_configuration')->save();
}
}
// Delete remaining general module variables.

View File

@ -87,7 +87,7 @@ class SearchPageRepository implements SearchPageRepositoryInterface {
}
// Otherwise, use the first active search page.
return reset($search_pages);
return is_array($search_pages) ? reset($search_pages) : FALSE;
}
/**

View File

@ -148,7 +148,7 @@ abstract class DrupalUnitTestBase extends UnitTestBase {
// \Drupal\Core\Config\ConfigInstaller::installDefaultConfig() to work.
// Write directly to active storage to avoid early instantiation of
// the event dispatcher which can prevent modules from registering events.
\Drupal::service('config.storage')->write('core.extension', array('module' => array()));
\Drupal::service('config.storage')->write('core.extension', array('module' => array(), 'theme' => array()));
// Collect and set a fixed module list.
$class = get_class($this);

View File

@ -1526,7 +1526,9 @@ abstract class TestBase {
$this->container->get('event_dispatcher'),
$this->container->get('config.manager'),
$this->container->get('lock'),
$this->container->get('config.typed')
$this->container->get('config.typed'),
$this->container->get('module_handler'),
$this->container->get('theme_handler')
);
}
// Always recalculate the changelist when called.

View File

@ -182,7 +182,7 @@ function simpletest_uninstall() {
// Do not clean the environment in case the Simpletest module is uninstalled
// in a (recursive) test for itself, since simpletest_clean_environment()
// would also delete the test site of the parent test process.
if (!DRUPAL_TEST_IN_CHILD_SITE) {
if (!drupal_valid_test_ua()) {
simpletest_clean_environment();
}
// Delete verbose test output and any other testing framework files.

View File

@ -4,6 +4,13 @@ bundle: article
mode: default
status: true
content:
field_image:
label: hidden
type: image
settings:
image_style: large
image_link: ''
weight: -1
body:
label: hidden
type: text_default
@ -14,13 +21,6 @@ content:
weight: 10
label: above
settings: { }
field_image:
label: hidden
type: image
settings:
image_style: large
image_link: ''
weight: -1
dependencies:
entity:
- field.instance.node.article.body

View File

@ -4,6 +4,13 @@ bundle: article
mode: teaser
status: true
content:
field_image:
label: hidden
type: image
settings:
image_style: medium
image_link: content
weight: -1
body:
label: hidden
type: text_summary_or_trimmed
@ -15,13 +22,6 @@ content:
weight: 10
label: above
settings: { }
field_image:
label: hidden
type: image
settings:
image_style: medium
image_link: content
weight: -1
dependencies:
entity:
- entity.view_mode.node.teaser