Issue #3618091: Allow a config key to stay overridden when its value equals the base configuration

Fixes #3618091.

A domain override row stores only what differs from the base configuration (#3587744), and an empty diff deletes the row (#3593636). A key deliberately given the base's own value is therefore not stored at all, and the domain silently follows a later change to the base. For a per-domain theme that means an explicit choice is lost the moment somebody edits the site default.

Two mechanisms, with deliberately different rules.

setOverride($key, $value) — creates. The method has been inert since #3587744 made save() recompute the override unconditionally, so it is free to take this meaning. It sets the value and records the key in $pinnedOverrideKeys; save() adds those keys back to the diff, reading values from the cast $this->data so a pinned key carries the same value the diff would have written. This pass is not conditional on a row already existing, because turning an empty diff into a row is exactly what an explicit pin is for. set() no longer forwards to setOverride(), so the sparse default is untouched for Domain Configuration UI.

Declared pinned keys — preserve. A pin created by one writer was still dropped by the next set()-based save of the same object, which is the normal situation when Domain Configuration UI and a dedicated module both write a configuration object for a domain. save() now also restores keys declared in domain_config.settings:pinned_keys and by hook_domain_config_pinned_keys_alter(), resolved by the factory the same way isAllowedConfiguration() merges its sources. This pass IS guarded: it preserves an existing override and never creates one, so an unrelated save on a domain that has no override cannot resurrect one at base values.

The list is read from the base storage rather than through the config factory, because this class is a config factory override and loading configuration through the factory here would recurse; that is the same reason the base payload already comes from the base storage.

Also in here: DomainConfigFactoryOverrideInterface::getOverrideEditable() declared @return \Drupal\Core\Config\Config while the implementation has always returned a DomainConfigOverrideEditable. Narrowed to the class actually returned, which is what makes setOverride() reachable through the interface. Documentation only and covariant, so no caller breaks.

Verified on Drupal 11.4.4. Twelve kernel tests in DomainConfigOverrideEditableTest, the six pre-existing ones included, plus the Domain Configuration UI kernel suite. Each new test was checked to fail for its own reason: removing the declared pass fails only "declared pin survives the second writer", and removing its guard fails only the two tests that pin the no-create and no-resurrect rules.

Backport for 3.x: !432 (merged). Not applicable to 3.0.x, which has no sparse diff and already stores every key set through set().

Edited by Frank Mably

Merge request reports

Loading