From e182bb335042f8845c0953fa0e97bd05daa1cb76 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Fri, 1 Mar 2024 11:18:58 +0000
Subject: [PATCH] Issue #3415041 by alexpott, Wim Leers: Creating blocks that
 depend on content via the config installer often generates a warning

---
 .../Core/Config/Entity/ConfigEntityBase.php   |  4 +-
 .../ckeditor_test/ckeditor_test.info.yml      |  6 ++
 .../ckeditor_test/ckeditor_test.module        | 17 ++++
 .../src/Kernel/SmartDefaultSettingsTest.php   | 77 +++++++--------
 .../ConfigImporterMissingContentTest.php      | 90 +++++++++++++++++-
 .../Entity/ConfigEntityBaseUnitTest.php       | 94 +++++++++++++++++--
 6 files changed, 240 insertions(+), 48 deletions(-)
 create mode 100644 core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.info.yml
 create mode 100644 core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module

diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
index be5840f77b47..ad50387947ff 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
@@ -158,7 +158,7 @@ public function get($property_name) {
    * {@inheritdoc}
    */
   public function set($property_name, $value) {
-    if ($this instanceof EntityWithPluginCollectionInterface) {
+    if ($this instanceof EntityWithPluginCollectionInterface && !$this->isSyncing()) {
       $plugin_collections = $this->getPluginCollections();
       if (isset($plugin_collections[$property_name])) {
         // If external code updates the settings, pass it along to the plugin.
@@ -288,7 +288,7 @@ public function preSave(EntityStorageInterface $storage) {
     /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage */
     parent::preSave($storage);
 
-    if ($this instanceof EntityWithPluginCollectionInterface) {
+    if ($this instanceof EntityWithPluginCollectionInterface && !$this->isSyncing()) {
       // Any changes to the plugin configuration must be saved to the entity's
       // copy as well.
       foreach ($this->getPluginCollections() as $plugin_config_key => $plugin_collection) {
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.info.yml b/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.info.yml
new file mode 100644
index 000000000000..b58e70e3d881
--- /dev/null
+++ b/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.info.yml
@@ -0,0 +1,6 @@
+name: CKEditor Test
+type: module
+description: "Provides test a test editor that with the ID ckeditor."
+package: Testing
+dependencies:
+  - drupal:editor_test
diff --git a/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module b/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module
new file mode 100644
index 000000000000..6073a13a4249
--- /dev/null
+++ b/core/modules/ckeditor5/tests/modules/ckeditor_test/ckeditor_test.module
@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Implements hooks for the CKEditor test module.
+ */
+
+declare(strict_types = 1);
+
+/**
+ * Implements hook_editor_info_alter().
+ */
+function ckeditor_test_editor_info_alter(array &$editors) {
+  // Drupal 9 used to have an editor called ckeditor. Copy the Unicorn editor to
+  // it to be able to test upgrading to CKEditor 5.
+  $editors['ckeditor'] = $editors['unicorn'];
+}
diff --git a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
index d5f50e4a98a7..6fe3d0e48ced 100644
--- a/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
+++ b/core/modules/ckeditor5/tests/src/Kernel/SmartDefaultSettingsTest.php
@@ -79,6 +79,8 @@ class SmartDefaultSettingsTest extends KernelTestBase {
     'views',
     'dblog',
     'help',
+    'editor_test',
+    'ckeditor_test',
   ];
 
   /**
@@ -110,7 +112,7 @@ protected function setUp(): void {
           ],
         ],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
     Editor::create([
       'format' => 'minimal_ckeditor_wrong_allowed_html',
       'editor' => 'ckeditor',
@@ -129,26 +131,25 @@ protected function setUp(): void {
         ],
         'plugins' => [],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
 
     FilterFormat::create(
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.full_html.yml')
-    )
-      ->setSyncing(TRUE)
-      ->save();
+    )->save();
+
     Editor::create(
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.full_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $basic_html_format = Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.basic_html.yml');
-    FilterFormat::create($basic_html_format)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format)->save();
     Editor::create(
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     FilterFormat::create(
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/filter.format.restricted_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $basic_html_format_without_image_uploads = $basic_html_format;
     $basic_html_format_without_image_uploads['name'] .= ' (without image uploads)';
@@ -158,7 +159,7 @@ protected function setUp(): void {
       ['format' => 'basic_html_without_image_uploads']
       +
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setImageUploadSettings(['status' => FALSE])->setSyncing(TRUE)->save();
+    )->setImageUploadSettings(['status' => FALSE])->save();
 
     $allowed_html_parents = ['filters', 'filter_html', 'settings', 'allowed_html'];
     $current_value = NestedArray::getValue($basic_html_format, $allowed_html_parents);
@@ -167,58 +168,58 @@ protected function setUp(): void {
     $basic_html_format_without_h4_h6['name'] .= ' (without H4 and H6)';
     $basic_html_format_without_h4_h6['format'] = 'basic_html_without_h4_h6';
     NestedArray::setValue($basic_html_format_without_h4_h6, $allowed_html_parents, $new_value);
-    FilterFormat::create($basic_html_format_without_h4_h6)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_without_h4_h6)->save();
     Editor::create(
       ['format' => 'basic_html_without_h4_h6']
       +
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $new_value = str_replace(['<h2 id> ', '<h3 id> ', '<h4 id> ', '<h5 id> ', '<h6 id> '], '', $current_value);
     $basic_html_format_without_headings = $basic_html_format;
     $basic_html_format_without_headings['name'] .= ' (without H*)';
     $basic_html_format_without_headings['format'] = 'basic_html_without_headings';
     NestedArray::setValue($basic_html_format_without_headings, $allowed_html_parents, $new_value);
-    FilterFormat::create($basic_html_format_without_headings)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_without_headings)->save();
     Editor::create(
       ['format' => 'basic_html_without_headings']
       +
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $basic_html_format_with_pre = $basic_html_format;
     $basic_html_format_with_pre['name'] .= ' (with <pre>)';
     $basic_html_format_with_pre['format'] = 'basic_html_with_pre';
     NestedArray::setValue($basic_html_format_with_pre, $allowed_html_parents, $current_value . ' <pre>');
-    FilterFormat::create($basic_html_format_with_pre)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_with_pre)->save();
     Editor::create(
       ['format' => 'basic_html_with_pre']
       +
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $basic_html_format_with_h1 = $basic_html_format;
     $basic_html_format_with_h1['name'] .= ' (with <h1>)';
     $basic_html_format_with_h1['format'] = 'basic_html_with_h1';
     NestedArray::setValue($basic_html_format_with_h1, $allowed_html_parents, $current_value . ' <h1>');
-    FilterFormat::create($basic_html_format_with_h1)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_with_h1)->save();
     Editor::create(
       ['format' => 'basic_html_with_h1']
       +
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $new_value = str_replace('<p>', '<p class="text-align-center text-align-justify">', $current_value);
     $basic_html_format_with_alignable_p = $basic_html_format;
     $basic_html_format_with_alignable_p['name'] .= ' (with alignable paragraph support)';
     $basic_html_format_with_alignable_p['format'] = 'basic_html_with_alignable_p';
     NestedArray::setValue($basic_html_format_with_alignable_p, $allowed_html_parents, $new_value);
-    FilterFormat::create($basic_html_format_with_alignable_p)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_with_alignable_p)->save();
     Editor::create(
       ['format' => 'basic_html_with_alignable_p']
       +
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $basic_html_format_with_media_embed = $basic_html_format;
     $basic_html_format_with_media_embed['name'] .= ' (with Media Embed support)';
@@ -227,7 +228,7 @@ protected function setUp(): void {
     $basic_html_format_with_media_embed['filters']['media_embed'] = ['status' => TRUE];
     $new_value = $current_value . ' <drupal-media data-entity-type data-entity-uuid data-align data-caption alt>';
     NestedArray::setValue($basic_html_format_with_media_embed, $allowed_html_parents, $new_value);
-    FilterFormat::create($basic_html_format_with_media_embed)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_with_media_embed)->save();
     $basic_html_editor_with_media_embed = Editor::create(
       ['format' => 'basic_html_with_media_embed']
       +
@@ -238,7 +239,7 @@ protected function setUp(): void {
     // pre-existing toolbar item group labeled "Media".
     $settings['toolbar']['rows'][0][3]['items'][] = 'DrupalMediaLibrary';
     $basic_html_editor_with_media_embed->setSettings($settings);
-    $basic_html_editor_with_media_embed->setSyncing(TRUE)->save();
+    $basic_html_editor_with_media_embed->save();
 
     $basic_html_format_with_media_embed_view_mode_invalid = $basic_html_format_with_media_embed;
     $basic_html_format_with_media_embed_view_mode_invalid['name'] = ' (with Media Embed support, view mode enabled but no view modes configured)';
@@ -246,7 +247,7 @@ protected function setUp(): void {
     $current_value_media_embed = NestedArray::getValue($basic_html_format_with_media_embed, $allowed_html_parents);
     $new_value = str_replace('<drupal-media data-entity-type data-entity-uuid data-align data-caption alt>', '<drupal-media data-entity-type data-entity-uuid data-align data-caption alt data-view-mode>', $current_value_media_embed);
     NestedArray::setValue($basic_html_format_with_media_embed_view_mode_invalid, $allowed_html_parents, $new_value);
-    FilterFormat::create($basic_html_format_with_media_embed_view_mode_invalid)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_with_media_embed_view_mode_invalid)->save();
     $basic_html_editor_with_media_embed_view_mode_enabled_no_view_modes_configured = Editor::create(
       ['format' => 'basic_html_with_media_embed_view_mode_enabled_no_view_modes_configured']
       +
@@ -257,24 +258,24 @@ protected function setUp(): void {
     // pre-existing toolbar item group labeled "Media".
     $settings['toolbar']['rows'][0][3]['items'][] = 'DrupalMediaLibrary';
     $basic_html_editor_with_media_embed_view_mode_enabled_no_view_modes_configured->setSettings($settings);
-    $basic_html_editor_with_media_embed_view_mode_enabled_no_view_modes_configured->setSyncing(TRUE)->save();
+    $basic_html_editor_with_media_embed_view_mode_enabled_no_view_modes_configured->save();
 
     $new_value = str_replace('<img src alt height width data-entity-type data-entity-uuid data-align data-caption>', '<img src alt height width data-*>', $current_value);
     $basic_html_format_with_any_data_attr = $basic_html_format;
     $basic_html_format_with_any_data_attr['name'] .= ' (with any data-* attribute on images)';
     $basic_html_format_with_any_data_attr['format'] = 'basic_html_with_any_data_attr';
     NestedArray::setValue($basic_html_format_with_any_data_attr, $allowed_html_parents, $new_value);
-    FilterFormat::create($basic_html_format_with_any_data_attr)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_with_any_data_attr)->save();
     Editor::create(
       ['format' => 'basic_html_with_any_data_attr']
       +
       Yaml::parseFile('core/modules/ckeditor5/tests/fixtures/ckeditor4_config/editor.editor.basic_html.yml')
-    )->setSyncing(TRUE)->save();
+    )->save();
 
     $basic_html_format_with_media_embed_view_mode_enabled_two_view_modes_configured = $basic_html_format_with_media_embed_view_mode_invalid;
     $basic_html_format_with_media_embed_view_mode_enabled_two_view_modes_configured['name'] = ' (with Media Embed support, view mode enabled and two view modes configured )';
     $basic_html_format_with_media_embed_view_mode_enabled_two_view_modes_configured['format'] = 'basic_html_with_media_embed_view_mode_enabled_two_view_modes_configured';
-    FilterFormat::create($basic_html_format_with_media_embed_view_mode_enabled_two_view_modes_configured)->setSyncing(TRUE)->save();
+    FilterFormat::create($basic_html_format_with_media_embed_view_mode_enabled_two_view_modes_configured)->save();
     $basic_html_editor_with_media_embed_view_mode_enabled_two_view_modes_configured = Editor::create(
       ['format' => 'basic_html_with_media_embed_view_mode_enabled_two_view_modes_configured']
       +
@@ -285,21 +286,21 @@ protected function setUp(): void {
     // pre-existing toolbar item group labeled "Media".
     $settings['toolbar']['rows'][0][3]['items'][] = 'DrupalMediaLibrary';
     $basic_html_editor_with_media_embed_view_mode_enabled_two_view_modes_configured->setSettings($settings);
-    $basic_html_editor_with_media_embed_view_mode_enabled_two_view_modes_configured->setSyncing(TRUE)->save();
+    $basic_html_editor_with_media_embed_view_mode_enabled_two_view_modes_configured->save();
     EntityViewMode::create([
       'id' => 'media.view_mode_1',
       'targetEntityType' => 'media',
       'status' => TRUE,
       'enabled' => TRUE,
       'label' => 'View Mode 1',
-    ])->setSyncing(TRUE)->save();
+    ])->save();
     EntityViewMode::create([
       'id' => 'media.view_mode_2',
       'targetEntityType' => 'media',
       'status' => TRUE,
       'enabled' => TRUE,
       'label' => 'View Mode 2',
-    ])->setSyncing(TRUE)->save();
+    ])->save();
     $filter_format = FilterFormat::load('basic_html_with_media_embed_view_mode_enabled_two_view_modes_configured');
     $filter_format->setFilterConfig('media_embed', [
       'status' => TRUE,
@@ -311,7 +312,7 @@ protected function setUp(): void {
           'view_mode_2' => 'view_mode_2',
         ],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
 
     $filter_plugin_manager = $this->container->get('plugin.manager.filter');
     FilterFormat::create([
@@ -323,12 +324,12 @@ protected function setUp(): void {
           'settings' => $filter_plugin_manager->getDefinition('filter_html')['settings'],
         ],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
 
     FilterFormat::create([
       'format' => 'cke4_plugins_with_settings',
       'name' => 'All CKEditor 4 core plugins with settings',
-    ])->setSyncing(TRUE)->save();
+    ])->save();
     Editor::create([
       'format' => 'cke4_plugins_with_settings',
       'editor' => 'ckeditor',
@@ -368,7 +369,7 @@ protected function setUp(): void {
           ],
         ],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
 
     FilterFormat::create([
       'format' => 'cke4_stylescombo_span',
@@ -381,7 +382,7 @@ protected function setUp(): void {
           ] + $filter_plugin_manager->getDefinition('filter_html')['settings'],
         ],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
     Editor::create([
       'format' => 'cke4_stylescombo_span',
       'editor' => 'ckeditor',
@@ -404,12 +405,12 @@ protected function setUp(): void {
           ],
         ],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
 
     FilterFormat::create([
       'format' => 'cke4_contrib_plugins_now_in_core',
       'name' => 'All CKEditor 4 contrib plugins now in core',
-    ])->setSyncing(TRUE)->save();
+    ])->save();
     Editor::create([
       'format' => 'cke4_contrib_plugins_now_in_core',
       'editor' => 'ckeditor',
@@ -470,7 +471,7 @@ protected function setUp(): void {
           ],
         ],
       ],
-    ])->setSyncing(TRUE)->save();
+    ])->save();
   }
 
   /**
diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php
index b81b679f3295..d542d9a83d1f 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigImporterMissingContentTest.php
@@ -2,17 +2,36 @@
 
 namespace Drupal\KernelTests\Core\Config;
 
+use Drupal\block_content\Entity\BlockContent;
+use Drupal\block_content\Entity\BlockContentType;
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Render\PlainTextOutput;
+use Drupal\Core\Block\Plugin\Block\Broken;
 use Drupal\Core\Config\ConfigImporter;
 use Drupal\Core\Config\StorageComparer;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Logger\RfcLoggerTrait;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\block\Traits\BlockCreationTrait;
+use Psr\Log\LoggerInterface;
 
 /**
  * Tests importing configuration which has missing content dependencies.
  *
  * @group config
  */
-class ConfigImporterMissingContentTest extends KernelTestBase {
+class ConfigImporterMissingContentTest extends KernelTestBase implements LoggerInterface {
+  use BlockCreationTrait;
+  use RfcLoggerTrait;
+
+  /**
+   * The logged messages.
+   *
+   * @var string[]
+   */
+  protected $logMessages = [];
 
   /**
    * Config Importer object used for testing.
@@ -34,6 +53,15 @@ class ConfigImporterMissingContentTest extends KernelTestBase {
     'config_import_test',
   ];
 
+  /**
+   * {@inheritdoc}
+   */
+  public function register(ContainerBuilder $container) {
+    parent::register($container);
+    $container->register('logger.ConfigImporterMissingContentTest', __CLASS__)->addTag('logger');
+    $container->set('logger.ConfigImporterMissingContentTest', $this);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -107,4 +135,64 @@ public function testMissingContent() {
     $this->assertEquals([$entity_one->getConfigDependencyName(), $entity_two->getConfigDependencyName(), $entity_three->getConfigDependencyName()], $original_dynamic_data['dependencies']['content'], 'The imported configuration entity has the missing content entity dependency.');
   }
 
+  /**
+   * Tests the missing content, config import and the block plugin manager.
+   *
+   * @see \Drupal\Core\Config\ConfigImporter::processMissingContent()
+   * @see \Drupal\config_import_test\EventSubscriber
+   */
+  public function testMissingBlockContent() {
+    $this->enableModules([
+      'block',
+      'block_content',
+    ]);
+    $this->container->get('theme_installer')->install(['stark']);
+    $this->installEntitySchema('block_content');
+    // Create a block content type.
+    $block_content_type = BlockContentType::create([
+      'id' => 'test',
+      'label' => 'Test block content',
+      'description' => "Provides a block type",
+    ]);
+    $block_content_type->save();
+    // And a block content entity.
+    $block_content = BlockContent::create([
+      'info' => 'Prototype',
+      'type' => 'test',
+      // Set the UUID to make asserting against missing test easy.
+      'uuid' => '6376f337-fcbf-4b28-b30e-ed5b6932e692',
+    ]);
+    $block_content->save();
+    $plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid();
+    $block = $this->placeBlock($plugin_id);
+
+    $storage = $this->container->get('config.storage');
+    $sync = $this->container->get('config.storage.sync');
+
+    $this->copyConfig($storage, $sync);
+
+    $block->delete();
+    $block_content->delete();
+    $block_content_type->delete();
+
+    // Import.
+    $this->logMessages = [];
+    $config_importer = $this->configImporter();
+    $config_importer->import();
+    $this->assertNotContains('The "block_content:6376f337-fcbf-4b28-b30e-ed5b6932e692" was not found', $this->logMessages);
+
+    // Ensure the expected message is generated when creating an instance of the
+    // block.
+    $instance = $this->container->get('plugin.manager.block')->createInstance($plugin_id);
+    $this->assertContains('The "block_content:6376f337-fcbf-4b28-b30e-ed5b6932e692" was not found', $this->logMessages);
+    $this->assertInstanceOf(Broken::class, $instance);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function log($level, $message, array $context = []): void {
+    $this->logMessages[] = PlainTextOutput::renderFromHtml(new FormattableMarkup($message, $context));
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
index c1dac0ec459d..16fa33ca7a3e 100644
--- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
@@ -18,6 +18,7 @@
 use Drupal\Tests\Core\Plugin\Fixtures\TestConfigurablePlugin;
 use Drupal\Tests\UnitTestCase;
 use Drupal\TestTools\Random;
+use Prophecy\Argument;
 
 /**
  * @coversDefaultClass \Drupal\Core\Config\Entity\ConfigEntityBase
@@ -354,23 +355,23 @@ public function testSleepWithPluginCollections() {
     $instance = new TestConfigurablePlugin([], $instance_id, []);
 
     $plugin_manager = $this->prophesize(PluginManagerInterface::class);
-    $plugin_manager->createInstance($instance_id, ['id' => $instance_id])->willReturn($instance);
+    $plugin_manager->createInstance($instance_id, Argument::any())->willReturn($instance);
 
     // Also set up a container with the plugin manager so that we can assert
     // that the plugin manager itself is also not serialized.
     $container = TestKernel::setContainerWithKernel();
     $container->set('plugin.manager.foo', $plugin_manager->reveal());
 
-    $entity_values = ['the_plugin_collection_config' => [$instance_id => ['foo' => 'original_value']]];
+    $entity_values = ['the_plugin_collection_config' => [$instance_id => ['id' => $instance_id, 'foo' => 'original_value']]];
     $entity = new TestConfigEntityWithPluginCollections($entity_values, $this->entityTypeId);
     $entity->setPluginManager($plugin_manager->reveal());
 
     // After creating the entity, change the plugin configuration.
-    $instance->setConfiguration(['foo' => 'new_value']);
+    $instance->setConfiguration(['id' => $instance_id, 'foo' => 'new_value']);
 
     // After changing the plugin configuration, the entity still has the
     // original value.
-    $expected_plugin_config = [$instance_id => ['foo' => 'original_value']];
+    $expected_plugin_config = [$instance_id => ['id' => $instance_id, 'foo' => 'original_value']];
     $this->assertSame($expected_plugin_config, $entity->get('the_plugin_collection_config'));
 
     // Ensure the plugin collection and manager is not stored.
@@ -379,7 +380,7 @@ public function testSleepWithPluginCollections() {
     $this->assertNotContains('pluginManager', $vars);
     $this->assertSame(['pluginManager' => 'plugin.manager.foo'], $entity->get('_serviceIds'));
 
-    $expected_plugin_config = [$instance_id => ['foo' => 'new_value']];
+    $expected_plugin_config = [$instance_id => ['id' => $instance_id, 'foo' => 'new_value']];
     // Ensure the updated values are stored in the entity.
     $this->assertSame($expected_plugin_config, $entity->get('the_plugin_collection_config'));
   }
@@ -636,6 +637,85 @@ public function testToArraySchemaException() {
     $this->entity->toArray();
   }
 
+  /**
+   * @covers ::set
+   * @dataProvider providerTestSetAndPreSaveWithPluginCollections
+   */
+  public function testSetWithPluginCollections(bool $syncing, string $expected_value) {
+    $instance_id = 'the_instance_id';
+    $instance = new TestConfigurablePlugin(['foo' => 'original_value'], $instance_id, []);
+
+    $plugin_manager = $this->prophesize(PluginManagerInterface::class);
+    if ($syncing) {
+      $plugin_manager->createInstance(Argument::cetera())->shouldNotBeCalled();
+    }
+    else {
+      $plugin_manager->createInstance($instance_id, Argument::any())->willReturn($instance);
+    }
+
+    $entity_values = ['the_plugin_collection_config' => [$instance_id => ['id' => $instance_id, 'foo' => 'original_value']]];
+    $entity = new TestConfigEntityWithPluginCollections($entity_values, $this->entityTypeId);
+    $entity->setSyncing($syncing);
+    $entity->setPluginManager($plugin_manager->reveal());
+
+    // After creating the entity, change the configuration using the entity.
+    $entity->set('the_plugin_collection_config', [$instance_id => ['id' => $instance_id, 'foo' => 'new_value']]);
+
+    $this->assertSame($expected_value, $instance->getConfiguration()['foo']);
+  }
+
+  /**
+   * @covers ::preSave
+   * @dataProvider providerTestSetAndPreSaveWithPluginCollections
+   */
+  public function testPreSaveWithPluginCollections(bool $syncing, string $expected_value) {
+    $instance_id = 'the_instance_id';
+    $instance = new TestConfigurablePlugin(['foo' => 'original_value'], $instance_id, ['provider' => 'core']);
+
+    $plugin_manager = $this->prophesize(PluginManagerInterface::class);
+    if ($syncing) {
+      $plugin_manager->createInstance(Argument::cetera())->shouldNotBeCalled();
+    }
+    else {
+      $plugin_manager->createInstance($instance_id, Argument::any())->willReturn($instance);
+    }
+
+    $entity_values = ['the_plugin_collection_config' => [$instance_id => ['id' => $instance_id, 'foo' => 'original_value']]];
+    $entity = new TestConfigEntityWithPluginCollections($entity_values, $this->entityTypeId);
+    $entity->setSyncing($syncing);
+    $entity->setPluginManager($plugin_manager->reveal());
+
+    // After creating the entity, change the plugin configuration.
+    $instance->setConfiguration(['foo' => 'new_value']);
+
+    $query = $this->createMock('\Drupal\Core\Entity\Query\QueryInterface');
+    $storage = $this->createMock('\Drupal\Core\Config\Entity\ConfigEntityStorageInterface');
+
+    $query->expects($this->any())
+      ->method('execute')
+      ->willReturn([]);
+    $query->expects($this->any())
+      ->method('condition')
+      ->willReturn($query);
+    $storage->expects($this->any())
+      ->method('getQuery')
+      ->willReturn($query);
+    $storage->expects($this->any())
+      ->method('loadUnchanged')
+      ->willReturn($entity);
+
+    $entity->preSave($storage);
+
+    $this->assertSame($expected_value, $entity->get('the_plugin_collection_config')[$instance_id]['foo']);
+  }
+
+  public function providerTestSetAndPreSaveWithPluginCollections(): array {
+    return [
+      'Not syncing' => [FALSE, 'new_value'],
+      'Syncing' => [TRUE, 'original_value'],
+    ];
+  }
+
 }
 
 class TestConfigEntityWithPluginCollections extends ConfigEntityBaseWithPluginCollections {
@@ -644,7 +724,7 @@ class TestConfigEntityWithPluginCollections extends ConfigEntityBaseWithPluginCo
 
   protected $pluginManager;
 
-  protected $the_plugin_collection_config;
+  protected array $the_plugin_collection_config = [];
 
   public function setPluginManager(PluginManagerInterface $plugin_manager) {
     $this->pluginManager = $plugin_manager;
@@ -655,7 +735,7 @@ public function setPluginManager(PluginManagerInterface $plugin_manager) {
    */
   public function getPluginCollections() {
     if (!$this->pluginCollection) {
-      $this->pluginCollection = new DefaultLazyPluginCollection($this->pluginManager, ['the_instance_id' => ['id' => 'the_instance_id']]);
+      $this->pluginCollection = new DefaultLazyPluginCollection($this->pluginManager, $this->the_plugin_collection_config);
     }
     return ['the_plugin_collection_config' => $this->pluginCollection];
   }
-- 
GitLab