Commit 0a93d9a1 authored by webchick's avatar webchick

Issue #2238069 by alexpott: Create a way to add steps to configuration sync.

parent e562ca4d
<?php
/**
* @file
* Contains \Drupal\Core\Config\BatchConfigImporter.
*/
namespace Drupal\Core\Config;
/**
* Defines a batch configuration importer.
*
* @see \Drupal\Core\Config\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.
*
* @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();
if (!$this->lock->acquire(static::LOCK_ID)) {
// Another process is synchronizing configuration.
throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID));
}
$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 extensions as a batch operation.
*
* @param array $context.
* The batch context.
*/
public function processExtensionBatch(array &$context) {
$operation = $this->getNextExtensionOperation();
if (!empty($operation)) {
$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;
}
}
/**
* 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)) {
if ($this->checkOp($operation['op'], $operation['name'])) {
$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));
// 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 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 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($config_names),
);
}
}
return FALSE;
}
}
......@@ -141,6 +141,20 @@ class ConfigImporter extends DependencySerialization {
*/
protected $errors = array();
/**
* 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;
/**
* Constructs a configuration import object.
*
......@@ -436,6 +450,56 @@ public function getUnprocessedExtensions($type) {
*/
public function import() {
if ($this->hasUnprocessedConfigurationChanges()) {
$sync_steps = $this->initialize();
foreach ($sync_steps as $step) {
$context = array();
do {
$this->doSyncStep($step, $context);
} while ($context['finished'] < 1);
}
}
return $this;
}
/**
* Calls a config import step.
*
* @param string|callable $sync_step
* The step to do. Either a method on the ConfigImporter class or a
* callable.
* @param array $context
* A batch context array. If the config importer is not running in a batch
* the only array key that is used is $context['finished']. A process needs
* to set $context['finished'] = 1 when it is done.
*
* @throws \InvalidArgumentException
* Exception thrown if the $sync_step can not be called.
*/
public function doSyncStep($sync_step, &$context) {
if (method_exists($this, $sync_step)) {
$this->$sync_step($context);
}
elseif (is_callable($sync_step)) {
call_user_func_array($sync_step, array(&$context));
}
else {
throw new \InvalidArgumentException('Invalid configuration synchronization step');
}
}
/**
* Initializes the config importer in preparation for processing a batch.
*
* @return array
* An array of \Drupal\Core\Config\ConfigImporter method names and callables
* that are invoked to complete the import. If there are modules or themes
* to process then an extra step is added.
*
* @throws \Drupal\Core\Config\ConfigImporterException
* If the configuration is already importing.
*/
public function initialize() {
$this->createExtensionChangelist();
// Ensure that the changes have been validated.
......@@ -446,26 +510,147 @@ public function import() {
throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID));
}
// Process any extension changes before importing configuration.
$this->handleExtensions();
$sync_steps = array();
$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) {
$sync_steps[] = 'processExtensions';
}
$sync_steps[] = 'processConfigurations';
// Allow modules to add new steps to configuration synchronization.
$this->moduleHandler->alter('config_import_steps', $sync_steps);
$sync_steps[] = 'finish';
return $sync_steps;
}
/**
* Processes extensions as a batch operation.
*
* @param array $context.
* The batch context.
*/
public function processExtensions(array &$context) {
$operation = $this->getNextExtensionOperation();
if (!empty($operation)) {
$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;
}
}
// First pass deleted, then new, and lastly changed configuration, in order
// to handle dependencies correctly.
/**
* Processes configuration as a batch operation.
*
* @param array $context.
* The batch context.
*/
public function processConfigurations(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) {
foreach ($this->getUnprocessedConfiguration($op) as $name) {
if ($this->checkOp($op, $name)) {
$this->processConfiguration($op, $name);
$this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op));
}
}
}
// Allow modules to react to a import.
$this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this));
$operation = $this->getNextConfigurationOperation();
if (!empty($operation)) {
if ($this->checkOp($operation['op'], $operation['name'])) {
$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 finish(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 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 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($config_names),
);
}
return $this;
}
return FALSE;
}
/**
......@@ -711,50 +896,6 @@ 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.
*
......
......@@ -8,6 +8,7 @@
namespace Drupal\config\Form;
use Drupal\Component\Uuid\UuidInterface;
use Drupal\Core\Config\ConfigImporter;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
......@@ -15,7 +16,6 @@
use Drupal\Core\Form\FormBase;
use Drupal\Core\Config\StorageInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Config\BatchConfigImporter;
use Drupal\Core\Config\StorageComparer;
use Drupal\Core\Config\TypedConfigManager;
use Drupal\Core\Routing\UrlGeneratorInterface;
......@@ -243,7 +243,7 @@ public function buildForm(array $form, array &$form_state) {
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state) {
$config_importer = new BatchConfigImporter(
$config_importer = new ConfigImporter(
$form_state['storage_comparer'],
$this->eventDispatcher,
$this->configManager,
......@@ -257,7 +257,7 @@ public function submitForm(array &$form, array &$form_state) {
drupal_set_message($this->t('Another request may be synchronizing configuration already.'));
}
else{
$operations = $config_importer->initialize();
$sync_steps = $config_importer->initialize();
$batch = array(
'operations' => array(),
'finished' => array(get_class($this), 'finishBatch'),
......@@ -267,8 +267,8 @@ public function submitForm(array &$form, array &$form_state) {
'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));
foreach ($sync_steps as $sync_step) {
$batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $sync_step));
}
batch_set($batch);
......@@ -278,18 +278,20 @@ public function submitForm(array &$form, array &$form_state) {
/**
* Processes the config import batch and persists the importer.
*
* @param BatchConfigImporter $config_importer
* @param \Drupal\Core\Config\ConfigImporter $config_importer
* The batch config importer object to persist.
* @param string $sync_step
* The synchronisation step to do.
* @param $context
* The batch context.
*/
public static function processBatch(BatchConfigImporter $config_importer, $operation, &$context) {
public static function processBatch(ConfigImporter $config_importer, $sync_step, &$context) {
if (!isset($context['sandbox']['config_importer'])) {
$context['sandbox']['config_importer'] = $config_importer;
}
$config_importer = $context['sandbox']['config_importer'];
$config_importer->$operation($context);
$config_importer->doSyncStep($sync_step, $context);
if ($errors = $config_importer->getErrors()) {
if (!isset($context['results']['errors'])) {
$context['results']['errors'] = array();
......
......@@ -30,7 +30,7 @@ class ConfigImporterTest extends DrupalUnitTestBase {
*
* @var array
*/
public static $modules = array('config_test', 'system');
public static $modules = array('config_test', 'system', 'config_import_test');
public static function getInfo() {
return array(
......@@ -198,6 +198,10 @@ function testNew() {
$this->assertFalse(isset($GLOBALS['hook_config_test']['predelete']));
$this->assertFalse(isset($GLOBALS['hook_config_test']['delete']));
// Verify that hook_config_import_steps_alter() can add steps to
// configuration synchronization.
$this->assertTrue(isset($GLOBALS['hook_config_test']['config_import_steps_alter']));
// Verify that there is nothing more to import.
$this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges());
$logs = $this->configImporter->getErrors();
......
......@@ -4,3 +4,22 @@
* @file
* Provides configuration import test helpers.
*/
/**
* Implements hook_config_import_steps_alter().
*/
function config_import_test_config_import_steps_alter(&$sync_steps) {
$sync_steps[] = '_config_import_test_config_import_steps_alter';
}
/**
* Implements configuration synchronization step added by an alter for testing.
*
* @param array $context
* The batch context.
*/
function _config_import_test_config_import_steps_alter(&$context) {
$GLOBALS['hook_config_test']['config_import_steps_alter'] = TRUE;
$context['finished'] = 1;
return;
}
......@@ -2878,6 +2878,31 @@ function hook_link_alter(&$variables) {
}
}
/**
* Alter the configuration synchronization steps.
*
* @param array $sync_steps
* A one-dimensional array of \Drupal\Core\Config\ConfigImporter method names
* or callables that are invoked to complete the import, in the order that
* they will be processed. Each callable item defined in $sync_steps should
* either be a global function or a public static method. The callable should
* accept a $context array by reference. For example:
* <code>
* function _additional_configuration_step(&$context) {
* // Do stuff.
* // If finished set $context['finished'] = 1.
* }
* </code>
* For more information on creating batches, see the
* @link batch Batch operations @endlink documentation.
*
* @see callback_batch_operation()
* @see \Drupal\Core\Config\ConfigImporter::initialize()
*/
function hook_config_import_steps_alter(&$sync_steps) {
$sync_steps[] = '_additional_configuration_step';
}
/**
* @} End of "addtogroup hooks".
*/
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment