diff --git a/core/lib/Drupal/Core/Config/BatchConfigImporter.php b/core/lib/Drupal/Core/Config/BatchConfigImporter.php new file mode 100644 index 0000000..d6a858f --- /dev/null +++ b/core/lib/Drupal/Core/Config/BatchConfigImporter.php @@ -0,0 +1,88 @@ +validate(); + + if (!$this->lock->acquire(static::ID)) { + // Another process is synchronizing configuration. + throw new ConfigImporterException(sprintf('%s is already importing', static::ID)); + } + $this->totalToProcess = 0; + foreach(array('create', 'delete', 'update') as $op) { + $this->totalToProcess += count($this->getUnprocessed($op)); + } + } + + /** + * Processes batch. + * + * @param array $context. + * The batch context. + */ + public function processBatch(array &$context) { + $operation = $this->getNextOperation(); + if (!empty($operation)) { + $this->process($operation['op'], $operation['name']); + $context['message'] = t('Synchronizing @name.', array('@name' => $operation['name'])); + $context['finished'] = $this->batchProgress(); + } + else { + $context['finished'] = 1; + } + if ($context['finished'] >= 1) { + $this->notify('import'); + // The import is now complete. + $this->lock->release(static::ID); + $this->reset(); + } + } + + /** + * Gets percentage of progress made. + * + * @return float + * The percentage of progress made expressed as a float between 0 and 1. + */ + protected function batchProgress() { + $processed_count = count($this->processed['create']) + count($this->processed['delete']) + count($this->processed['update']); + return $processed_count / $this->totalToProcess; + } + + /** + * Gets the next 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)) { + return array( + 'op' => $op, + 'name' => array_shift($names), + ); + } + } + return FALSE; + } +} diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index d159034..b91964e 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -7,6 +7,7 @@ namespace Drupal\Core\Config; +use Drupal\Core\DependencyInjection\DependencySerialization; use Drupal\Core\Lock\LockBackendInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -29,7 +30,7 @@ * * @see \Drupal\Core\Config\ConfigImporterEvent */ -class ConfigImporter { +class ConfigImporter extends DependencySerialization { /** * The name used to identify events and the lock. @@ -204,8 +205,15 @@ public function import() { // Another process is synchronizing configuration. throw new ConfigImporterException(sprintf('%s is already importing', static::ID)); } - $this->importInvokeOwner(); - $this->importConfig(); + // 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); + } + } // Allow modules to react to a import. $this->notify('import'); @@ -234,63 +242,84 @@ public function validate() { } /** - * Writes an array of config changes from the source to the target storage. + * Processes a configuration change. + * + * @param string $op + * The change operation. + * @param string $name + * The name of the configuration to process. */ - protected function importConfig() { - foreach (array('delete', 'create', 'update') as $op) { - foreach ($this->getUnprocessed($op) as $name) { - $config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); - if ($op == 'delete') { - $config->delete(); - } - else { - $data = $this->storageComparer->getSourceStorage()->read($name); - $config->setData($data ? $data : array()); - $config->save(); - } - $this->setProcessed($op, $name); - } + protected function process($op, $name) { + if (!$this->importInvokeOwner($op, $name)) { + $this->importConfig($op, $name); } } /** + * Writes a configuration change from the source to the target storage. + * + * @param string $op + * The change operation. + * @param string $name + * The name of the configuration to process. + */ + protected function importConfig($op, $name) { + $config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + if ($op == 'delete') { + $config->delete(); + } + else { + $data = $this->storageComparer->getSourceStorage()->read($name); + $config->setData($data ? $data : array()); + $config->save(); + } + $this->setProcessed($op, $name); + } + + /** * Invokes import* methods on configuration entity storage controllers. * * Allow modules to take over configuration change operations for higher-level * configuration data. * * @todo Add support for other extension types; e.g., themes etc. + * + * @param string $op + * The change operation to get the unprocessed list for, either delete, + * create or update. + * @param string $name + * The name of the configuration to process. + * + * @return bool + * TRUE if the configuration was imported as a configuration entity. FALSE + * otherwise. */ - protected function importInvokeOwner() { - // First pass deleted, then new, and lastly changed configuration, in order - // to handle dependencies correctly. - foreach (array('delete', 'create', 'update') as $op) { - foreach ($this->getUnprocessed($op) as $name) { - // Call to the configuration entity's storage controller to handle the - // configuration change. - $handled_by_module = FALSE; - // Validate the configuration object name before importing it. - // Config::validateName($name); - if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { - $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); - if ($old_data = $this->storageComparer->getTargetStorage()->read($name)) { - $old_config->initWithData($old_data); - } - - $data = $this->storageComparer->getSourceStorage()->read($name); - $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); - if ($data !== FALSE) { - $new_config->setData($data); - } - - $method = 'import' . ucfirst($op); - $handled_by_module = $this->configManager->getEntityManager()->getStorageController($entity_type)->$method($name, $new_config, $old_config); - } - if (!empty($handled_by_module)) { - $this->setProcessed($op, $name); - } + protected function importInvokeOwner($op, $name) { + // Call to the configuration entity's storage controller to handle the + // configuration change. + $handled_by_module = FALSE; + // Validate the configuration object name before importing it. + // Config::validateName($name); + if ($entity_type = $this->configManager->getEntityTypeIdByName($name)) { + $old_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + if ($old_data = $this->storageComparer->getTargetStorage()->read($name)) { + $old_config->initWithData($old_data); + } + + $data = $this->storageComparer->getSourceStorage()->read($name); + $new_config = new Config($name, $this->storageComparer->getTargetStorage(), $this->eventDispatcher, $this->typedConfigManager); + if ($data !== FALSE) { + $new_config->setData($data); } + + $method = 'import' . ucfirst($op); + $handled_by_module = $this->configManager->getEntityManager()->getStorageController($entity_type)->$method($name, $new_config, $old_config); + } + if (!empty($handled_by_module)) { + $this->setProcessed($op, $name); + return TRUE; } + return FALSE; } /** diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index e13eb3f..f9a7baa 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -11,9 +11,8 @@ 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\ConfigImporter; -use Drupal\Core\Config\ConfigException; use Drupal\Core\Config\TypedConfigManager; use Drupal\Core\Routing\UrlGeneratorInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -218,7 +217,7 @@ public function buildForm(array $form, array &$form_state) { * {@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, @@ -229,21 +228,56 @@ public function submitForm(array &$form, array &$form_state) { drupal_set_message($this->t('Another request may be synchronizing configuration already.')); } else{ - try { - $config_importer->import(); - drupal_flush_all_caches(); - drupal_set_message($this->t('The configuration was imported successfully.')); - } - catch (ConfigException $e) { - // Return a negative result for UI purposes. We do not differentiate - // between an actual synchronization error and a failed lock, because - // concurrent synchronizations are an edge-case happening only when - // multiple developers or site builders attempt to do it without - // coordinating. - watchdog_exception('config_import', $e); - drupal_set_message($this->t('The import failed due to an error. Any errors have been logged.'), 'error'); - } + $config_importer->initialise(); + $batch = array( + 'operations' => array( + array(array($this, 'processBatch'), array($config_importer)), + ), + 'finished' => array($this, 'finishBatch'), + 'title' => t('Synchronizing configuration'), + 'init_message' => t('Starting configuration synchronization.'), + 'progress_message' => t('Synchronized @current configuration files out of @total.'), + 'error_message' => t('Configuration synchronization has encountered an error.'), + 'file' => drupal_get_path('module', 'config') . '/config.admin.inc', + ); + + batch_set($batch); } } + /** + * Processes the config import batch and persists the importer. + * + * @param BatchConfigImporter $config_importer + * The batch config importer object to persist. + * @param $context + * The batch context. + */ + public function processBatch(BatchConfigImporter $config_importer, &$context) { + if (!isset($context['sandbox']['config_importer'])) { + $context['sandbox']['config_importer'] = $config_importer; + } + + $config_importer = $context['sandbox']['config_importer']; + $config_importer->processBatch($context); + } + + /** + * Finish batch. + */ + public function finishBatch($success, $results, $operations) { + if ($success) { + drupal_set_message($this->t('The configuration was imported successfully.')); + } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = $this->t('An error occurred while processing %error_operation with arguments: @arguments', array('%error_operation' => $error_operation[0], '@arguments' => print_r($error_operation[1], TRUE))); + drupal_set_message($message, 'error'); + } + drupal_flush_all_caches(); + } + + }