Expose API to sort arbitrary config arrays: TypedConfigManager::getCanonicalRepresentation() should override its parent

Created on 1 September 2021, over 3 years ago
Updated 2 February 2024, 12 months ago

Problem/Motivation

In Drupal 8.8 we added the configuration storage transformation API. It was a big improvement and it opened new possibilities which lots of contrib modules are now taking advantage of.

However, when manipulating the configuration - other than crudely adding or removing whole config arrays - there is a problem: When new things are added to the array there is no generic way to sort the config such that there is no unnecessary diff.

Furthermore, the config values that are validated are different from the values that are saved, and this is a huge DX problem. It means "magic" is happening, that makes the config system more difficult to understand.

Steps to reproduce

Create a event subscriber that subscribes to the config export and import event.
On export remove some part of the configuration.
On import add it back.
Observe that on the config import screen you most likely get a diff when there shouldn't be any.

As a practical example: This would happen if the $settings['config_exclude_modules'] would update the config as if the modules had been uninstalled ✨ Change config as if the modules defined in config_exclude_modules had been uninstalled Needs work instead of just removing all config that depends on that module.

Proposed resolution

Use the existing \Drupal\Core\TypedData\TypedDataManagerInterface::getCanonicalRepresentation() that #2488568: Add a TypedDataManagerInterface and use it for typed parameters β†’ added but #1866610: Introduce Kwalify-inspired schema format for configuration β†’ did not override for TypedConfigManagerInterface (which extends that interface).



πŸ‘† That last sentence is what the current merge request does!

The goal is that there is a canonical sorting of config arrays that will make sure, no matter how you shuffle your array in a import storage transformation, drupal can sort it so that only meaningful differences are shown.

Remaining tasks

Reliable sorting for config:

moving sorting to a service.

Wim already did some great work on this, in #3416287: ConfigConfigurator should be insensitive to key order when comparing active config to recipe config β†’ (see https://git.drupalcode.org/project/distributions_recipes/-/merge_request..., starting from this commit). So the first step is to extract that work into a new merge request, in this issue.

User interface changes

no more unnecessary diffs.

API changes

Yes; the exact extent of the changes is TBD.

Data model changes

none

Release notes snippet

tbd

πŸ“Œ Task
Status

Needs work

Version

11.0 πŸ”₯

Component
ConfigurationΒ  β†’

Last updated 4 days ago

Created by

