From c655b16803817faddc974187e6d22dd4433ce863 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Thu, 28 Apr 2016 11:58:55 +0100
Subject: [PATCH] Issue #2711645 by alexpott, dawehner:
 ConfigInstaller::isSyncing() should return true always during a config sync

---
 .../lib/Drupal/Core/Config/ConfigImporter.php |  6 +-
 .../Config/Entity/ConfigEntityStorage.php     | 26 ++++-
 .../tests/config_test/config_test.hooks.inc   | 44 ++++++++-
 .../tests/config_test/config_test.module      | 11 ---
 .../Core/Config/ConfigImporterTest.php        | 94 +++++++++++++++++++
 5 files changed, 160 insertions(+), 21 deletions(-)

diff --git a/core/lib/Drupal/Core/Config/ConfigImporter.php b/core/lib/Drupal/Core/Config/ConfigImporter.php
index bd9af28e3363..25071b006faa 100644
--- a/core/lib/Drupal/Core/Config/ConfigImporter.php
+++ b/core/lib/Drupal/Core/Config/ConfigImporter.php
@@ -484,14 +484,17 @@ public function import() {
    */
   public function doSyncStep($sync_step, &$context) {
     if (!is_array($sync_step) && method_exists($this, $sync_step)) {
+      \Drupal::service('config.installer')->setSyncing(TRUE);
       $this->$sync_step($context);
     }
     elseif (is_callable($sync_step)) {
+      \Drupal::service('config.installer')->setSyncing(TRUE);
       call_user_func_array($sync_step, array(&$context, $this));
     }
     else {
       throw new \InvalidArgumentException('Invalid configuration synchronization step');
     }
+    \Drupal::service('config.installer')->setSyncing(FALSE);
   }
 
   /**
@@ -778,7 +781,6 @@ protected function processExtension($type, $op, $name) {
     // Set the config installer to use the sync directory instead of the
     // extensions own default config directories.
     \Drupal::service('config.installer')
-      ->setSyncing(TRUE)
       ->setSourceStorage($this->storageComparer->getSourceStorage());
     if ($type == 'module') {
       $this->moduleInstaller->$op(array($name), FALSE);
@@ -805,8 +807,6 @@ protected function processExtension($type, $op, $name) {
     }
 
     $this->setProcessedExtension($type, $op, $name);
-    \Drupal::service('config.installer')
-      ->setSyncing(FALSE);
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
index 95d481146540..3834dae082a4 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -382,8 +382,7 @@ protected function getQueryServiceName() {
    * {@inheritdoc}
    */
   public function importCreate($name, Config $new_config, Config $old_config) {
-    $entity = $this->createFromStorageRecord($new_config->get());
-    $entity->setSyncing(TRUE);
+    $entity = $this->_doCreateFromStorageRecord($new_config->get(), TRUE);
     $entity->save();
     return TRUE;
   }
@@ -425,6 +424,27 @@ public function importRename($old_name, Config $new_config, Config $old_config)
    * {@inheritdoc}
    */
   public function createFromStorageRecord(array $values) {
+    return $this->_doCreateFromStorageRecord($values);
+  }
+
+  /**
+   * Helps create a configuration entity from storage values.
+   *
+   * Allows the configuration entity storage to massage storage values before
+   * creating an entity.
+   *
+   * @param array $values
+   *   The array of values from the configuration storage.
+   * @param bool $is_syncing
+   *   Is the configuration entity being created as part of a config sync.
+   *
+   * @return ConfigEntityInterface
+   *   The configuration entity.
+   *
+   * @see \Drupal\Core\Config\Entity\ConfigEntityStorageInterface::createFromStorageRecord()
+   * @see \Drupal\Core\Config\Entity\ImportableEntityStorageInterface::importCreate()
+   */
+  protected function _doCreateFromStorageRecord(array $values, $is_syncing = FALSE) {
     // Assign a new UUID if there is none yet.
     if ($this->uuidKey && $this->uuidService && !isset($values[$this->uuidKey])) {
       $values[$this->uuidKey] = $this->uuidService->generate();
@@ -432,6 +452,7 @@ public function createFromStorageRecord(array $values) {
     $data = $this->mapFromStorageRecords(array($values));
     $entity = current($data);
     $entity->original = clone $entity;
+    $entity->setSyncing($is_syncing);
     $entity->enforceIsNew();
     $entity->postCreate($this);
 
@@ -439,6 +460,7 @@ public function createFromStorageRecord(array $values) {
     // entity object, for instance to fill-in default values.
     $this->invokeHook('create', $entity);
     return $entity;
+
   }
 
   /**
diff --git a/core/modules/config/tests/config_test/config_test.hooks.inc b/core/modules/config/tests/config_test/config_test.hooks.inc
index 80f33818d8d1..7608d067e95c 100644
--- a/core/modules/config/tests/config_test/config_test.hooks.inc
+++ b/core/modules/config/tests/config_test/config_test.hooks.inc
@@ -9,6 +9,8 @@
  * config_test entity hooks themselves.
  */
 
+use Drupal\config_test\Entity\ConfigTest;
+
 /**
  * Implements hook_config_test_load().
  */
@@ -16,37 +18,69 @@ function config_test_config_test_load() {
   $GLOBALS['hook_config_test']['load'] = __FUNCTION__;
 }
 
+/**
+ * Implements hook_ENTITY_TYPE_create() for 'config_test'.
+ */
+function config_test_config_test_create(ConfigTest $config_test) {
+  if (\Drupal::state()->get('config_test.prepopulate')) {
+    $config_test->set('foo', 'baz');
+  }
+  _config_test_update_is_syncing_store('create', $config_test);
+}
+
 /**
  * Implements hook_config_test_presave().
  */
-function config_test_config_test_presave() {
+function config_test_config_test_presave(ConfigTest $config_test) {
   $GLOBALS['hook_config_test']['presave'] = __FUNCTION__;
+  _config_test_update_is_syncing_store('presave', $config_test);
 }
 
 /**
  * Implements hook_config_test_insert().
  */
-function config_test_config_test_insert() {
+function config_test_config_test_insert(ConfigTest $config_test) {
   $GLOBALS['hook_config_test']['insert'] = __FUNCTION__;
+  _config_test_update_is_syncing_store('insert', $config_test);
 }
 
 /**
  * Implements hook_config_test_update().
  */
-function config_test_config_test_update() {
+function config_test_config_test_update(ConfigTest $config_test) {
   $GLOBALS['hook_config_test']['update'] = __FUNCTION__;
+  _config_test_update_is_syncing_store('update', $config_test);
 }
 
 /**
  * Implements hook_config_test_predelete().
  */
-function config_test_config_test_predelete() {
+function config_test_config_test_predelete(ConfigTest $config_test) {
   $GLOBALS['hook_config_test']['predelete'] = __FUNCTION__;
+  _config_test_update_is_syncing_store('predelete', $config_test);
 }
 
 /**
  * Implements hook_config_test_delete().
  */
-function config_test_config_test_delete() {
+function config_test_config_test_delete(ConfigTest $config_test) {
   $GLOBALS['hook_config_test']['delete'] = __FUNCTION__;
+  _config_test_update_is_syncing_store('delete', $config_test);
+}
+
+/**
+ * Helper function for testing hooks during configuration sync.
+ *
+ * @param string $hook
+ *   The fired hook.
+ * @param \Drupal\config_test\Entity\ConfigTest $config_test
+ *   The ConfigTest entity.
+ */
+function _config_test_update_is_syncing_store($hook, ConfigTest $config_test) {
+  $current_value = \Drupal::state()->get('config_test.store_isSyncing', FALSE);
+  if ($current_value !== FALSE) {
+    $current_value['global_state::' . $hook] = \Drupal::isConfigSyncing();
+    $current_value['entity_state::' . $hook] = $config_test->isSyncing();
+    \Drupal::state()->set('config_test.store_isSyncing', $current_value);
+  }
 }
diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module
index 08aeac61e943..5008575425ed 100644
--- a/core/modules/config/tests/config_test/config_test.module
+++ b/core/modules/config/tests/config_test/config_test.module
@@ -5,8 +5,6 @@
  * Provides Config module hook implementations for testing purposes.
  */
 
-use Drupal\config_test\Entity\ConfigTest;
-
 require_once dirname(__FILE__) . '/config_test.hooks.inc';
 
 /**
@@ -17,15 +15,6 @@ function config_test_cache_flush() {
   $GLOBALS['hook_cache_flush'] = __FUNCTION__;
 }
 
-/**
- * Implements hook_ENTITY_TYPE_create() for 'config_test'.
- */
-function config_test_config_test_create(ConfigTest $config_test) {
-  if (\Drupal::state()->get('config_test.prepopulate')) {
-    $config_test->set('foo', 'baz');
-  }
-}
-
 /**
  * Implements hook_entity_type_alter().
  */
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
index 1117f347a22b..945e994e3ba7 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterTest.php
@@ -686,4 +686,98 @@ public function testConfigGetConfigDirectory() {
     }
   }
 
+  /**
+   * Tests the isSyncing flags.
+   */
+  public function testIsSyncingInHooks() {
+    $dynamic_name = 'config_test.dynamic.dotted.default';
+    $storage = $this->container->get('config.storage');
+
+    // Verify the default configuration values exist.
+    $config = $this->config($dynamic_name);
+    $this->assertSame('dotted.default', $config->get('id'));
+
+    // Delete the config so that create hooks will fire.
+    $storage->delete($dynamic_name);
+    \Drupal::state()->set('config_test.store_isSyncing', []);
+    $this->configImporter->reset()->import();
+
+    // The values of the syncing values should be stored in state by
+    // config_test_config_test_create().
+    $state = \Drupal::state()->get('config_test.store_isSyncing');
+    $this->assertTrue($state['global_state::create'], '\Drupal::isConfigSyncing() returns TRUE');
+    $this->assertTrue($state['entity_state::create'], 'ConfigEntity::isSyncing() returns TRUE');
+    $this->assertTrue($state['global_state::presave'], '\Drupal::isConfigSyncing() returns TRUE');
+    $this->assertTrue($state['entity_state::presave'], 'ConfigEntity::isSyncing() returns TRUE');
+    $this->assertTrue($state['global_state::insert'], '\Drupal::isConfigSyncing() returns TRUE');
+    $this->assertTrue($state['entity_state::insert'], 'ConfigEntity::isSyncing() returns TRUE');
+
+    // Cause a config update so update hooks will fire.
+    $config = $this->config($dynamic_name);
+    $config->set('label', 'A new name')->save();
+    \Drupal::state()->set('config_test.store_isSyncing', []);
+    $this->configImporter->reset()->import();
+
+    // The values of the syncing values should be stored in state by
+    // config_test_config_test_create().
+    $state = \Drupal::state()->get('config_test.store_isSyncing');
+    $this->assertTrue($state['global_state::presave'], '\Drupal::isConfigSyncing() returns TRUE');
+    $this->assertTrue($state['entity_state::presave'], 'ConfigEntity::isSyncing() returns TRUE');
+    $this->assertTrue($state['global_state::update'], '\Drupal::isConfigSyncing() returns TRUE');
+    $this->assertTrue($state['entity_state::update'], 'ConfigEntity::isSyncing() returns TRUE');
+
+    // Cause a config delete so delete hooks will fire.
+    $sync = $this->container->get('config.storage.sync');
+    $sync->delete($dynamic_name);
+    \Drupal::state()->set('config_test.store_isSyncing', []);
+    $this->configImporter->reset()->import();
+
+    // The values of the syncing values should be stored in state by
+    // config_test_config_test_create().
+    $state = \Drupal::state()->get('config_test.store_isSyncing');
+    $this->assertTrue($state['global_state::predelete'], '\Drupal::isConfigSyncing() returns TRUE');
+    $this->assertTrue($state['entity_state::predelete'], 'ConfigEntity::isSyncing() returns TRUE');
+    $this->assertTrue($state['global_state::delete'], '\Drupal::isConfigSyncing() returns TRUE');
+    $this->assertTrue($state['entity_state::delete'], 'ConfigEntity::isSyncing() returns TRUE');
+  }
+
+  /**
+   * Tests that the isConfigSyncing flag is cleanup after an invalid step.
+   */
+  public function testInvalidStep() {
+    $this->assertFalse(\Drupal::isConfigSyncing(), 'Before an import \Drupal::isConfigSyncing() returns FALSE');
+    $context = [];
+    try {
+      $this->configImporter->doSyncStep('a_non_existent_step', $context);
+      $this->fail('Expected \InvalidArgumentException thrown');
+    }
+    catch (\InvalidArgumentException $e) {
+      $this->pass('Expected \InvalidArgumentException thrown');
+    }
+    $this->assertFalse(\Drupal::isConfigSyncing(), 'After an invalid step \Drupal::isConfigSyncing() returns FALSE');
+  }
+
+  /**
+   * Tests that the isConfigSyncing flag is set correctly during a custom step.
+   */
+  public function testCustomStep() {
+    $this->assertFalse(\Drupal::isConfigSyncing(), 'Before an import \Drupal::isConfigSyncing() returns FALSE');
+    $context = [];
+    $this->configImporter->doSyncStep([self::class, 'customStep'], $context);
+    $this->assertTrue($context['is_syncing'], 'Inside a custom step \Drupal::isConfigSyncing() returns TRUE');
+    $this->assertFalse(\Drupal::isConfigSyncing(), 'After an valid custom step \Drupal::isConfigSyncing() returns FALSE');
+  }
+
+  /**
+   * Helper meothd to test custom config installer steps.
+   *
+   * @param array $context
+   *   Batch context.
+   * @param \Drupal\Core\Config\ConfigImporter $importer
+   *   The config importer.
+   */
+  public static function customStep(array &$context, ConfigImporter $importer) {
+    $context['is_syncing'] = \Drupal::isConfigSyncing();
+  }
+
 }
-- 
GitLab