diff --git a/core/modules/config/src/Form/ConfigSingleImportForm.php b/core/modules/config/src/Form/ConfigSingleImportForm.php index 15f173d..68eb9f4 100644 --- a/core/modules/config/src/Form/ConfigSingleImportForm.php +++ b/core/modules/config/src/Form/ConfigSingleImportForm.php @@ -8,12 +8,23 @@ namespace Drupal\config\Form; use Drupal\Component\Serialization\Yaml; +use Drupal\config\StorageOverrideWrapper; +use Drupal\Core\Config\ConfigImporter; +use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\ConfigManagerInterface; +use Drupal\Core\Config\StorageComparer; use Drupal\Core\Config\StorageInterface; +use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ModuleInstallerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Form\ConfirmFormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Lock\LockBackendInterface; use Drupal\Core\Url; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Provides a form for importing a single configuration file. @@ -35,6 +46,63 @@ class ConfigSingleImportForm extends ConfirmFormBase { protected $configStorage; /** + * The database lock object. + * + * @var \Drupal\Core\Lock\LockBackendInterface + */ + protected $lock; + + /** + * The snapshot configuration object. + * + * @var \Drupal\Core\Config\StorageInterface + */ + protected $snapshotStorage; + + /** + * Event dispatcher. + * + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * The configuration manager. + * + * @var \Drupal\Core\Config\ConfigManagerInterface; + */ + protected $configManager; + + /** + * The typed config manager. + * + * @var \Drupal\Core\Config\TypedConfigManagerInterface + */ + protected $typedConfigManager; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * The theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** + * The module installer. + * + * @var \Drupal\Core\Extension\ModuleInstallerInterface + */ + protected $moduleInstaller; + + + /** * If the config exists, this is that object. Otherwise, FALSE. * * @var \Drupal\Core\Config\Config|\Drupal\Core\Config\Entity\ConfigEntityInterface|bool @@ -56,9 +124,17 @@ class ConfigSingleImportForm extends ConfirmFormBase { * @param \Drupal\Core\Config\StorageInterface $config_storage * The config storage. */ - public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage) { + public function __construct(EntityManagerInterface $entity_manager, StorageInterface $config_storage, StorageInterface $snapshot_storage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ModuleInstallerInterface $module_installer, ThemeHandlerInterface $theme_handler) { $this->entityManager = $entity_manager; $this->configStorage = $config_storage; + $this->snapshotStorage = $snapshot_storage; + $this->lock = $lock; + $this->eventDispatcher = $event_dispatcher; + $this->configManager = $config_manager; + $this->typedConfigManager = $typed_config; + $this->moduleHandler = $module_handler; + $this->moduleInstaller = $module_installer; + $this->themeHandler = $theme_handler; } /** @@ -67,7 +143,15 @@ public function __construct(EntityManagerInterface $entity_manager, StorageInter public static function create(ContainerInterface $container) { return new static( $container->get('entity.manager'), - $container->get('config.storage') + $container->get('config.storage'), + $container->get('config.storage.snapshot'), + $container->get('lock.persistent'), + $container->get('event_dispatcher'), + $container->get('config.manager'), + $container->get('config.typed'), + $container->get('module_handler'), + $container->get('module_installer'), + $container->get('theme_handler') ); } @@ -241,28 +325,114 @@ public function submitForm(array &$form, FormStateInterface $form_state) { return; } - // If a simple configuration file was added, set the data and save. if ($this->data['config_type'] === 'system.simple') { - $this->configFactory()->getEditable($this->data['config_name'])->setData($this->data['import'])->save(); - drupal_set_message($this->t('The %name configuration was imported.', array('%name' => $this->data['config_name']))); + $id = $this->data['config_name']; } - // For a config entity, create an entity and save it. else { + $entity_type = $this->entityManager->getDefinition($this->data['config_type']); + $id_key = $entity_type->getKey('id'); + $id = $entity_type->getConfigPrefix() . '.' . $this->data['import'][$id_key]; + } + $source_storage = new StorageOverrideWrapper($this->configStorage); + $source_storage->override($id, $this->data['import']); + $storage_comparer = new StorageComparer( + $source_storage, + $this->configStorage, + $this->configManager + ); + + $config_importer = new ConfigImporter( + $storage_comparer, + $this->eventDispatcher, + $this->configManager, + $this->lock, + $this->typedConfigManager, + $this->moduleHandler, + $this->moduleInstaller, + $this->themeHandler, + $this->getStringTranslation() + ); + if ($config_importer->alreadyImporting()) { + drupal_set_message($this->t('Another request may be synchronizing configuration already.')); + } + else{ try { - $entity_storage = $this->entityManager->getStorage($this->data['config_type']); - if ($this->configExists) { - $entity = $entity_storage->updateFromStorageRecord($this->configExists, $this->data['import']); + $sync_steps = $config_importer->initialize(); + $batch = array( + 'operations' => array(), + 'finished' => array(get_class($this), 'finishBatch'), + 'title' => t('Synchronizing configuration'), + 'init_message' => t('Starting configuration synchronization.'), + 'progress_message' => t('Completed @current step of @total.'), + 'error_message' => t('Configuration synchronization has encountered an error.'), + ); + foreach ($sync_steps as $sync_step) { + $batch['operations'][] = array(array(get_class($this), 'processBatch'), array($config_importer, $sync_step)); + } + + batch_set($batch); + } + catch (ConfigImporterException $e) { + // There are validation errors. + drupal_set_message($this->t('The configuration cannot be imported because it failed validation for the following reasons:'), 'error'); + foreach ($config_importer->getErrors() as $message) { + drupal_set_message($message, 'error'); } - else { - $entity = $entity_storage->createFromStorageRecord($this->data['import']); + } + } + } + + /** + * Processes the config import batch and persists the importer. + * + * @param \Drupal\Core\Config\ConfigImporter $config_importer + * The batch config importer object to persist. + * @param string $sync_step + * The synchronization step to do. + * @param $context + * The batch 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->doSyncStep($sync_step, $context); + if ($errors = $config_importer->getErrors()) { + if (!isset($context['results']['errors'])) { + $context['results']['errors'] = array(); + } + $context['results']['errors'] += $errors; + } + } + + /** + * Finish batch. + * + * This function is a static function to avoid serializing the ConfigSync + * object unnecessarily. + */ + public static function finishBatch($success, $results, $operations) { + if ($success) { + if (!empty($results['errors'])) { + foreach ($results['errors'] as $error) { + drupal_set_message($error, 'error'); + \Drupal::logger('config_sync')->error($error); } - $entity->save(); - drupal_set_message($this->t('The @entity_type %label was imported.', array('@entity_type' => $entity->getEntityTypeId(), '%label' => $entity->label()))); + drupal_set_message(\Drupal::translation()->translate('The configuration was imported with errors.'), 'warning'); } - catch (\Exception $e) { - drupal_set_message($e->getMessage(), 'error'); + else { + drupal_set_message(\Drupal::translation()->translate('The configuration was imported successfully.')); } } + else { + // An error occurred. + // $operations contains the operations that remained unprocessed. + $error_operation = reset($operations); + $message = \Drupal::translation()->translate('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'); + } } } diff --git a/core/modules/config/src/StorageOverrideWrapper.php b/core/modules/config/src/StorageOverrideWrapper.php new file mode 100644 index 0000000..c4fc49b --- /dev/null +++ b/core/modules/config/src/StorageOverrideWrapper.php @@ -0,0 +1,187 @@ +storage = $storage; + $this->collection = $collection; + } + + /** + * {@inheritdoc} + */ + public function exists($name) { + return isset($this->overrideData[$this->collection][$name]) || $this->storage->exists($name); + } + + /** + * {@inheritdoc} + */ + public function read($name) { + if (isset($this->overrideData[$this->collection][$name])) { + return $this->overrideData[$this->collection][$name]; + } + return $this->storage->read($name); + } + + /** + * {@inheritdoc} + */ + public function readMultiple(array $names) { + $data = $this->storage->readMultiple(($names)); + foreach ($names as $name) { + if (isset($this->overrideData[$this->collection][$name])) { + $data[$name] = $this->overrideData[$this->collection][$name]; + } + } + return $data; + } + + /** + * {@inheritdoc} + */ + public function write($name, array $data) { + if (isset($this->overrideData[$this->collection][$name])) { + unset($this->overrideData[$this->collection][$name]); + } + return $this->storage->write($name, $data); + } + + /** + * {@inheritdoc} + */ + public function delete($name) { + if (isset($this->overrideData[$this->collection][$name])) { + unset($this->overrideData[$this->collection][$name]); + } + return $this->storage->delete($name); + } + + /** + * {@inheritdoc} + */ + public function rename($name, $new_name) { + if (isset($this->overrideData[$this->collection][$name])) { + $this->overrideData[$this->collection][$new_name] = $this->overrideData[$this->collection][$name]; + unset($this->overrideData[$this->collection][$name]); + } + return $this->rename($name, $new_name); + } + + /** + * {@inheritdoc} + */ + public function encode($data) { + return $this->storage->encode($data); + } + + /** + * {@inheritdoc} + */ + public function decode($raw) { + return $this->storage->decode($raw); + } + + /** + * {@inheritdoc} + */ + public function listAll($prefix = '') { + $names = $this->storage->listAll($prefix); + $additional_names = []; + if ($prefix === '') { + $additional_names = array_keys($this->overrideData[$this->collection]); + } + else { + foreach (array_keys($this->overrideData[$this->collection]) as $name) { + if (strpos($name, $prefix) === 0) { + $additional_names[] = $name; + } + } + } + if (!empty($additional_names)) { + $names = array_unique(array_merge($names, $additional_names)); + } + return $names; + } + + /** + * {@inheritdoc} + */ + public function deleteAll($prefix = '') { + if ($prefix === '') { + $this->overrideData[$this->collection] = []; + } + else { + foreach (array_keys($this->overrideData[$this->collection]) as $name) { + if (strpos($name, $prefix) === 0) { + unset($this->overrideData[$this->collection][$name]); + } + } + } + return $this->storage->deleteAll($prefix); + } + + /** + * {@inheritdoc} + */ + public function createCollection($collection) { + $this->collection = $collection; + return $this->storage->createCollection($collection); + } + + /** + * {@inheritdoc} + */ + public function getAllCollectionNames() { + return $this->storage->getAllCollectionNames(); + } + + /** + * {@inheritdoc} + */ + public function getCollectionName() { + return $this->collection; + } + + public function override($name, array $data) { + $this->overrideData[$this->collection][$name] = $data; + return $this; + } +} diff --git a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php index a51fda8..20331ae 100644 --- a/core/modules/config/src/Tests/ConfigSingleImportExportTest.php +++ b/core/modules/config/src/Tests/ConfigSingleImportExportTest.php @@ -56,7 +56,7 @@ public function testImport() { $this->assertIdentical($entity->label(), 'First'); $this->assertIdentical($entity->id(), 'first'); $this->assertTrue($entity->status()); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); // Attempt an import with an existing ID but missing UUID. $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); @@ -72,8 +72,7 @@ public function testImport() { $this->drupalPostForm('admin/config/development/configuration/single/import', $edit, t('Import')); $this->assertRaw(t('Are you sure you want to create a new %name @type?', array('%name' => 'custom_id', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); - $entity = $storage->load('custom_id'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); // Perform an import with a unique ID and UUID. $import = <<assertRaw(t('Are you sure you want to create a new %name @type?', array('%name' => 'second', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); $entity = $storage->load('second'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); $this->assertIdentical($entity->label(), 'Second'); $this->assertIdentical($entity->id(), 'second'); $this->assertFalse($entity->status()); @@ -116,7 +115,7 @@ public function testImport() { $this->assertRaw(t('Are you sure you want to update the %name @type?', array('%name' => 'second', '@type' => 'test configuration'))); $this->drupalPostForm(NULL, array(), t('Confirm')); $entity = $storage->load('second'); - $this->assertRaw(t('The @entity_type %label was imported.', array('@entity_type' => 'config_test', '%label' => $entity->label()))); + $this->assertRaw(t('The configuration was imported successfully.')); $this->assertIdentical($entity->label(), 'Second updated'); }