Issue #3587744: Make DomainConfigOverrideEditable::save() write a sparse, cast-aware diff against base

See #3587744.

What this fixes

DomainConfigOverrideEditable::save() previously wrote $moduleOverrides verbatim. For per-key set() callers (ConfigFormBase) that worked because each set() populated $moduleOverrides. For setData() callers (ConfigEntityStorage::doSave(), drush, custom code) the in-memory data mutation never reached $moduleOverrides and the changes were silently dropped.

save() now derives the override row as a sparse diff against base on every save path — both flows produce the same result and the #3547172 contract holds (the override row holds only what really differs from base; setting a key back to base drops it).

How it works

  1. $base_data constructor argument on DomainConfigOverrideEditable. The factory passes the override-free base payload at construction time.
  2. castThroughSchema() helper runs the typed-config schema cast on both $this->data and $base_data before the diff so values are compared like-for-like. Without that, base data stored under an older schema (e.g. string "1" for a key now typed boolean) would strict-mismatch the freshly-cast new value and produce spurious override entries that grow across upgrades.
  3. DiffArray::diffAssocRecursive($cast_data, $cast_base) is what lands in $moduleOverrides on every save. The legacy "write moduleOverrides verbatim" path is gone.
  4. updateExistingKeysInNestedArray() deprecated — no longer used internally, no drop-in replacement in core (different shape than DiffArray::diffAssocRecursive).

Read path

Unchanged. loadOverrides() returns the raw stored override; core's Config::setOverriddenData() continues merging via NestedArray::mergeDeepArray.

Known limitation: sequence shrinks

Per-domain overrides on type: sequence configs cannot drop trailing items from base. This matches core's mergeDeepArray semantics across every override mechanism (settings.php, module overrides, …) — not a divergence introduced here. Documented in docs/domain_config/index.md with three working alternatives, and pinned by testSequenceShrinkInheritsCoreMergeSemantics so future refactors that try to "fix" this unilaterally fail the test and force the read-side conversation.

Deprecations (cleanup in domain:4.0.0)

  • DomainConfigOverrideEditable::__construct(... ?array $base_data = NULL) — passing NULL triggers E_USER_DEPRECATED. The default goes away and the parameter becomes required in 4.0.0.
  • DomainConfigOverrideEditable::updateExistingKeysInNestedArray() — removed in 4.0.0.

Tests

DomainConfigOverrideEditableTest (kernel, 5 tests / 22 assertions):

  • testSetDataPreservesExistingOverrideAndAddsNewDiff — the BlockListBuilder::submitForm regression.
  • testSetDataDropsKeysThatNoLongerDifferFromBase — the #3547172 contract.
  • testSequenceShrinkInheritsCoreMergeSemantics — pins both storage and runtime read to core's merge semantics.
  • testSetPerKeyFlowProducesSparseOverride — non-regression for ConfigFormBase per-key flow.
  • testSchemaDriftDoesNotProduceSpuriousDiff — empirically verified to fail when castThroughSchema() is disabled on $base_data.

Documentation

docs/domain_config/index.md gains:

  • Override write semantics — the diff-against-base contract.
  • Known limitation: sequence shrinks — explanation and workarounds.

Out of scope (deferred)

The EntityForm-based per-domain experimental flow (form-alter EntityForm branch, ParamConverter, SettingsForm checkbox, list-builder swap) is entirely deferred to the domain_config_entity_ui submodule in domain_extras (!32). That submodule sits behind explicit per-entity-type opt-in via its own SettingsForm; this MR ships pure write-side correctness with no UI changes and no opt-in surface.

Edited by Frank Mably

Merge request reports

Loading