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
$base_dataconstructor argument onDomainConfigOverrideEditable. The factory passes the override-free base payload at construction time.castThroughSchema()helper runs the typed-config schema cast on both$this->dataand$base_databefore 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 typedboolean) would strict-mismatch the freshly-cast new value and produce spurious override entries that grow across upgrades.DiffArray::diffAssocRecursive($cast_data, $cast_base)is what lands in$moduleOverrideson every save. The legacy "write moduleOverrides verbatim" path is gone.updateExistingKeysInNestedArray()deprecated — no longer used internally, no drop-in replacement in core (different shape thanDiffArray::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)— passingNULLtriggersE_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— theBlockListBuilder::submitFormregression.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 whencastThroughSchema()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.