diff --git a/core/lib/Drupal/Core/Config/BatchConfigImporter.php b/core/lib/Drupal/Core/Config/BatchConfigImporter.php new file mode 100644 index 00000000000..a57a0764c20 --- /dev/null +++ b/core/lib/Drupal/Core/Config/BatchConfigImporter.php @@ -0,0 +1,190 @@ +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 28dd74ed8b2..9ddbfb04dc8 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -141,20 +141,6 @@ 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. * @@ -450,207 +436,36 @@ class ConfigImporter extends DependencySerialization { */ public function import() { if ($this->hasUnprocessedConfigurationChanges()) { - $sync_steps = $this->initialize(); + $this->createExtensionChangelist(); - foreach ($sync_steps as $step) { - $context = array(); - do { - $this->doSyncStep($step, $context); - } while ($context['finished'] < 1); + // 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)); } - } - 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'); - } - } + // Process any extension changes before importing configuration. + $this->handleExtensions(); - /** - * 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. - $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]); - } - - // 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; - } - } - - /** - * 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(); + // First pass deleted, then new, and lastly changed configuration, in order + // to handle dependencies correctly. foreach (array('delete', 'create', 'update') as $op) { foreach ($this->getUnprocessedConfiguration($op) as $name) { - $this->totalConfigurationToProcess += count($this->getUnprocessedConfiguration($op)); + if ($this->checkOp($op, $name)) { + $this->processConfiguration($op, $name); + } } } - } - $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; - } - } + // Allow modules to react to a import. + $this->eventDispatcher->dispatch(ConfigEvents::IMPORT, new ConfigImporterEvent($this)); - /** - * 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]), - ); - } + // The import is now complete. + $this->lock->release(static::LOCK_ID); + $this->reset(); } - 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; + return $this; } /** @@ -896,6 +711,50 @@ class ConfigImporter extends DependencySerialization { 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. * diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index 6c81152abd6..907340cc749 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -8,7 +8,6 @@ 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; @@ -16,6 +15,7 @@ use Drupal\Core\Config\ConfigManagerInterface; 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 @@ class ConfigSync extends FormBase { * {@inheritdoc} */ public function submitForm(array &$form, array &$form_state) { - $config_importer = new ConfigImporter( + $config_importer = new BatchConfigImporter( $form_state['storage_comparer'], $this->eventDispatcher, $this->configManager, @@ -257,7 +257,7 @@ class ConfigSync extends FormBase { drupal_set_message($this->t('Another request may be synchronizing configuration already.')); } else{ - $sync_steps = $config_importer->initialize(); + $operations = $config_importer->initialize(); $batch = array( 'operations' => array(), 'finished' => array(get_class($this), 'finishBatch'), @@ -267,8 +267,8 @@ class ConfigSync extends FormBase { 'error_message' => t('Configuration synchronization has encountered an error.'), 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', ); - foreach ($sync_steps as $sync_step) { - $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $sync_step)); + foreach ($operations as $operation) { + $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $operation)); } batch_set($batch); @@ -278,20 +278,18 @@ class ConfigSync extends FormBase { /** * Processes the config import batch and persists the importer. * - * @param \Drupal\Core\Config\ConfigImporter $config_importer + * @param BatchConfigImporter $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(ConfigImporter $config_importer, $sync_step, &$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->doSyncStep($sync_step, $context); + $config_importer->$operation($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 37875b80f12..759765949fb 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', 'config_import_test'); + public static $modules = array('config_test', 'system'); public static function getInfo() { return array( @@ -198,10 +198,6 @@ class ConfigImporterTest extends DrupalUnitTestBase { $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(); 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 6ec1d93d816..936b72b74b1 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,22 +4,3 @@ * @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; -} diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 146fe2f4c49..550cd2888f7 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -2878,31 +2878,6 @@ 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: - * - * function _additional_configuration_step(&$context) { - * // Do stuff. - * // If finished set $context['finished'] = 1. - * } - * - * 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". */