Problem/Motivation

In \Drupal\Core\Config\Entity\ConfigEntityBase::preSave() we consult all plugin collections for their up-to-date configuration before saving.

Proposed resolution

We need to do the same thing before serialization.

Remaining tasks

Write tests.

User interface changes

N/A

API changes

N/A, unless you count removing unwanted/unavoidable exceptions as an API change

Data model changes

N/A

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

tim.plunkett created an issue. See original summary.

tim.plunkett’s picture

Status: Active » Needs review
FileSize
1.88 KB
EclipseGc’s picture

I'm roughly +10000000000 to fixing this. We're having to code around this limitation in page_manager work right now, and it was simple once we figured it out, but then again, most things are.

Eclipse

tim.plunkett’s picture

Actually there's on more step. Unfortunately this cuts down on the reusability part. Ditching the protected method.

tim.plunkett’s picture

Should really just write those tests...

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
    @@ -365,6 +365,31 @@ public function preSave(EntityStorageInterface $storage) {
    +        foreach ($vars as $key => $value) {
    +          if ($plugin_collection === $value) {
    +            $keys_to_unset[] = $key;
    +          }
    +        }
    

    This could be replaced by array_filter() easily.

  2. +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
    @@ -365,6 +365,31 @@ public function preSave(EntityStorageInterface $storage) {
    +    foreach ($keys_to_unset as $key) {
    +      unset($vars[array_search($key, $vars)]);
    +    }
    

    Can't you use an array_diff here?

tim.plunkett’s picture

Assigned: Unassigned » tim.plunkett

Working on that, and tests

tim.plunkett’s picture

Not sure if the array_filter/array_keys are better, but the array_diff is definitely an improvement.

The last submitted patch, 8: 2650588-entity-plugin-collection-8-FAIL.patch, failed testing.

alexpott’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
    @@ -365,6 +365,32 @@ public function preSave(EntityStorageInterface $storage) {
    +        $keys_to_unset += array_filter($vars, function ($value) use ($plugin_collection) {
    +          return $plugin_collection === $value;
    +        });
    ...
    +    if (!empty($keys_to_unset)) {
    +      $vars = array_diff($vars, array_keys($keys_to_unset));
    +    }
    

    This doesn't look tested. I'm missing something, I can't see why it is here.

  2. +++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
    @@ -308,6 +311,37 @@ public function testCalculateDependenciesWithThirdPartySettings() {
    +    $this->assertFalse(in_array('pluginCollection', $entity_keys));
    

    $this->assertContains() has a better message when it goes wrong.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
972 bytes
4.32 KB

#2 is the code that tests #1.

A plugin collection is usually represented in two properties. The array of config used to initialize it, and the property containing the object

class Foo extends ConfigEntityBase implements EntityWithPluginCollectionInterface {
  protected $pluginConfig = [];
  protected $pluginCollection;

  public function getPluginCollections() {
    if (!$this->pluginCollection) {
      $this->pluginCollection = new DefaultLazyPluginCollection(\Drupal::service('some_plugin_manager'), $this->pluginConfig);
    }
    return $this->pluginCollection;
  }

}

The first half of __sleep() handles updating the $this->pluginConfig with the correct values.
The second half is to unset $this->pluginCollection

Thanks for the tip about assertContains/assertNotContains, always nice to use the most appropriate assert for a given situation.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityBaseUnitTest.php
@@ -308,6 +311,36 @@ public function testCalculateDependenciesWithThirdPartySettings() {
+    // Ensure the plugin collection is not stored.
+    $this->assertNotContains('pluginCollection', $entity->__sleep());

Should we call serialize() here instead?

tim.plunkett’s picture

I vote no: I think it's fine to call __ methods in tests, when that is the exact functionality we are testing.
Also, it would be a pain to assert on the serialized result.

tim.plunkett’s picture

Contrib (including two places in page_manager) will need to work around this until it goes in

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work

Shouldn't this be extracted into a helper method that \Drupal\Core\Config\Entity\ConfigEntityBase::preSave() can then also call?

alexpott’s picture

Status: Needs work » Needs review

@Wim Leers I wondered the smae thing put see #4

Wim Leers’s picture

  1. +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
    @@ -365,6 +365,32 @@ public function preSave(EntityStorageInterface $storage) {
    +      foreach ($this->getPluginCollections() as $plugin_config_key => $plugin_collection) {
    +        // Save any changes to the plugin configuration to the entity.
    +        $this->set($plugin_config_key, $plugin_collection->getConfiguration());
    

    So this portion is the same in ::sleep() and ::preSave().

  2. +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
    @@ -365,6 +365,32 @@ public function preSave(EntityStorageInterface $storage) {
    +        // If the plugin collections are stored as properties on the entity,
    +        // mark them to be unset.
    +        $keys_to_unset += array_filter($vars, function ($value) use ($plugin_collection) {
    +          return $plugin_collection === $value;
    +        });
    

    It's this portion that is different.

Note that in #4, the unset() calls were happening in the logic itself. Now it's just being returned, to be unset later. So I think it is possible to have it in a shared helper method. In the ::__sleep() method, you use the return value to unset those entity properties, in the ::preSave() method you just don't do anything with that return value.

Wim Leers’s picture

+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
@@ -355,6 +349,44 @@ public function preSave(EntityStorageInterface $storage) {
+  }
+
+
+  /**

Oops, one newline too many here.

alexpott’s picture

+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
@@ -355,6 +349,44 @@ public function preSave(EntityStorageInterface $storage) {
+      $vars = get_object_vars($this);
...
+        $keys_to_unset += array_filter($vars, function ($value) use ($plugin_collection) {
+          return $plugin_collection === $value;
+        });

Well now we're doing this on every save - for no reason...

Wim Leers’s picture

tim.plunkett’s picture

+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php
@@ -318,13 +318,7 @@ protected function getTypedConfig() {
+    $this->syncPluginCollectionConfiguration(FALSE);

@@ -355,6 +349,48 @@ public function preSave(EntityStorageInterface $storage) {
+    $plugin_collection_properties = $this->syncPluginCollectionConfiguration(TRUE);
...
+   * @return string[]|null
+   *   The entity properties to that store plugin collections, or null.
+   */
+  protected function syncPluginCollectionConfiguration($determine_plugin_collection_properties) {

string[]|null, or TRUE/FALSE?

Honestly I tried to do it and decided it was cleaner and easier to understand by duplicating the code...

alexpott’s picture

I agree with @tim.plunkett

it was cleaner and easier to understand by duplicating the code...

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
4.32 KB

Alright. Re-uploading #11 unchanged.

At first I had NFI what this patch was doing, now I do, and I now agree that the duplicated complexity is minimal.

Sorry for the detour!

mondrake’s picture

I tested the patch in #23 with the test-only patch in #2393387-32: Add test for editing image effect when configuration form is Ajax enabled, and it seems that it fixes that issue too. Bumping to major as the other issue is triaged major.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

I think this is eligible for 8.0.x since implementing a magic method is not an API change. Also this is fixing a core bug and unblocking contrib.

Committed 9bd6902 and pushed to 8.0.x and 8.1.x. Thanks!

  • alexpott committed 3c24816 on 8.1.x
    Issue #2650588 by tim.plunkett, Wim Leers, alexpott, dawehner: Entities...

  • alexpott committed 9bd6902 on 8.0.x
    Issue #2650588 by tim.plunkett, Wim Leers, alexpott, dawehner: Entities...
mondrake’s picture

I can confirm #2393387: Add test for editing image effect when configuration form is Ajax enabled was fixed by this commit, great!

Would appreciate a review of #2393387-49: Add test for editing image effect when configuration form is Ajax enabled to decide whether to close the issue or still add some test to prevent regressions.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

Berdir’s picture

For the record, if anyone else ends up looking at this.

This did break one of our modules because we had this logic duplicated. But then the key already didn't exist in it anymore, so array_search() returned FALSE, it casted that to 0 and removed the id from the serialized entity: #2693501: Fix failing ProcessingWebTest

The code here could have the same problem, although I guess we can rely on those being set in $vars. We might still want to make it type safe and check that the return of array_search() is not FALSE.