diff --git a/core/lib/Drupal/Core/Config/BatchConfigImporter.php b/core/lib/Drupal/Core/Config/BatchConfigImporter.php index 67aee13..a57a076 100644 --- a/core/lib/Drupal/Core/Config/BatchConfigImporter.php +++ b/core/lib/Drupal/Core/Config/BatchConfigImporter.php @@ -108,7 +108,9 @@ public function processConfigurationBatch(array &$context) { } $operation = $this->getNextConfigurationOperation(); if (!empty($operation)) { - $this->processConfiguration($operation['op'], $operation['name']); + 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; diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 1303b7f..9ddbfb0 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -14,6 +14,7 @@ use Drupal\Core\DependencyInjection\DependencySerialization; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Lock\LockBackendInterface; +use Drupal\Core\StringTranslation\TranslationManager; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -120,6 +121,13 @@ class ConfigImporter extends DependencySerialization { protected $themeHandler; /** + * The string translation service. + * + * @var \Drupal\Core\StringTranslation\TranslationManager + */ + protected $translationManager; + + /** * Flag set to import system.theme during processing theme enable and disables. * * @var bool @@ -127,6 +135,13 @@ class ConfigImporter extends DependencySerialization { protected $processedSystemTheme = FALSE; /** + * List of errors that were logged during a config import. + * + * @var array + */ + protected $errors = array(); + + /** * Constructs a configuration import object. * * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer @@ -144,8 +159,10 @@ class ConfigImporter extends DependencySerialization { * The module handler * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler * The theme handler + * @param \Drupal\Core\StringTranslation\TranslationManager $translation_manager + * The string translation service. */ - public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler) { + public function __construct(StorageComparerInterface $storage_comparer, EventDispatcherInterface $event_dispatcher, ConfigManagerInterface $config_manager, LockBackendInterface $lock, TypedConfigManagerInterface $typed_config, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, TranslationManager $translation_manager) { $this->storageComparer = $storage_comparer; $this->eventDispatcher = $event_dispatcher; $this->configManager = $config_manager; @@ -153,11 +170,32 @@ public function __construct(StorageComparerInterface $storage_comparer, EventDis $this->typedConfigManager = $typed_config; $this->moduleHandler = $module_handler; $this->themeHandler = $theme_handler; + $this->translationManager = $translation_manager; $this->processedConfiguration = $this->storageComparer->getEmptyChangelist(); $this->processedExtensions = $this->getEmptyExtensionsProcessedList(); } /** + * Logs an error message. + * + * @param string $message + * The message to log. + */ + protected function logError($message) { + $this->errors[] = $message; + } + + /** + * Returns error messages created while running the import. + * + * @return array + * List of messages. + */ + public function getErrors() { + return $this->errors; + } + + /** * Gets the configuration storage comparer. * * @return \Drupal\Core\Config\StorageComparerInterface @@ -415,7 +453,9 @@ public function import() { // to handle dependencies correctly. foreach (array('delete', 'create', 'update') as $op) { foreach ($this->getUnprocessedConfiguration($op) as $name) { - $this->processConfiguration($op, $name); + if ($this->checkOp($op, $name)) { + $this->processConfiguration($op, $name); + } } } // Allow modules to react to a import. @@ -452,10 +492,23 @@ 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) { - if (!$this->importInvokeOwner($op, $name)) { - $this->importConfig($op, $name); + try { + if (!$this->importInvokeOwner($op, $name)) { + $this->importConfig($op, $name); + } + } + catch (\Exception $e) { + $this->logError($this->t('Unexpected error during import with operation @op for @name: @message', array('@op' => $op, '@name' => $name, '@message' => $e->getMessage()))); + // Error for that operation was logged, mark it as processed so that + // the import can continue. + $this->setProcessedConfiguration($op, $name); } } @@ -506,6 +559,68 @@ protected function processExtension($type, $op, $name) { } /** + * Checks that the operation is still valid. + * + * During a configuration import secondary writes and deletes are possible. + * This method checks that the operation is still valid before processing a + * configuration change. + * + * @param string $op + * The change operation. + * @param string $name + * The name of the configuration to process. + * + * @throws \Drupal\Core\Config\ConfigImporterException + * + * @return bool + * TRUE is to continue processing, FALSE otherwise. + */ + protected function checkOp($op, $name) { + $target_exists = $this->storageComparer->getTargetStorage()->exists($name); + switch ($op) { + case 'delete': + if (!$target_exists) { + // The configuration has already been deleted. For example, a field + // is automatically deleted if all the instances are. + $this->setProcessedConfiguration($op, $name); + return FALSE; + } + break; + + case 'create': + if ($target_exists) { + // If the target already exists, use the entity storage to delete it + // again, if is a simple config, delete it directly. + if ($entity_type_id = $this->configManager->getEntityTypeIdByName($name)) { + $entity_storage = $this->configManager->getEntityManager()->getStorage($entity_type_id); + $entity_type = $this->configManager->getEntityManager()->getDefinition($entity_type_id); + $entity = $entity_storage->load($entity_storage->getIDFromConfigName($name, $entity_type->getConfigPrefix())); + $entity->delete(); + $this->logError($this->translationManager->translate('Deleted and replaced configuration entity "@name"', array('@name' => $name))); + } + else { + $this->storageComparer->getTargetStorage()->delete($name); + $this->logError($this->t('Deleted and replaced configuration "@name"', array('@name' => $name))); + } + return TRUE; + } + break; + + case 'update': + if (!$target_exists) { + $this->logError($this->t('Update target "@name" is missing.', array('@name' => $name))); + // Mark as processed so that the synchronisation continues. Once the + // the current synchronisation is complete it will show up as a + // create. + $this->setProcessedConfiguration($op, $name); + return FALSE; + } + break; + } + return TRUE; + } + + /** * Writes a configuration change from the source to the target storage. * * @param string $op @@ -574,7 +689,6 @@ protected function importInvokeOwner($op, $name) { $this->setProcessedConfiguration($op, $name); return TRUE; } - return FALSE; } /** @@ -656,5 +770,31 @@ protected function reInjectMe() { $this->typedConfigManager = \Drupal::service('config.typed'); $this->moduleHandler = \Drupal::moduleHandler(); $this->themeHandler = \Drupal::service('theme_handler'); + $this->translationManager = \Drupal::service('string_translation'); + } + + /** + * Translates a string to the current language or to a given language. + * + * @param string $string + * A string containing the English string to translate. + * @param array $args + * An associative array of replacements to make after translation. Based + * on the first character of the key, the value is escaped and/or themed. + * See \Drupal\Component\Utility\String::format() for details. + * @param array $options + * An associative array of additional options, with the following elements: + * - 'langcode': The language code to translate to a language other than + * what is used to display the page. + * - 'context': The context the source string belongs to. + * + * @return string + * The translated string. + * + * @see \Drupal\Core\StringTranslation\TranslationManager::translate() + */ + protected function t($string, array $args = array(), array $options = array()) { + return $this->translationManager->translate($string, $args, $options); } + } diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php index 2d773dc..a5d634c 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php @@ -9,6 +9,7 @@ use Drupal\Component\Utility\String; use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityMalformedException; use Drupal\Core\Entity\EntityStorageBase; @@ -425,6 +426,9 @@ public function importCreate($name, Config $new_config, Config $old_config) { public function importUpdate($name, Config $new_config, Config $old_config) { $id = static::getIDFromConfigName($name, $this->entityType->getConfigPrefix()); $entity = $this->load($id); + if (!$entity) { + throw new ConfigImporterException(String::format('Attempt to update non-existing entity "@id".', array('@id' => $id))); + } $entity->setSyncing(TRUE); $entity->original = clone $entity; 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/modules/block/custom_block/lib/Drupal/custom_block/Entity/CustomBlockType.php b/core/modules/block/custom_block/lib/Drupal/custom_block/Entity/CustomBlockType.php index e8eee2e..de4a109 100644 --- a/core/modules/block/custom_block/lib/Drupal/custom_block/Entity/CustomBlockType.php +++ b/core/modules/block/custom_block/lib/Drupal/custom_block/Entity/CustomBlockType.php @@ -75,7 +75,7 @@ class CustomBlockType extends ConfigEntityBase implements CustomBlockTypeInterfa public function postSave(EntityStorageInterface $storage, $update = TRUE) { parent::postSave($storage, $update); - if (!$update) { + if (!$update && !$this->isSyncing()) { entity_invoke_bundle_hook('create', 'custom_block', $this->id()); if (!$this->isSyncing()) { custom_block_add_body_field($this->id); diff --git a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php index c48a776..907340c 100644 --- a/core/modules/config/lib/Drupal/config/Form/ConfigSync.php +++ b/core/modules/config/lib/Drupal/config/Form/ConfigSync.php @@ -250,7 +250,8 @@ public function submitForm(array &$form, array &$form_state) { $this->lock, $this->typedConfigManager, $this->moduleHandler, - $this->themeHandler + $this->themeHandler, + $this->translationManager() ); if ($config_importer->alreadyImporting()) { drupal_set_message($this->t('Another request may be synchronizing configuration already.')); @@ -289,6 +290,12 @@ public static function processBatch(BatchConfigImporter $config_importer, $opera $config_importer = $context['sandbox']['config_importer']; $config_importer->$operation($context); + if ($errors = $config_importer->getErrors()) { + if (!isset($context['results']['errors'])) { + $context['results']['errors'] = array(); + } + $context['results']['errors'] += $errors; + } } /** @@ -299,7 +306,16 @@ public static function processBatch(BatchConfigImporter $config_importer, $opera */ public static function finishBatch($success, $results, $operations) { if ($success) { - drupal_set_message(\Drupal::translation()->translate('The configuration was imported successfully.')); + if (!empty($results['errors'])) { + foreach ($results['errors'] as $error) { + drupal_set_message($error, 'error'); + watchdog('config_sync', $error, NULL, WATCHDOG_ERROR); + } + drupal_set_message(\Drupal::translation()->translate('The configuration was imported with errors.'), 'warning'); + } + else { + drupal_set_message(\Drupal::translation()->translate('The configuration was imported successfully.')); + } } else { // An error occurred. diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php index 038e318..2d08077 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportAllTest.php @@ -95,6 +95,9 @@ public function testInstallUninstall() { // Import the configuration thereby re-installing all the modules. $this->configImporter()->import(); + // Check that there are no errors. + $this->assertIdentical($this->configImporter()->getErrors(), array()); + // Check that all modules that were uninstalled are now reinstalled. $this->assertModules(array_keys($modules_to_uninstall), TRUE); foreach($modules_to_uninstall as $module => $info) { diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php index 3f86c9b..ac77a58 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportRecreateTest.php @@ -59,7 +59,8 @@ public function setUp() { $this->container->get('lock'), $this->container->get('config.typed'), $this->container->get('module_handler'), - $this->container->get('theme_handler') + $this->container->get('theme_handler'), + $this->container->get('string_translation') ); } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php index 084be15..94f35fb 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImportUITest.php @@ -7,6 +7,7 @@ namespace Drupal\config\Tests; +use Drupal\Component\Utility\String; use Drupal\Core\Config\InstallStorage; use Drupal\simpletest\WebTestBase; @@ -312,4 +313,55 @@ 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, + 'style' => NULL, + 'test_dependencies' => array(), + 'status' => TRUE, + 'uuid' => $uuid->generate(), + 'langcode' => 'en', + 'dependencies' => array(), + 'protected_property' => null, + ); + $staging->write($name_primary, $values_primary); + $values_secondary = array( + 'id' => 'secondary', + 'label' => 'Secondary Sync', + 'weight' => 0, + 'style' => NULL, + 'test_dependencies' => array(), + 'status' => TRUE, + 'uuid' => $uuid->generate(), + 'langcode' => 'en', + // Add a dependency on primary, to ensure that is synced first. + 'dependencies' => array( + 'entity' => array($name_primary), + ), + 'protected_property' => null, + ); + $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->assertText(String::format('Deleted and replaced configuration entity "@name"', array('@name' => $name_secondary))); + $this->assertText(t('The configuration was imported with errors.')); + $this->assertNoText(t('The configuration was imported successfully.')); + $this->assertText(t('There are no configuration changes.')); + } + } diff --git a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php index ff154b7..7597659 100644 --- a/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php +++ b/core/modules/config/lib/Drupal/config/Tests/ConfigImporterTest.php @@ -7,6 +7,7 @@ namespace Drupal\config\Tests; +use Drupal\Component\Utility\String; use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\ConfigImporterException; use Drupal\Core\Config\StorageComparer; @@ -64,7 +65,8 @@ function setUp() { $this->container->get('lock'), $this->container->get('config.typed'), $this->container->get('module_handler'), - $this->container->get('theme_handler') + $this->container->get('theme_handler'), + $this->container->get('string_translation') ); } @@ -149,6 +151,8 @@ function testDeleted() { $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 0); } /** @@ -196,6 +200,274 @@ function testNew() { // Verify that there is nothing more to import. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 0); + } + + /** + * Tests that secondary writes are overwritten. + */ + function testSecondaryWritePrimaryFirst() { + $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); + + // Import. + $this->configImporter->reset()->import(); + + $entity_storage = \Drupal::entityManager()->getStorage('config_test'); + $primary = $entity_storage->load('primary'); + $this->assertEqual($primary->id(), 'primary'); + $this->assertEqual($primary->uuid(), $values_primary['uuid']); + $this->assertEqual($primary->label(), $values_primary['label']); + $secondary = $entity_storage->load('secondary'); + $this->assertEqual($secondary->id(), 'secondary'); + $this->assertEqual($secondary->uuid(), $values_secondary['uuid']); + $this->assertEqual($secondary->label(), $values_secondary['label']); + + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 1); + $this->assertEqual($logs[0], String::format('Deleted and replaced configuration entity "@name"', array('@name' => $name_secondary))); + } + + /** + * Tests that secondary writes are overwritten. + */ + function testSecondaryWriteSecondaryFirst() { + $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(), + // Add a dependency on secondary, so that is synced first. + 'dependencies' => array( + 'entity' => array($name_secondary), + ) + ); + $staging->write($name_primary, $values_primary); + $values_secondary = array( + 'id' => 'secondary', + 'label' => 'Secondary Sync', + 'weight' => 0, + 'uuid' => $uuid->generate(), + ); + $staging->write($name_secondary, $values_secondary); + + // Import. + $this->configImporter->reset()->import(); + + $entity_storage = \Drupal::entityManager()->getStorage('config_test'); + $primary = $entity_storage->load('primary'); + $this->assertEqual($primary->id(), 'primary'); + $this->assertEqual($primary->uuid(), $values_primary['uuid']); + $this->assertEqual($primary->label(), $values_primary['label']); + $secondary = $entity_storage->load('secondary'); + $this->assertEqual($secondary->id(), 'secondary'); + $this->assertEqual($secondary->uuid(), $values_secondary['uuid']); + $this->assertEqual($secondary->label(), $values_secondary['label']); + + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 1); + $message = String::format('config_test entity with ID @name already exists', array('@name' => 'secondary')); + $this->assertEqual($logs[0], String::format('Unexpected error during import with operation @op for @name: @message.', array('@op' => 'create', '@name' => $name_primary, '@message' => $message))); + } + + /** + * Tests that secondary updates for deleted files work as expected. + */ + function testSecondaryUpdateDeletedDeleterFirst() { + $name_deleter = 'config_test.dynamic.deleter'; + $name_deletee = 'config_test.dynamic.deletee'; + $name_other = 'config_test.dynamic.other'; + $storage = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + $uuid = $this->container->get('uuid'); + + $values_deleter = array( + 'id' => 'deleter', + 'label' => 'Deleter', + 'weight' => 0, + 'uuid' => $uuid->generate(), + ); + $storage->write($name_deleter, $values_deleter); + $values_deleter['label'] = 'Updated Deleter'; + $staging->write($name_deleter, $values_deleter); + $values_deletee = array( + 'id' => 'deletee', + 'label' => 'Deletee', + 'weight' => 0, + 'uuid' => $uuid->generate(), + // Add a dependency on deleter, to make sure that is synced first. + 'dependencies' => array( + 'entity' => array($name_deleter), + ) + ); + $storage->write($name_deletee, $values_deletee); + $values_deletee['label'] = 'Updated Deletee'; + $staging->write($name_deletee, $values_deletee); + + // Ensure that import will continue after the error. + $values_other = array( + 'id' => 'other', + 'label' => 'Other', + 'weight' => 0, + 'uuid' => $uuid->generate(), + // Add a dependency on deleter, to make sure that is synced first. This + // will also be synced after the deletee due to alphabetical ordering. + 'dependencies' => array( + 'entity' => array($name_deleter), + ) + ); + $storage->write($name_other, $values_other); + $values_other['label'] = 'Updated other'; + $staging->write($name_other, $values_other); + + // Check update changelist order. + $updates = $this->configImporter->reset()->getStorageComparer()->getChangelist('update'); + $expected = array( + $name_deleter, + $name_deletee, + $name_other, + ); + $this->assertIdentical($expected, $updates); + + // Import. + $this->configImporter->import(); + + $entity_storage = \Drupal::entityManager()->getStorage('config_test'); + $deleter = $entity_storage->load('deleter'); + $this->assertEqual($deleter->id(), 'deleter'); + $this->assertEqual($deleter->uuid(), $values_deleter['uuid']); + $this->assertEqual($deleter->label(), $values_deleter['label']); + + // The deletee was deleted in + // \Drupal\config_test\Entity\ConfigTest::postSave(). + $this->assertFalse($entity_storage->load('deletee')); + + $other = $entity_storage->load('other'); + $this->assertEqual($other->id(), 'other'); + $this->assertEqual($other->uuid(), $values_other['uuid']); + $this->assertEqual($other->label(), $values_other['label']); + + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 1); + $this->assertEqual($logs[0], String::format('Update target "@name" is missing.', array('@name' => $name_deletee))); + } + + /** + * Tests that secondary updates for deleted files work as expected. + */ + function testSecondaryUpdateDeletedDeleteeFirst() { + $name_deleter = 'config_test.dynamic.deleter'; + $name_deletee = 'config_test.dynamic.deletee'; + $storage = $this->container->get('config.storage'); + $staging = $this->container->get('config.storage.staging'); + $uuid = $this->container->get('uuid'); + + $values_deleter = array( + 'id' => 'deleter', + 'label' => 'Deleter', + 'weight' => 0, + 'uuid' => $uuid->generate(), + // Add a dependency on deletee, to make sure that is synced first. + 'dependencies' => array( + 'entity' => array($name_deletee), + ), + ); + $storage->write($name_deleter, $values_deleter); + $values_deleter['label'] = 'Updated Deleter'; + $staging->write($name_deleter, $values_deleter); + $values_deletee = array( + 'id' => 'deletee', + 'label' => 'Deletee', + 'weight' => 0, + 'uuid' => $uuid->generate(), + ); + $storage->write($name_deletee, $values_deletee); + $values_deletee['label'] = 'Updated Deletee'; + $staging->write($name_deletee, $values_deletee); + + // Import. + $this->configImporter->reset()->import(); + + $entity_storage = \Drupal::entityManager()->getStorage('config_test'); + $deleter = $entity_storage->load('deleter'); + $this->assertEqual($deleter->id(), 'deleter'); + $this->assertEqual($deleter->uuid(), $values_deleter['uuid']); + $this->assertEqual($deleter->label(), $values_deleter['label']); + // @todo The deletee entity does not exist as the update worked but the + // entity was deleted after that. There is also no log message as this + // happened outside of the config importer. + $this->assertFalse($entity_storage->load('deletee')); + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 0); + } + + /** + * Tests that secondary deletes for deleted files work as expected. + */ + function testSecondaryDeletedDeleteeSecond() { + $name_deleter = 'config_test.dynamic.deleter'; + $name_deletee = 'config_test.dynamic.deletee'; + $storage = $this->container->get('config.storage'); + + $uuid = $this->container->get('uuid'); + + $values_deleter = array( + 'id' => 'deleter', + 'label' => 'Deleter', + 'weight' => 0, + 'uuid' => $uuid->generate(), + // Add a dependency on deletee, to make sure this delete is synced first. + 'dependencies' => array( + 'entity' => array($name_deletee), + ), + ); + $storage->write($name_deleter, $values_deleter); + $values_deletee = array( + 'id' => 'deletee', + 'label' => 'Deletee', + 'weight' => 0, + 'uuid' => $uuid->generate(), + ); + $storage->write($name_deletee, $values_deletee); + + // Import. + $this->configImporter->reset()->import(); + + $entity_storage = \Drupal::entityManager()->getStorage('config_test'); + $this->assertFalse($entity_storage->load('deleter')); + $this->assertFalse($entity_storage->load('deletee')); + // The deletee entity does not exist as the delete worked and although the + // delete occurred in \Drupal\config_test\Entity\ConfigTest::postDelete() + // this does not matter. + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 0); } /** @@ -251,6 +523,8 @@ function testUpdated() { // Verify that there is nothing more to import. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); + $logs = $this->configImporter->getErrors(); + $this->assertEqual(count($logs), 0); } } diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/Entity/ConfigTest.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/Entity/ConfigTest.php index d5217bf..1f44f4f 100644 --- a/core/modules/config/tests/config_test/lib/Drupal/config_test/Entity/ConfigTest.php +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/Entity/ConfigTest.php @@ -10,6 +10,7 @@ use Drupal\Core\Config\Entity\ConfigEntityBase; use Drupal\config_test\ConfigTestInterface; use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Entity\EntityStorageInterface; /** * Defines the ConfigTest configuration entity. @@ -109,6 +110,37 @@ public static function sort(ConfigEntityInterface $a, ConfigEntityInterface $b) /** * {@inheritdoc} */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + // Used to test secondary writes during config sync. + if ($this->id() == 'primary') { + $secondary = $storage->create(array( + 'id' => 'secondary', + 'label' => 'Secondary Default', + )); + $secondary->save(); + } + if ($this->id() == 'deleter') { + $deletee = $storage->load('deletee'); + $deletee->delete(); + } + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + foreach ($entities as $entity) { + if ($entity->id() == 'deleter') { + $deletee = $storage->load('deletee'); + $deletee->delete(); + } + } + } + + /** + * {@inheritdoc} + */ public function calculateDependencies() { parent::calculateDependencies(); foreach ($this->test_dependencies as $type => $deps) { diff --git a/core/modules/entity_reference/entity_reference.module b/core/modules/entity_reference/entity_reference.module index cce3e3f..846a723 100644 --- a/core/modules/entity_reference/entity_reference.module +++ b/core/modules/entity_reference/entity_reference.module @@ -72,6 +72,11 @@ function entity_reference_field_config_update(FieldConfigInterface $field) { return; } + if ($field->isSyncing()) { + // Don't change anything during a configuration sync. + return; + } + if ($field->getSetting('target_type') == $field->original->getSetting('target_type')) { // Target type didn't change. return; diff --git a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php index d2388f7..b0485c4 100644 --- a/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php +++ b/core/modules/field/lib/Drupal/field/Tests/FieldImportDeleteTest.php @@ -7,6 +7,8 @@ namespace Drupal\field\Tests; +use Drupal\Component\Utility\String; + /** * Tests deleting fields and instances as part of config import. */ @@ -31,13 +33,21 @@ public static function getInfo() { * Tests deleting fields and instances as part of config import. */ public function testImportDelete() { + // At this point there are 5 field configuration objects in the active + // storage. + // - field.field.entity_test.field_test_import + // - field.field.entity_test.field_test_import_2 + // - field.instance.entity_test.entity_test.field_test_import + // - field.instance.entity_test.entity_test.field_test_import_2 + // - field.instance.entity_test.test_bundle.field_test_import_2 + $field_name = 'field_test_import'; $field_id = "entity_test.$field_name"; $field_name_2 = 'field_test_import_2'; $field_id_2 = "entity_test.$field_name_2"; - $instance_id = "entity_test.test_bundle.$field_name"; - $instance_id_2a = "entity_test.test_bundle.$field_name_2"; - $instance_id_2b = "entity_test.test_bundle_2.$field_name_2"; + $instance_id = "entity_test.entity_test.$field_name"; + $instance_id_2a = "entity_test.entity_test.$field_name_2"; + $instance_id_2b = "entity_test.test_bundle.$field_name_2"; $field_config_name = "field.field.$field_id"; $field_config_name_2 = "field.field.$field_id_2"; $instance_config_name = "field.instance.$instance_id"; @@ -45,7 +55,7 @@ public function testImportDelete() { $instance_config_name_2b = "field.instance.$instance_id_2b"; // Create a second bundle for the 'Entity test' entity type. - entity_test_create_bundle('test_bundle_2'); + entity_test_create_bundle('test_bundle'); // Import default config. $this->installConfig(array('field_test_config')); @@ -57,11 +67,14 @@ public function testImportDelete() { $active = $this->container->get('config.storage'); $staging = $this->container->get('config.storage.staging'); $this->copyConfig($active, $staging); - $staging->delete($field_config_name); - $staging->delete($field_config_name_2); - $staging->delete($instance_config_name); - $staging->delete($instance_config_name_2a); - $staging->delete($instance_config_name_2b); + $this->assertTrue($staging->delete($field_config_name), String::format('Deleted field: !field', array('!field' => $field_config_name))); + $this->assertTrue($staging->delete($field_config_name_2), String::format('Deleted field: !field', array('!field' => $field_config_name_2))); + $this->assertTrue($staging->delete($instance_config_name), String::format('Deleted field instance: !field_instance', array('!field_instance' => $instance_config_name))); + $this->assertTrue($staging->delete($instance_config_name_2a), String::format('Deleted field instance: !field_instance', array('!field_instance' => $instance_config_name_2a))); + $this->assertTrue($staging->delete($instance_config_name_2b), String::format('Deleted field instance: !field_instance', array('!field_instance' => $instance_config_name_2b))); + + $deletes = $this->configImporter()->getUnprocessedConfiguration('delete'); + $this->assertEqual(count($deletes), 5, 'Importing configuration will delete 3 field instances and 2 fields.'); // Import the content of the staging directory. $this->configImporter()->import(); diff --git a/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php b/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php index 365a8aa..2c475a5 100644 --- a/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php +++ b/core/modules/filter/lib/Drupal/filter/Entity/FilterFormat.php @@ -239,7 +239,7 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { // Clear the filter cache whenever a text format is updated. Cache::deleteTags(array('filter_format' => $this->id())); } - else { + elseif (!$this->isSyncing()) { // Default configuration of modules and installation profiles is allowed // to specify a list of user roles to grant access to for the new format; // apply the defined user role permissions when a new format is inserted diff --git a/core/modules/image/image.module b/core/modules/image/image.module index dc25998..61b30b9 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -387,6 +387,10 @@ function image_entity_presave(EntityInterface $entity) { return; } + if ($field->isSyncing()) { + return; + } + $fid = $entity->settings['default_image']['fid']; if ($fid) { $original_fid = isset($entity->original) ? $entity->original->settings['default_image']['fid'] : NULL; diff --git a/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php b/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php index 7bd7537..6ac64a6 100644 --- a/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php +++ b/core/modules/image/lib/Drupal/image/Entity/ImageStyle.php @@ -105,7 +105,9 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { // The old image style name needs flushing after a rename. $this->original->flush(); // Update field instance settings if necessary. - static::replaceImageStyle($this); + if (!$this->isSyncing()) { + static::replaceImageStyle($this); + } } else { // Flush image style when updating without changing the name. @@ -126,7 +128,7 @@ public static function postDelete(EntityStorageInterface $storage, array $entiti // Check whether field instance settings need to be updated. // In case no replacement style was specified, all image fields that are // using the deleted style are left in a broken state. - if ($new_id = $style->getReplacementID()) { + if (!$style->isSyncing() && $new_id = $style->getReplacementID()) { // The deleted ID is still set as originalID. $style->setName($new_id); static::replaceImageStyle($style); diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module index 76da041..47b5813 100644 --- a/core/modules/menu/menu.module +++ b/core/modules/menu/menu.module @@ -133,6 +133,11 @@ function menu_menu_insert(Menu $menu) { if (\Drupal::moduleHandler()->moduleExists('block')) { \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); } + + if ($menu->isSyncing()) { + return; + } + // Make sure the menu is present in the active menus variable so that its // items may appear in the menu active trail. // See menu_set_active_menu_names(). @@ -322,6 +327,9 @@ function menu_node_update(EntityInterface $node) { * Implements hook_node_type_insert(). */ function menu_node_type_insert(NodeTypeInterface $type) { + if ($type->isSyncing()) { + return; + } \Drupal::config('menu.entity.node.' . $type->id()) ->set('available_menus', array('main')) ->set('parent', 'main:0') @@ -332,6 +340,9 @@ function menu_node_type_insert(NodeTypeInterface $type) { * Implements hook_node_type_delete(). */ function menu_node_type_delete(NodeTypeInterface $type) { + if ($type->isSyncing()) { + return; + } \Drupal::config('menu.entity.node.' . $type->id())->delete(); } diff --git a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php index 50f2eb1..4005305 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/TestBase.php @@ -1527,7 +1527,8 @@ public function configImporter() { $this->container->get('lock'), $this->container->get('config.typed'), $this->container->get('module_handler'), - $this->container->get('theme_handler') + $this->container->get('theme_handler'), + $this->container->get('string_translation') ); } // Always recalculate the changelist when called. diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Vocabulary.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Vocabulary.php index f5a7ba7..ec46b6b 100644 --- a/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Vocabulary.php +++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Entity/Vocabulary.php @@ -102,7 +102,7 @@ public function postSave(EntityStorageInterface $storage, $update = TRUE) { if (!$update) { entity_invoke_bundle_hook('create', 'taxonomy_term', $this->id()); } - elseif ($this->getOriginalId() != $this->id()) { + elseif ($this->getOriginalId() != $this->id() && !$this->isSyncing()) { // Reflect machine name changes in the definitions of existing 'taxonomy' // fields. $field_ids = array(); @@ -153,6 +153,13 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie public static function postDelete(EntityStorageInterface $storage, array $entities) { parent::postDelete($storage, $entities); + // Reset caches. + $storage->resetCache(array_keys($entities)); + + if (reset($entities)->isSyncing()) { + return; + } + $vocabularies = array(); foreach ($entities as $vocabulary) { $vocabularies[$vocabulary->id()] = $vocabulary->id();