Commit ae702dc5 authored by catch's avatar catch

Issue #1808248 by alexpott, beejeebus, tayzlor, Nitesh Sethia: Add a separate...

Issue #1808248 by alexpott, beejeebus, tayzlor, Nitesh Sethia: Add a separate module install/uninstall step to the config import process.
parent 8866760f
...@@ -612,4 +612,14 @@ public static function formBuilder() { ...@@ -612,4 +612,14 @@ public static function formBuilder() {
return static::$container->get('form_builder'); 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();
}
} }
...@@ -14,10 +14,34 @@ ...@@ -14,10 +14,34 @@
*/ */
class BatchConfigImporter extends ConfigImporter { 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. * 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() { public function initialize() {
$batch_operations = array();
$this->createExtensionChangelist();
// Ensure that the changes have been validated. // Ensure that the changes have been validated.
$this->validate(); $this->validate();
...@@ -25,61 +49,137 @@ public function initialize() { ...@@ -25,61 +49,137 @@ public function initialize() {
// Another process is synchronizing configuration. // Another process is synchronizing configuration.
throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID)); throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID));
} }
$this->totalToProcess = 0;
foreach(array('create', 'delete', 'update') as $op) { $modules = $this->getUnprocessedExtensions('module');
$this->totalToProcess += count($this->getUnprocessed($op)); 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. * @param array $context.
* The batch context. * The batch context.
*/ */
public function processBatch(array &$context) { public function processExtensionBatch(array &$context) {
$operation = $this->getNextOperation(); $operation = $this->getNextExtensionOperation();
if (!empty($operation)) { if (!empty($operation)) {
$this->process($operation['op'], $operation['name']); $this->processExtension($operation['type'], $operation['op'], $operation['name']);
$context['message'] = t('Synchronizing @name.', array('@name' => $operation['name'])); $context['message'] = t('Synchronising extensions: @op @name.', array('@op' => $operation['op'], '@name' => $operation['name']));
$context['finished'] = $this->batchProgress(); $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 { else {
$context['finished'] = 1; $context['finished'] = 1;
} }
if ($context['finished'] >= 1) { }
/**
* 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;
}
}
/**
* Finishes the batch.
*
* @param array $context.
* The batch context.
*/
public function finishBatch(array &$context) {
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this)); $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
// The import is now complete. // The import is now complete.
$this->lock->release(static::LOCK_ID); $this->lock->release(static::LOCK_ID);
$this->reset(); $this->reset();
} $context['message'] = t('Finalising configuration synchronisation.');
$context['finished'] = 1;
} }
/** /**
* Gets percentage of progress made. * Gets the next extension operation to perform.
* *
* @return float * @return array|bool
* The percentage of progress made expressed as a float between 0 and 1. * An array containing the next operation and extension name to perform it
* on. If there is nothing left to do returns FALSE;
*/ */
protected function batchProgress() { protected function getNextExtensionOperation() {
$processed_count = count($this->processed['create']) + count($this->processed['delete']) + count($this->processed['update']); foreach (array('install', 'uninstall') as $op) {
return $processed_count / $this->totalToProcess; $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 operation to perform. * Gets the next configuration operation to perform.
* *
* @return array|bool * @return array|bool
* An array containing the next operation and configuration name to perform * An array containing the next operation and configuration name to perform
* it on. If there is nothing left to do returns FALSE; * it on. If there is nothing left to do returns FALSE;
*/ */
protected function getNextOperation() { protected function getNextConfigurationOperation() {
foreach(array('create', 'delete', 'update') as $op) { // The order configuration operations is processed is important. Deletes
$names = $this->getUnprocessed($op); // have to come first so that recreates can work.
if (!empty($names)) { foreach (array('delete', 'create', 'update') as $op) {
$config_names = $this->getUnprocessedConfiguration($op);
if (!empty($config_names)) {
return array( return array(
'op' => $op, 'op' => $op,
'name' => array_shift($names), 'name' => array_shift($config_names),
); );
} }
} }
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
namespace Drupal\Core\Config; namespace Drupal\Core\Config;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Component\Utility\String; use Drupal\Component\Utility\String;
use Drupal\Core\Config\Entity\ImportableEntityStorageInterface; use Drupal\Core\Config\Entity\ImportableEntityStorageInterface;
use Drupal\Core\DependencyInjection\DependencySerialization; use Drupal\Core\DependencyInjection\DependencySerialization;
...@@ -71,16 +73,30 @@ class ConfigImporter extends DependencySerialization { ...@@ -71,16 +73,30 @@ class ConfigImporter extends DependencySerialization {
/** /**
* The typed config manager. * The typed config manager.
* *
* @var \Drupal\Core\Config\TypedConfigManager * @var \Drupal\Core\Config\TypedConfigManagerInterface
*/ */
protected $typedConfigManager; protected $typedConfigManager;
/** /**
* List of changes processed by the import(). * List of configuration file changes processed by the import().
* *
* @var array * @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. * Indicates changes to import have been validated.
...@@ -89,6 +105,27 @@ class ConfigImporter extends DependencySerialization { ...@@ -89,6 +105,27 @@ class ConfigImporter extends DependencySerialization {
*/ */
protected $validated; 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. * Constructs a configuration import object.
* *
...@@ -103,14 +140,21 @@ class ConfigImporter extends DependencySerialization { ...@@ -103,14 +140,21 @@ class ConfigImporter extends DependencySerialization {
* The lock backend to ensure multiple imports do not occur at the same time. * The lock backend to ensure multiple imports do not occur at the same time.
* @param \Drupal\Core\Config\TypedConfigManager $typed_config * @param \Drupal\Core\Config\TypedConfigManager $typed_config
* The typed configuration manager. * 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->storageComparer = $storage_comparer;
$this->eventDispatcher = $event_dispatcher; $this->eventDispatcher = $event_dispatcher;
$this->configManager = $config_manager; $this->configManager = $config_manager;
$this->lock = $lock; $this->lock = $lock;
$this->typedConfigManager = $typed_config; $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 @@ public function getStorageComparer() { ...@@ -131,13 +175,35 @@ public function getStorageComparer() {
*/ */
public function reset() { public function reset() {
$this->storageComparer->reset(); $this->storageComparer->reset();
$this->processed = $this->storageComparer->getEmptyChangelist(); $this->processedConfiguration = $this->storageComparer->getEmptyChangelist();
$this->processedExtensions = $this->getEmptyExtensionsProcessedList();
$this->createExtensionChangelist();
$this->validated = FALSE; $this->validated = FALSE;
$this->processedSystemTheme = FALSE;
return $this; 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 * @param array $ops
* The operations to check for changes. Defaults to all operations, i.e. * The operations to check for changes. Defaults to all operations, i.e.
...@@ -146,9 +212,9 @@ public function reset() { ...@@ -146,9 +212,9 @@ public function reset() {
* @return bool * @return bool
* TRUE if there are changes to process and FALSE if not. * 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) { foreach ($ops as $op) {
if (count($this->getUnprocessed($op))) { if (count($this->getUnprocessedConfiguration($op))) {
return TRUE; return TRUE;
} }
} }
...@@ -161,8 +227,8 @@ public function hasUnprocessedChanges($ops = array('delete', 'create', 'update') ...@@ -161,8 +227,8 @@ public function hasUnprocessedChanges($ops = array('delete', 'create', 'update')
* @return array * @return array
* An array containing a list of processed changes. * An array containing a list of processed changes.
*/ */
public function getProcessed() { public function getProcessedConfiguration() {
return $this->processed; return $this->processedConfiguration;
} }
/** /**
...@@ -173,8 +239,8 @@ public function getProcessed() { ...@@ -173,8 +239,8 @@ public function getProcessed() {
* @param string $name * @param string $name
* The name of the configuration processed. * The name of the configuration processed.
*/ */
protected function setProcessed($op, $name) { protected function setProcessedConfiguration($op, $name) {
$this->processed[$op][] = $name; $this->processedConfiguration[$op][] = $name;
} }
/** /**
...@@ -187,8 +253,139 @@ protected function setProcessed($op, $name) { ...@@ -187,8 +253,139 @@ protected function setProcessed($op, $name) {
* @return array * @return array
* An array of configuration names. * An array of configuration names.
*/ */
public function getUnprocessed($op) { public function getUnprocessedConfiguration($op) {
return array_diff($this->storageComparer->getChangelist($op), $this->processed[$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']),
);
}