diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 82f7aa1..9bf424b 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -14,7 +14,6 @@ use Drupal\Core\DependencyInjection\DependencySerialization; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Lock\LockBackendInterface; -use Guzzle\Common\Exception\ExceptionCollection; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -165,15 +164,33 @@ public function __construct(StorageComparerInterface $storage_comparer, EventDis $this->processedExtensions = $this->getEmptyExtensionsProcessedList(); } + /** + * Sets the importer log that should be used. + * + * @param \Drupal\Core\Config\ConfigImporterLogInterface $log + */ public function setLog(ConfigImporterLogInterface $log = NULL) { $this->log = $log; } + /** + * Logs a message to the importer log when one has been set. + * + * @param string $message + * The message to log. + * @param string $severity + * The log severity, error, warning or info. + * + * @return bool + * TRUE when the log is available and the message has been logged, FALSE + * otherwise. + */ protected function log($message, $severity) { if ($this->log) { $this->log->log($message, $severity); return TRUE; } + return FALSE; } /** @@ -473,6 +490,11 @@ public function validate() { * The change operation. * @param string $name * The name of the configuration to process. + * + * @throws \Exception + * Thrown when the import process fails, only thrown when no importer log is + * set, otherwise the exception message is logged and the configuration + * is skipped. */ protected function processConfiguration($op, $name) { try { @@ -484,6 +506,10 @@ protected function processConfiguration($op, $name) { if (!$this->log($e->getMessage(), 'error')) { throw $e; } + else { + // Error for that operation was logged, mark it as processed. + $this->setProcessedConfiguration($op, $name); + } } } diff --git a/core/lib/Drupal/Core/Config/ConfigImporterLogState.php b/core/lib/Drupal/Core/Config/ConfigImporterLogArray.php similarity index 52% copy from core/lib/Drupal/Core/Config/ConfigImporterLogState.php copy to core/lib/Drupal/Core/Config/ConfigImporterLogArray.php index 3cc86ff..7f9eb26 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporterLogState.php +++ b/core/lib/Drupal/Core/Config/ConfigImporterLogArray.php @@ -2,18 +2,25 @@ /** * @file - * Contains Drupal\Core\Config\ConfigInstallerLogInterface. + * Contains Drupal\Core\Config\ConfigImporterLogArray. */ namespace Drupal\Core\Config; /** - * Interface for logging events during a config import. + * A config importer log using a simple PHP array. */ -class ConfigImporterLogState implements ConfigImporterLogInterface { +class ConfigImporterLogArray implements ConfigImporterLogInterface { + /** + * Array of log entries. + * @var array + */ protected $logs = array(); + /** + * {@inheritdoc} + */ public function log($message, $severity) { $this->logs[] = array( 'message' => $message, @@ -21,6 +28,9 @@ public function log($message, $severity) { ); } + /** + * {@inheritdoc} + */ public function getLogMessages() { return $this->logs; } diff --git a/core/lib/Drupal/Core/Config/ConfigImporterLogInterface.php b/core/lib/Drupal/Core/Config/ConfigImporterLogInterface.php index 2d77ca1..cd461fb 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporterLogInterface.php +++ b/core/lib/Drupal/Core/Config/ConfigImporterLogInterface.php @@ -2,7 +2,7 @@ /** * @file - * Contains Drupal\Core\Config\ConfigInstallerLogInterface. + * Contains Drupal\Core\Config\ConfigImporterLogInterface. */ namespace Drupal\Core\Config; diff --git a/core/lib/Drupal/Core/Config/ConfigImporterLogState.php b/core/lib/Drupal/Core/Config/ConfigImporterLogState.php index 3cc86ff..d55753d 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporterLogState.php +++ b/core/lib/Drupal/Core/Config/ConfigImporterLogState.php @@ -2,27 +2,64 @@ /** * @file - * Contains Drupal\Core\Config\ConfigInstallerLogInterface. + * Contains Drupal\Core\Config\ConfigImporterLogState. */ namespace Drupal\Core\Config; +use Drupal\Core\DependencyInjection\DependencySerialization; +use Drupal\Core\KeyValueStore\StateInterface; + /** - * Interface for logging events during a config import. + * A config importer log using state as the storage. */ -class ConfigImporterLogState implements ConfigImporterLogInterface { +class ConfigImporterLogState extends DependencySerialization implements ConfigImporterLogInterface { + /** + * Key to use for state. + */ + const STATE_KEY = 'config_importer_log'; + + /** + * Array of log entries. + * @var array + */ protected $logs = array(); + /** + * State backend. + * + * @var \Drupal\Core\KeyValueStore\StateInterface + */ + protected $state; + + /** + * Constructs a ConfigImporterLogState object. + * + * @param \Drupal\Core\KeyValueStore\StateInterface $state + * The state object to store the logs in. + */ + public function __construct(StateInterface $state) { + $this->state = $state; + } + + /** + * {@inheritdoc} + */ public function log($message, $severity) { - $this->logs[] = array( + $logs = $this->state->get(static::STATE_KEY, array()); + $logs[] = array( 'message' => $message, 'severity' => $severity, ); + $this->state->set(static::STATE_KEY, $logs); } + /** + * {@inheritdoc} + */ public function getLogMessages() { - return $this->logs; + return $this->state->get(static::STATE_KEY, array()); } } diff --git a/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php b/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php index 6aca4df..4eae41e 100644 --- a/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php +++ b/core/lib/Drupal/Core/Config/Entity/ImportableEntityStorageInterface.php @@ -38,6 +38,9 @@ public function importCreate($name, Config $new_config, Config $old_config); * A configuration object containing the new configuration data. * @param \Drupal\Core\Config\Config $old_config * A configuration object containing the old configuration data. + * + * @throws \Drupal\Core\Config\ConfigImporterException + * Thrown when the config entity that should be updated can not be found. */ public function importUpdate($name, Config $new_config, Config $old_config); diff --git a/core/lib/Drupal/Core/Config/StorageComparer.php b/core/lib/Drupal/Core/Config/StorageComparer.php index 7fbf1d5..ee467b6 100644 --- a/core/lib/Drupal/Core/Config/StorageComparer.php +++ b/core/lib/Drupal/Core/Config/StorageComparer.php @@ -249,12 +249,4 @@ protected function getAndSortConfigData() { $this->sourceNames = $dependency_manager->setData($this->sourceData)->sortAll(); } - /** - * {@inheritdoc} - */ - public function swapOp($from, $to, $name) { - $key = array_search($name, $this->changelist[$from]); - unset($this->changelist[$from][$key]); - $this->addChangeList($to, array($name)); - } } diff --git a/core/lib/Drupal/Core/Config/StorageComparerInterface.php b/core/lib/Drupal/Core/Config/StorageComparerInterface.php index bc27635..3c6c666 100644 --- a/core/lib/Drupal/Core/Config/StorageComparerInterface.php +++ b/core/lib/Drupal/Core/Config/StorageComparerInterface.php @@ -80,18 +80,4 @@ public function hasChanges($ops = array('delete', 'create', 'update')); */ public function validateSiteUuid(); - /** - * Moves a configuration name from one change list to another. - * - * @param string $name - * The configuration object name. - * @param string $from - * The current change operation. Either delete, create or update. - * @param string $to - * The change operation to change to. Either delete, create or update. - * - * @return $this - */ - public function swapOp($name, $from, $to); - } diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index c48a776..4dffd54 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -7,8 +7,7 @@ namespace Drupal\config\Form; -use Drupal\Component\Uuid\UuidInterface; -use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Config\ConfigImporterLogArray; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Extension\ThemeHandlerInterface; use Drupal\Core\Config\ConfigManagerInterface; @@ -111,6 +110,8 @@ class ConfigSync extends FormBase { * The module handler * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler * The theme handler + * @param \Drupal\Core\KeyValueStore\StateInterface $state + * The state object. */ public function __construct(StorageInterface $sourceStorage, StorageInterface $targetStorage, LockBackendInterface $lock, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, UrlGeneratorInterface $url_generator, TypedConfigManager $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { $this->sourceStorage = $sourceStorage; @@ -288,7 +289,15 @@ public static function processBatch(BatchConfigImporter $config_importer, $opera } $config_importer = $context['sandbox']['config_importer']; + $log = new ConfigImporterLogArray(); + $config_importer->setLog($log); $config_importer->$operation($context); + $config_importer->setLog(NULL); + foreach ($log->getLogMessages() as $log) { + drupal_set_message(t('Error while importing configuration: %error.', array('%error' => $log['message'])), $log['severity']); + // @todo Improve severity, use constants? + watchdog('config_sync', 'Error while importing configuration: %error.', array('%error' => $log['message']), WATCHDOG_ERROR); + } } /** diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index 084be15..9494348 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -312,4 +312,43 @@ function prepareSiteNameUpdate($new_site_name) { $config_data['name'] = $new_site_name; $staging->write('system.site', $config_data); } + + /** + * Tests an import that results in an error. + */ + function testImportErrorLog() { + $name_primary = 'config_test.dynamic.primary'; + $name_secondary = 'config_test.dynamic.secondary'; + $staging = $this->container->get('config.storage.staging'); + $uuid = $this->container->get('uuid'); + + $values_primary = array( + 'id' => 'primary', + 'label' => 'Primary', + 'weight' => 0, + 'uuid' => $uuid->generate(), + ); + $staging->write($name_primary, $values_primary); + $values_secondary = array( + 'id' => 'secondary', + 'label' => 'Secondary Sync', + 'weight' => 0, + 'uuid' => $uuid->generate(), + // Add a dependency on primary, to ensure that is synced first. + 'dependencies' => array( + 'entity' => array($name_primary), + ) + ); + $staging->write($name_secondary, $values_secondary); + // Verify that there are configuration differences to import. + $this->drupalGet('admin/config/development/configuration'); + $this->assertNoText(t('There are no configuration changes.')); + + // Attempt to import configuration and verify that an error message appears. + $this->drupalPostForm(NULL, array(), t('Import all')); + $this->assertRaw(t('Error while importing configuration: %error.', array('%error' => 'config_test entity with ID secondary already exists.'))); + $this->assertText(t('1 new')); + $this->assertText(t('1 removed')); + } + } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php index dedba77..2c53a61 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -9,7 +9,7 @@ use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\ConfigImporterException; -use Drupal\Core\Config\ConfigImporterLogState; +use Drupal\Core\Config\ConfigImporterLogArray; use Drupal\Core\Config\StorageComparer; use Drupal\Component\Utility\String; use Drupal\simpletest\DrupalUnitTestBase; @@ -75,7 +75,7 @@ function setUp() { $this->container->get('module_handler'), $this->container->get('theme_handler') ); - $this->configLog = new ConfigImporterLogState(); + $this->configLog = new ConfigImporterLogArray(); $this->configImporter->setLog($this->configLog); $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); } diff --git a/core/tests/Drupal/Tests/Core/Config/ConfigImporterLogArrayTest.php b/core/tests/Drupal/Tests/Core/Config/ConfigImporterLogArrayTest.php new file mode 100644 index 0000000..2b0ce03 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Config/ConfigImporterLogArrayTest.php @@ -0,0 +1,92 @@ + 'Config importer state log', + 'description' => 'Tests ConfigImporterLogState', + 'group' => 'Configuration' + ); + } + + /** + * @covers ::log + */ + public function testLogEmpty() { + $state = $this->getMock('Drupal\Core\KeyValueStore\StateInterface'); + $state->expects($this->once()) + ->method('get') + ->with(ConfigImporterLogState::STATE_KEY, array()) + ->will($this->returnValue(array())); + + $message = $this->randomName(); + $severity = 'error'; + $state->expects($this->once()) + ->method('set') + ->with(ConfigImporterLogState::STATE_KEY, array(array('message' => $message, 'severity' => $severity))); + + $log = new ConfigImporterLogState($state); + $log->log($message, $severity); + } + + /** + * @covers ::log + */ + public function testLogExisting() { + $state = $this->getMock('Drupal\Core\KeyValueStore\StateInterface'); + + $logs = array( + array('message' => $this->randomName(), 'severity' => 'warning'), + ); + $state->expects($this->once()) + ->method('get') + ->with(ConfigImporterLogState::STATE_KEY, array()) + ->will($this->returnValue($logs)); + + $message = $this->randomName(); + $severity = 'error'; + + $logs[] = array('message' => $message, 'severity' => $severity); + + $state->expects($this->once()) + ->method('set') + ->with(ConfigImporterLogState::STATE_KEY, $logs); + + $log = new ConfigImporterLogState($state); + $log->log($message, $severity); + } + + /** + * @covers ::getLogMessages + */ + public function testgetLogMessages() { + $state = $this->getMock('Drupal\Core\KeyValueStore\StateInterface'); + + $logs = array( + array('message' => $this->randomName(), 'severity' => 'warning'), + ); + $state->expects($this->once()) + ->method('get') + ->with(ConfigImporterLogState::STATE_KEY, array()) + ->will($this->returnValue($logs)); + + $log = new ConfigImporterLogState($state); + $log->getLogMessages(); + } + +}