diff --git a/core/lib/Drupal/Core/Config/BatchConfigImporter.php b/core/lib/Drupal/Core/Config/BatchConfigImporter.php deleted file mode 100644 index a57a076..0000000 --- a/core/lib/Drupal/Core/Config/BatchConfigImporter.php +++ /dev/null @@ -1,190 +0,0 @@ -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; - } -} diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 9ddbfb0..51e6b93 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -142,6 +142,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. * * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer @@ -436,36 +450,206 @@ public function getUnprocessedExtensions($type) { */ public function import() { if ($this->hasUnprocessedConfigurationChanges()) { - $this->createExtensionChangelist(); - - // Ensure that the changes have been validated. - $this->validate(); + $sync_steps = $this->initialize(); - if (!$this->lock->acquire(static::LOCK_ID)) { - // Another process is synchronizing configuration. - throw new ConfigImporterException(sprintf('%s is already importing', static::LOCK_ID)); + 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 $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 ConfigImporterException + * Exception thrown if the $operation 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 ConfigImporterException(String::format('Operation %operation can not be called,', array('%operation', $sync_step))); + } + } + + /** + * Initializes the config importer in preparation for processing a batch. + * + * @return array + * An array of method names that to be called to complete the import. 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() { + $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)); + } + + $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]); + } - // Process any extension changes before importing configuration. - $this->handleExtensions(); + // We have extensions to process. + if ($this->totalExtensionsToProcess > 0) { + $sync_steps[] = 'processExtensions'; + } + $sync_steps[] = 'processConfigurations'; + + $presync = $this->moduleHandler->invokeAll('config_import_presync_steps'); + $postsync = $this->moduleHandler->invokeAll('config_import_postsync_steps'); + + return array_merge($presync, $sync_steps, $postsync, array('finish')); + } - // First pass deleted, then new, and lastly changed configuration, in order - // to handle dependencies correctly. + /** + * 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; + } + } + + /** + * 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; + } - // The import is now complete. - $this->lock->release(static::LOCK_ID); - $this->reset(); + /** + * 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]), + ); + } } - return $this; + 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; } /** @@ -712,50 +896,6 @@ public function getId() { } /** - * 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 diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index 907340c..6c81152 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -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(); diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php index 7597659..75126c4 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -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,12 @@ function testNew() { $this->assertFalse(isset($GLOBALS['hook_config_test']['predelete'])); $this->assertFalse(isset($GLOBALS['hook_config_test']['delete'])); + // Verify that hook_config_import_presync_steps() and + // hook_config_import_postsync_steps() can add steps to configuration + // synchronization. + $this->assertTrue(isset($GLOBALS['hook_config_test']['config_import_presync_steps'])); + $this->assertTrue(isset($GLOBALS['hook_config_test']['config_import_postsync_steps'])); + // Verify that there is nothing more to import. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); $logs = $this->configImporter->getErrors(); diff --git a/core/modules/config/tests/config_import_test/config_import_test.module b/core/modules/config/tests/config_import_test/config_import_test.module index 936b72b..d64d741 100644 --- a/core/modules/config/tests/config_import_test/config_import_test.module +++ b/core/modules/config/tests/config_import_test/config_import_test.module @@ -4,3 +4,41 @@ * @file * Provides configuration import test helpers. */ + +/** + * Implements hook_config_import_presync_steps(). + */ +function config_import_test_config_import_presync_steps() { + return array('_config_import_test_config_import_presync_step'); +} + +/** + * Implements hook_config_import_postsync_steps(). + */ +function config_import_test_config_import_postsync_steps() { + return array('_config_import_test_config_import_postsync_step'); +} + +/** + * Implements pre configuration synchronization step for testing. + * + * @param array $context + * The batch context. + */ +function _config_import_test_config_import_presync_step(&$context) { + $GLOBALS['hook_config_test']['config_import_presync_steps'] = TRUE; + $context['finished'] = 1; + return; +} + +/** + * Implements post configuration synchronization step for testing. + * + * @param array $context + * The batch context. + */ +function _config_import_test_config_import_postsync_step(&$context) { + $GLOBALS['hook_config_test']['config_import_postsync_steps'] = TRUE; + $context['finished'] = 1; + return; +}