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.
- Validating
\Drupal\Core\TypedData\Validation\RecursiveContextualValidator::validateNode()does:// Pass the canonical representation of the data as validated value to // constraint validators, such that they do not have to care about Typed // Data. $value = $typed_data_manager->getCanonicalRepresentation($data); … $this->validateConstraints($value, $cache_key, $constraints);- Saving
\Drupal\Core\Config\Config::save()does:$this->data = $this->castValue(NULL, $this->data); … $this->storage->write($this->name, $this->data);
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 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).
Add a new service, or add a method to the typed config manager with the signature:
public function sortConfigArray(string $name, array $config): array (the name obviously is up for debate, normalizing is also used, or castSorted.)
Basically what \Drupal\Core\Config\StorableConfigBase::castValue is doing as a service. (which if it was a method on the typed config manager could just be replaced by that)
👆 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:
- #2852557: Config export key order is not predictable, use config schema to order keys for maps
- #2855675: Add orderby key to all sequences in core
- #2625212: Add ConfigSchemaChecker to development.services.yml
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
Issue fork drupal-3230826
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
bircherComment #7
bircherRe-titling the issue, because we don't necessarily need a new service it can be used with the typed config.
Comment #8
wim leersThis 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.
Comment #9
phenaproximaTo 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.
Comment #10
phenaproximaAdjusting 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.
Comment #12
wim leersIt's gonna be tricky to exactly match the expectations of
SchemaConfigListenerTestTrait, i.e. this rather odd output:… I'll need some more time tomorrow to figure out a way forward for that 🤓
That being said, 99% of tests are passing already 😊
Comment #13
wim leersFollowing up on #12: I had two choices:
SchemaCheckTrait::checkValueunchanged: it must be able to find flaws in stored config dataSo went with choice 1 in
f2c1390d2ecb0ffde75514e2e75012211d7b98cd.Comment #14
wim leersThe 3 remaining failures are all in Views test coverage. They all relate to
ViewExecutablewrappingView(the "storage" config entity) and both of them manipulating (in these 3 tests and only these 3!) settings on-the-fly and expectingViewExecutable,View, display plugin, style plugin etc. all to remain in sync thanks to references to a private array 😱See
\Drupal\views\DisplayPluginCollection::initializePlugin(), which doesthere 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 in60631295ad052a64464d14b758cc3c2d99d55db1and that fixes 1 of the 3 failures.Comment #15
wim leersWhew. 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
ViewandViewExecutablebeing expected to magically remain in sync with in-memory-only overrides (i.e. modifications that are not part of theViewconfig entity, but manipulate runtime state). These two needed tiny tweaks to have more reasonable expectations of the connection betweenViewandViewExecutable.Comment #16
phenaproximaThis 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.
Comment #17
wim leersTIL
\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? 🤓Comment #18
wim leersComment #19
wim leersWhew, #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... (EDIT: oops, some leftovers snuck in, so I removed them) has
13 files changed, 77 insertions(+), 103 deletions(-)as the diffstat and causes:…
same exactsmaller diffstat, but34 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 .
Comment #20
wim leersConfigEntityAdapterTest::testValidate()—ConfigEntityAdapterwas 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.[]to e.g.StringDataorBooleanData. What's needed is to instead ensure that as soon as anyPrimitiveInterfacecontains non-compliant data, the raw value is used in the canonical representation.FloatDataandIntegerDatawhich have the empty string''assigned to be converted toNULL🤪 Still, always better to minimize disruption, so matched that logic fromStorableConfigBase::castValue(), by treating those as valid values instead (which exactly matches the intent) inPrimitiveTypeConstraintValidatorand allowing(Float|Integer)Data::getCastedValue()to do whichever casting they please. 👍BooleanDataaccepting'TRUE'/'FALSE'as an equivalent ofTRUE/FALSE, despite no explicit PHP logic existing to do that 😳 … nor test coverage 😅type: booleansettings.alt_field(for the "image" field type's settings) toTest alt, which obviously is not a boolean 😅)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
TypedDataobject 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.
Comment #21
wim leersIntegerandFloat(https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co...) and another one forInteger(https://git.drupalcode.org/project/drupal/-/merge_requests/6369/diffs?co...)\Drupal\KernelTests\Core\TypedData\TypedDataTest::testGetAndSet(). This bit:… this is only passing because it's testing an alternate reality: there's LOTS of examples of string values being assigned to
type: booleanand the configuration system silently ignoring this (not even reporting it viaSchemaCheckTrait), because as soon as we disallow this (by reverting the boolean-related additions toPrimitiveTypeConstraintValidator), 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: #2716891: DateTimeIso8601::setDateTime() vs. DateTimeItem::schema(): (at least) one of them is broken.ConfigEntityBase::preSave()to sort dependencies correctly.That is now entirely done based on config schema, in a single place, no extra logic needed anymore! To avoid ballooning scope, for now I'm avoiding removing explicit sorts that were added there, but I'd be happy to do so :)ConfigInstallerdoes$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...::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)InstallerExistingConfigTestBase… turns out the multilingual fixture in there is wrong, it should've been updated in #2541074: Get rid of using 'views.settings':skip_cache in ViewsData but wasn't.(To be continued. Down to 2 functional test failures. And a LOT more sanity.)
Comment #23
bbralaRebased, lets see if this is still green.