Problem / Motivation
domain_config_ui supports per-domain configuration overrides through ConfigFormBase forms (system.site, etc.). It does not currently surface those overrides on EntityForm-based config-entity admin flows (block, view modes, search pages, views, …):
- The parent module's
form_alteronly injects the "Enable domain configuration" toggle onConfigFormBaseandConfigTranslationFormBase;EntityForm-based edit pages never see it. - Drupal core's
AdminPathConfigEntityConverterloads config entities override-free on every admin route, so the edit form for a domain-overridden block renders the base values. ConfigEntityListBuilder::load()callsloadMultipleOverrideFree()on the entity storage handler, so admin list pages render base values too. The same call path bitesBlockListBuilder::submitForm(), where saving region or weight on a block whose label is overridden silently overwrote the existing override via the diff bridge inDomainConfigOverrideEditable::save().
Proposed resolution
A new experimental submodule domain_config_entity_ui in this project, layering five coordinated pieces on top of domain_config_ui:
DomainAwareConfigEntityStorageTraitoverridesdoLoadMultiple()to fold the active domain's override on top of the override-free read path. Regular loads (saves, runtime, drush, …) flow through unchanged: the trait short-circuits when$this->overrideFreeis FALSE.DomainAwareConfigEntityStorageInterfaceis an empty marker. The form_alter and ParamConverter both gate on$storage instanceof DomainAwareConfigEntityStorageInterface— capability discovery is runtime introspection rather than a hardcoded entity-type list.DomainAwareConfigEntityStorageis a thin shell that uses the trait, implements the interface, and extends core'sConfigEntityStorage.DomainAwareSwapRegistry::computeSwaps()auto-discovers every config entity type whose defaultstorage_classisConfigEntityStorage(block, view modes, search pages, views, …) and registers a swap to this class.hook_entity_type_alter()applies it.DomainConfigEntityUiFormHooks::formAlter()exposes the parent module's "Enable domain configuration" toggle on EntityForm-based config-entity edit pages, gated on the marker interface so types we do not cover never see the toggle.DomainOverrideConfigEntityConverterruns at higher priority than core'sAdminPathConfigEntityConverter. Same capability gate; loads override-merged for registered configs on the active domain, otherwise defers to core's behavior.
Two-stage opt-in
- Installing the submodule —
lifecycle: experimentalplus Drupal's install confirmation prompt is the gross gate. - Per-entity-type checkboxes on a SettingsForm at
/admin/config/domain/config-entity-ui. Default install: empty. The user explicitly checks each entity type they want covered. The settings subscriber clears entity-type definitions on save so the swap takes effect on the next request withoutdrush cr. The form opens with a yellow warning that onlyblockhas been validated end-to-end; the other auto-discovered types are best-effort and should be tested on a non-production environment first.
Extensibility — hook_domain_config_entity_ui_swaps_alter
Documented in domain_config_entity_ui.api.php. Contrib modules ship a sibling DomainAware*Storage subclass (extending the entity type's existing storage handler, using the trait, implementing the interface) and register it via the alter hook for entity types whose default storage_class is a custom subclass (image_style → ImageStyleStorage, user_role → RoleStorage, menu → MenuStorage, …). Modules can also REMOVE auto-discovered entries from $swaps to opt a vanilla-storage entity type out of coverage entirely.
Tests
Kernel (3 files, 11 tests / 49 assertions):
DomainAwareConfigEntityStorageTest— storage swap on install, override-merged read on registered configs, BlockListBuilder save flow regression, mid-request override re-read, non-curated entity type stays on its own handler, disabling un-checks the swap.DomainOverrideConfigEntityConverterTest— converter capability gate (registered → override-merged, unregistered → defer to parent).DomainAwareSwapRegistryAlterTest— alter-registered entries surface in the registry, coexist with auto-discovery, strict-equality guard rejects mismatched contrib registrations.
Functional (DomainConfigEntityUiToggleTest):
- Toggle present on Configure block; absent on Place block; absent on uncovered
user_role. - SettingsForm flips coverage on the next request (full UX path: empty → check block → toggle appears).
- Save round-trip: toggle → register → edit label → save → per-domain override holds the new value, base config untouched, form re-renders override-merged. Skipped on environments where the parent diff bridge is missing (
property_exists($baseData)probe).
Module info
- name: Domain Configuration Entity UI
- package: Domain
- lifecycle: experimental, lifecycle_link points at this issue
- core_version_requirement: ^10.3 || ^11 (uses
\Drupal\Core\Form\ConfigTargetfrom 10.3) - configure:
domain_config_entity_ui.settings_form(Configure link on the Modules admin page) - menu link: under Configuration → Domain → Domain config entity types
- permission:
administer domain config entity uifor the SettingsForm - dependencies:
domain:domain_config_ui(no hardblockdep — runtime is generic)
Related
- domain #3587744 — parent issue, write-side correctness in
domain_config; merged. - #3588057 / #3588108 — parallel "menu plugin manager cache leaks across domains" pair.
Issue fork domain_extras-3588091
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 #3
mably commentedLatest state on the MR (!32):
domain_config_ui_extras—lifecycle: experimental. Depends ondomain:domain_config_uianddrupal:block.DomainAwareConfigEntityStorage extends ConfigEntityStorage. OverridedoLoadMultiple()folds the active domain's override on top of the override-free read path for configurations registered on that domain. Stamps the entity with thedomaincache context. Static cache trio is no-op (setStaticCache/getFromStaticCache/resetCache) so a mid-request override save isn't served stale;ConfigFactory's own cache still holds the underlyingImmutableConfig.DomainAwareBlockListBuilder extends BlockListBuilderis a thincreateInstanceadapter that hands aDomainAwareConfigEntityStorageinstance to the parent constructor in place of the regular storage.load()andsubmitForm()inherit unchanged through the parallel storage. The block entity type'sstorage_classis left untouched, so saves, drush and any third-party API code keep seeing the regularConfigEntityStorage.domain_config_ui.settings.enable_config_entity_support(introduced in #3587744). Installing this submodule alone does not bypass the opt-in.DomainConfigUiSettingsSubscriberlistens toConfigEvents::SAVEand clears entity-type definitions when the flag flips, so toggling the gate at runtime takes effect on the next handler resolution without a manualdrush cr.BlockListBuilder::submitFormnow reads through the domain-aware storage, so saving region/weight on a block whose label is overridden no longer overwrites the existing override via the diff bridge.DomainAwareConfigEntityStorageTestcovers all four behaviour promises (co-gating round-trip, storage isolation, override-free read semantics, static-cache no-op). 4 tests / 26 assertions.strictConfigSchemais relaxed locally because the parent module'senable_config_entity_supportschema key only ships from #3587744 on; can come back out once that lands and a tagged domain release picks the schema up.createInstancefactory pattern (proper DI, no\Drupal::service()).Comment #4
mably commentedComment #5
mably commentedComment #7
mably commented