πŸ‡¨πŸ‡­Switzerland bircher πŸ‡¨πŸ‡Ώ

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡¨πŸ‡­Switzerland bircher πŸ‡¨πŸ‡Ώ

    Re-titling the issue, because we don't necessarily need a new service it can be used with the typed config.

  • Assigned to wim leers
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    This blocks Recipes; it prevents Recipes from reliably detecting whether config is the same or has changed: #3416287: ConfigConfigurator should be insensitive to key order when comparing active config to recipe config β†’ .

  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    To be clear, this is a stable blocker for the Recipes initiative. (In practical terms, it is a soft blocker.)

    Initially in #3416287: ConfigConfigurator should be insensitive to key order when comparing active config to recipe config β†’ , I identified a workaround that will suffice in the short term, and we can commit that to the Recipes repository for now; see #3416287-27: ConfigConfigurator should be insensitive to key order when comparing active config to recipe config β†’ .

  • Status changed to Needs work 12 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    Adjusting the remaining tasks; let's start by bringing Wim's excellent work from #3416287: ConfigConfigurator should be insensitive to key order when comparing active config to recipe config β†’ into a new MR on this issue.

  • Merge request !6369Resolve #3230826 "Sorted config" β†’ (Open) created by wim leers
  • Pipeline finished with Success
    12 months ago
    Total: 597s
    #84265
  • Pipeline finished with Failed
    12 months ago
    Total: 479s
    #84277
  • Pipeline finished with Failed
    12 months ago
    #84292
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    It's gonna be tricky to exactly match the expectations of SchemaConfigListenerTestTrait, i.e. this rather odd output:

        catch (SchemaIncompleteException $e) {
          $this->assertEquals("Schema errors for config_test.types with the following errors: config_test.types:array variable type is integer but applied schema class is Drupal\Core\Config\Schema\Sequence, config_test.types:foo missing schema, 0 [foo] 'foo' is not a supported key.", $e->getMessage());
        }
    

    … I'll need some more time tomorrow to figure out a way forward for that πŸ€“

    That being said, 99% of tests are passing already 😊

  • Pipeline finished with Failed
    12 months ago
    Total: 615s
    #84318
  • Pipeline finished with Failed
    12 months ago
    Total: 599s
    #84655
  • Pipeline finished with Failed
    12 months ago
    Total: 559s
    #84665
  • Pipeline finished with Failed
    12 months ago
    Total: 628s
    #84678
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Following up on #12: I had two choices:

    1. either not changing any of those assertions and hence being forced to save invalid config data first, because that is necessary for keeping SchemaCheckTrait::checkValue unchanged: it must be able to find flaws in stored config data
    2. or changing the assertions and disallowing invalid config data to be saved β€” but this would be a disruptive BC break and a DX regression (not all info about invalid config data/missing schema at once, but one-by-one)

    Did that in f2c1390d2ecb0ffde75514e2e75012211d7b98cd.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    The 3 remaining failures are all in Views test coverage. They all relate to ViewExecutable wrapping View (the "storage" config entity) and both of them manipulating (in these 3 tests and only these 3!) settings on-the-fly and expecting ViewExecutable, View, display plugin, style plugin etc. all to remain in sync thanks to references to a private array 😱

    See \Drupal\views\DisplayPluginCollection::initializePlugin(), which does

        // Retrieve and initialize the new display handler with data.
        $display = &$this->view->storage->getDisplay($display_id);
    …
        $this->pluginInstances[$display_id]->initDisplay($this->view, $display);
    

    there is a reference to the raw array in the config from within the ViewExecutable, this one is the culprit. Automatically resetting this is what I did in 60631295ad052a64464d14b758cc3c2d99d55db1 and that fixes 1 of the 3 failures.

  • Pipeline finished with Failed
    12 months ago
    Total: 576s
    #84833
  • Pipeline finished with Canceled
    12 months ago
    Total: 294s
    #84867
  • Pipeline finished with Failed
    12 months ago
    Total: 514s
    #84869
  • Pipeline finished with Success
    12 months ago
    Total: 777s
    #84936
  • Issue was unassigned.
  • Status changed to Needs review 12 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Whew. There are 2 functional Views tests that are doing very unholy things β€” the 1 (out of 3) that was fixed in #14 was the least bad one. See https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co... + https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co... for two examples of View and ViewExecutable being expected to magically remain in sync with in-memory-only overrides (i.e. modifications that are not part of the View config entity, but manipulate runtime state). These two needed tiny tweaks to have more reasonable expectations of the connection between View and ViewExecutable.

  • Pipeline finished with Failed
    12 months ago
    Total: 611s
    #84941
  • Pipeline finished with Failed
    12 months ago
    Total: 1207s
    #84954
  • Status changed to Needs work 12 months ago
  • πŸ‡ΊπŸ‡ΈUnited States phenaproxima Massachusetts

    This looks really good to me. I have a few minor things (mostly commenting-phrasing nits), as I usually do, but in general I understand this -- and when dealing with the typed config system, that's saying something -- and feel okay with it.

    That said, I think the final sign-off here should belong to @bircher, who understands this more deeply than I and whose blessing would give me more confidence.

  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    TIL \Drupal\Core\TypedData\TypedDataManagerInterface::getCanonicalRepresentation() was introduced in #2488568: Add a TypedDataManagerInterface and use it for typed parameters β†’ β€” we should be able to use that instead, rather than expanding the API surface? πŸ€“

  • Assigned to wim leers
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
  • Pipeline finished with Success
    12 months ago
    Total: 640s
    #85455
  • Pipeline finished with Success
    12 months ago
    Total: 516s
    #85476
  • Issue was unassigned.
  • Status changed to Needs review 12 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί

    Whew, #17 was tricky, but I managed to do it β€” with a much simpler and less disruptive MR as the result! 🀩 I'm kinda proud of this one πŸ€“

    https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co... has 13 files changed, 103 insertions(+), 103 deletions(-) as the diffstat and causes:

    … same exact diffstat, but 3 fewer files touched, and no BC breaks/contrib disruption πŸ₯³

    And actually, looks like @bircher kinda predicted this years ago, because in the issue summary he stated .

  • Pipeline finished with Failed
    12 months ago
    Total: 185s
    #85547
  • Pipeline finished with Failed
    12 months ago
    Total: 673s
    #85909
  • Pipeline finished with Failed
    12 months ago
    Total: 521s
    #86117
  • Pipeline finished with Failed
    12 months ago
    Total: 638s
    #86125
  • Pipeline finished with Failed
    12 months ago
    Total: 836s
    #86156
  • Pipeline finished with Failed
    12 months ago
    #86175
  • Pipeline finished with Failed
    12 months ago
    Total: 488s
    #86190
  • Pipeline finished with Failed
    12 months ago
    Total: 504s
    #86196
  • Pipeline finished with Failed
    12 months ago
    Total: 570s
    #86205
  • Pipeline finished with Failed
    12 months ago
    Total: 588s
    #86218
  • Pipeline finished with Failed
    12 months ago
    Total: 595s
    #86226
  • Assigned to wim leers
  • Status changed to Needs work 12 months ago
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
    1. The only failing test was ConfigEntityAdapterTest::testValidate() β€” ConfigEntityAdapter was added in #1818574: Support config entities in typed data EntityAdapter β†’ . With very thin test coverage, because config entities were even less validatable back then than they are today.
    2. In fixing that, it turns out that https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co... was insufficient: that only caught the case of assigning the empty array [] to e.g. StringData or BooleanData. What's needed is to instead ensure that as soon as any PrimitiveInterface contains non-compliant data, the raw value is used in the canonical representation.
    3. But then another test starts failing, because there is one test case in all of core that requires FloatData and IntegerData which have the empty string '' assigned to be converted to NULL πŸ€ͺ Still, always better to minimize disruption, so matched that logic from StorableConfigBase::castValue(), by treating those as valid values instead (which exactly matches the intent) in PrimitiveTypeConstraintValidator and allowing (Float|Integer)Data::getCastedValue() to do whichever casting they please. πŸ‘
    4. Then a wide range of additional failures occurred … because apparently a LOT of tests rely on BooleanData accepting 'TRUE'/'FALSE' as an equivalent of TRUE/FALSE, despite no explicit PHP logic existing to do that 😳 … nor test coverage πŸ˜…
    5. I was able to work around that, but then followed a huge range of migration test failures (and some node type-related test failures). Lots of migrations are migrating nonsense  β€” for example it's setting the type: boolean settings.alt_field (for the "image" field type's settings) to Test alt, which obviously is not a boolean πŸ˜…)
    6. … to be continued tomorrow

    The end result will be a more consistent (same representation during validation + saving β€” issue summary updated to reflect this) and understandable (no global knowledge in a protected method in a base class but each TypedData object has knowledge about itself) config system.

    It also makes it possible to deprecate weird things (see the boolean/float/integer special cases above) at some point in the future.

  • Pipeline finished with Failed
    12 months ago
    Total: 637s
    #86283
  • Pipeline finished with Failed
    12 months ago
    Total: 616s
    #86592
  • Pipeline finished with Failed
    12 months ago
    Total: 509s
    #86603
  • Pipeline finished with Success
    12 months ago
    Total: 562s
    #86610
  • Pipeline finished with Success
    12 months ago
    Total: 1544s
    #86621
  • Pipeline finished with Failed
    12 months ago
    Total: 589s
    #86633
  • Pipeline finished with Success
    12 months ago
    Total: 999s
    #86659
  • Pipeline finished with Failed
    12 months ago
    Total: 611s
    #86663
  • Pipeline finished with Failed
    12 months ago
    Total: 541s
    #86682
  • Pipeline finished with Failed
    12 months ago
    Total: 883s
    #86757
  • Pipeline finished with Failed
    12 months ago
    Total: 511s
    #86775
  • πŸ‡§πŸ‡ͺBelgium wim leers Ghent πŸ‡§πŸ‡ͺπŸ‡ͺπŸ‡Ί
    1. Found a similar problem with Integer and Float (https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co...) and another one for Integer (https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co...)
    2. That got us again to a single test failure. This time in \Drupal\KernelTests\Core\TypedData\TypedDataTest::testGetAndSet(). This bit:
          // Boolean type.
          $typed_data = $this->createTypedData(['type' => 'boolean'], TRUE);
          $this->assertInstanceOf(BooleanInterface::class, $typed_data);
      …
          $typed_data->setValue('invalid');
          $this->assertEquals(1, $typed_data->validate()->count(), 'Validation detected invalid value.');
      

      … this is only passing because it's testing an alternate reality: there's LOTS of examples of string values being assigned to type: boolean and the configuration system silently ignoring this (not even reporting it via SchemaCheckTrait), because as soon as we disallow this (by reverting the boolean-related additions to PrimitiveTypeConstraintValidator), lots of tests fail.

      Which means the only choice we have here is to change this expectation, so that it matches reality.

      P.S.: in reading that method, it's clear there's many more known bugs in this area, for example it points to the following @todo: πŸ› DateTimeIso8601::setDateTime() vs. DateTimeItem::schema(): (at least) one of them is broken Needs work .

    3. NOW IT IS GREEN AGAIN!
    4. While working on this, I discovered how closely related this issue is to πŸ“Œ Config export key order is not predictable, use config schema to order keys for maps Fixed . This builds on top of what that did, and simplifies it; it makes the overall system more consistent. I bet it'd have been far more difficult if that had not happened! In principle, many of the test changes that happened there should no longer be necessary, as well as the various explicit pieces of sorting logic. To prove this, https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co... reverted the changes that issue made to ConfigEntityBase::preSave() to sort dependencies correctly.
    5. Nope, that didn't work out because ConfigInstaller does $entity->trustData()->save();, which bypasses the call to ::getCanonicalRepresentation(). That's easily mitigated by making \Drupal\Core\Config\Config::save() always use ::getCanonicalRepresentation(), even for trusted data: https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co...
    6. This unveiled another range of invalid config in tests, and hence tests of an alternate reality rather than the real world. πŸ€ͺ
    7. So another range of trivial test expectation adjustments was needed: https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co..., https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co..., https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co....
    8. This also needed to invert a single assertion that #2432791: Skip Config::save schema validation of config data for trusted data β†’ introduced β€” the test suite takes just as long as to run as in HEAD after the change to ALWAYS save with the canonical representation! Turns out that #2432791 especially made core faster thanks to not using config schema in ::toArray() πŸ‘ That inverted expectation: https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co... (which related to #2432791: Skip Config::save schema validation of config data for trusted data β†’ )
    9. Down to zero kernel test failures again, now a handful of functional test failures. The first two were for InstallerExistingConfigTestBase … turns out the multilingual fixture in there is wrong, it should've been updated in πŸ“Œ Get rid of using 'views.settings':skip_cache in ViewsData Fixed but wasn't.

    (To be continued. Down to 2 functional test failures. And a LOT more sanity.)

  • Pipeline finished with Failed
    12 months ago
    Total: 717s
    #86781
  • Pipeline finished with Failed
    12 months ago
    Total: 556s
    #86860
  • Pipeline finished with Success
    12 months ago
    Total: 531s
    #87788
Production build 0.71.5 2024