diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php index 1303b7f..9bf424b 100644 --- a/core/lib/Drupal/Core/Config/ConfigImporter.php +++ b/core/lib/Drupal/Core/Config/ConfigImporter.php @@ -127,6 +127,13 @@ class ConfigImporter extends DependencySerialization { protected $processedSystemTheme = FALSE; /** + * The config importer log. + * + * @var \Drupal\Core\Config\ConfigImporterLogInterface + */ + protected $log; + + /** * Constructs a configuration import object. * * @param \Drupal\Core\Config\StorageComparerInterface $storage_comparer @@ -158,6 +165,35 @@ public function __construct(StorageComparerInterface $storage_comparer, EventDis } /** + * 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; + } + + /** * Gets the configuration storage comparer. * * @return \Drupal\Core\Config\StorageComparerInterface @@ -415,7 +451,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 +490,26 @@ 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) { + if (!$this->log($e->getMessage(), 'error')) { + throw $e; + } + else { + // Error for that operation was logged, mark it as processed. + $this->setProcessedConfiguration($op, $name); + } } } @@ -506,6 +560,63 @@ 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->setProcessed($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->log(String::format('Deleted and replaced configuration entity "@name"', array('@name' => $name)), 'warning'); + } + else { + $this->storageComparer->getTargetStorage()->delete($name); + } + return TRUE; + } + break; + + case 'update': + if (!$target_exists) { + $this->log(String::format('Update target "@name" is missing.', array('@name' => $name)), 'error'); + return FALSE; + } + break; + } + return TRUE; + } + + /** * Writes a configuration change from the source to the target storage. * * @param string $op @@ -574,7 +685,6 @@ protected function importInvokeOwner($op, $name) { $this->setProcessedConfiguration($op, $name); return TRUE; } - return FALSE; } /** diff --git a/core/lib/Drupal/Core/Config/ConfigImporterLogArray.php b/core/lib/Drupal/Core/Config/ConfigImporterLogArray.php new file mode 100644 index 0000000..7f9eb26 --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigImporterLogArray.php @@ -0,0 +1,38 @@ +logs[] = array( + 'message' => $message, + 'severity' => $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 new file mode 100644 index 0000000..cd461fb --- /dev/null +++ b/core/lib/Drupal/Core/Config/ConfigImporterLogInterface.php @@ -0,0 +1,19 @@ +state = $state; + } + + /** + * {@inheritdoc} + */ + public function log($message, $severity) { + $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->state->get(static::STATE_KEY, array()); + } + +} 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..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 ff154b7..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,9 @@ use Drupal\Core\Config\ConfigImporter; use Drupal\Core\Config\ConfigImporterException; +use Drupal\Core\Config\ConfigImporterLogArray; use Drupal\Core\Config\StorageComparer; +use Drupal\Component\Utility\String; use Drupal\simpletest\DrupalUnitTestBase; /** @@ -25,6 +27,13 @@ class ConfigImporterTest extends DrupalUnitTestBase { protected $configImporter; /** + * Config importer log. + * + * @var \Drupal\Core\Config\ConfigImporterLogInterface + */ + protected $configLog; + + /** * Modules to enable. * * @var array @@ -66,6 +75,9 @@ function setUp() { $this->container->get('module_handler'), $this->container->get('theme_handler') ); + $this->configLog = new ConfigImporterLogArray(); + $this->configImporter->setLog($this->configLog); + $this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.staging')); } /** @@ -149,6 +161,8 @@ function testDeleted() { $this->assertTrue(isset($GLOBALS['hook_config_test']['delete'])); $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); + $logs = $this->configLog->getLogMessages(); + $this->assertEqual(count($logs), 0); } /** @@ -196,6 +210,201 @@ function testNew() { // Verify that there is nothing more to import. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); + $logs = $this->configLog->getLogMessages(); + $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->configLog->getLogMessages(); + $this->assertEqual(count($logs), 1); + $this->assertEqual($logs[0]['message'], String::format('Deleted and replaced configuration entity "@name"', array('@name' => $name_secondary))); + $this->assertEqual($logs[0]['severity'], 'warning'); + } + + /** + * 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->configLog->getLogMessages(); + $this->assertEqual(count($logs), 1); + $this->assertEqual($logs[0]['message'], String::format('config_test entity with ID @name already exists.', array('@name' => 'secondary'))); + $this->assertEqual($logs[0]['severity'], 'error'); + + } + + /** + * Tests that secondary updates for deleted files work as expected. + */ + function testSecondaryUpdateDeletedDeleterFirst() { + $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(), + ); + $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); + + // 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 failed. Should the + // importer attempt a create instead? + + $logs = $this->configLog->getLogMessages(); + $this->assertEqual(count($logs), 1); + $this->assertEqual($logs[0]['message'], String::format('Update target "@name" is missing.', array('@name' => $name_deletee))); + $this->assertEqual($logs[0]['severity'], 'error'); + } + + /** + * 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. + $logs = $this->configLog->getLogMessages(); + $this->assertEqual(count($logs), 0); } /** @@ -251,6 +460,8 @@ function testUpdated() { // Verify that there is nothing more to import. $this->assertFalse($this->configImporter->hasUnprocessedConfigurationChanges()); + $logs = $this->configLog->getLogMessages(); + $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..b244234 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,24 @@ 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 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..0728ebf 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()->getUnprocessed('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 a57fe55..9367cf0 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -373,6 +373,7 @@ function image_filter_keyword($value, $current_pixels, $new_pixels) { */ function image_entity_presave(EntityInterface $entity) { $field = FALSE; + $entity_type_id = $entity->getEntityTypeId(); if ($entity_type_id == 'field_instance_config') { $field = $entity->getField(); @@ -385,6 +386,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/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(); 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(); + } + +}