Background
Domain 3.x replaced the in-form domain selector (the old SwitchForm from domain_config_ui 2.x) with a negotiation-based model: per-domain configuration overrides live in config collections and always apply to the active (negotiated) domain — the hostname currently being browsed. See the related Domain support request #3592797.
This is deterministic and clean, but it drops a workflow some sites relied on: administering every domain's configuration from a single (main) domain. Today an admin must browse to each domain to edit its overrides.
Proposal
Add an opt-in domain_extras submodule (working name domain_config_switcher) that reintroduces a domain (and language) selector for domain-specific configuration, without changing the deterministic default Domain 3.x ships. It pairs naturally with domain_sso for a fully centralized admin experience.
Feasibility
This can be built entirely against Domain's public service interfaces — no dependency on @internal classes, and no changes required in domain or domain_config_ui:
DomainConfigFactoryOverrideInterface: getOverride($domain_id, $name) and getOverrideEditable($domain_id, $name) already read and write any domain's collection.DomainConfigUIManagerInterface: addConfigurationsToDomain($domain_id, ...) and friends manage the per-domain overridable registry.DomainNegotiationContext: setDomain() can set the active domain for a request.
All three are public, service-aliased and autowireable.
Two possible approaches
- Option A (session re-negotiation): a request subscriber scoped to the config-UI admin routes reads a session-selected domain and calls DomainNegotiationContext::setDomain(). The whole config form then reflects the selected domain — display, save, access and cache suffix all follow the single negotiation lever. Closest to the 2.x "page reloads and values change" behaviour; must be admin-route-scoped to avoid switching theme/blocks and to keep the domain cache context contained.
- Option B (form-only): leave negotiation untouched; populate the config form's display values from the selected domain via getOverride() and route the save through getOverrideEditable(). More surgical and cache-safe (admin config forms carry form tokens and are not page-cached), at the cost of per-form value interception.
Scope and open questions
- Option A vs Option B (leaning B for cache-safety, A for fidelity to 2.x).
- Language dimension: the selector should carry a language alongside the domain, matching the 2.x switcher.
- Access: checks should key on the selected domain rather than the active one.
- UI: a selector block/form storing the choice in session, plus a permission to use it.
Feedback welcome on the preferred approach before implementation.
Issue fork domain_extras-3592820
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
mably commentedPushed a first implementation to the issue fork branch
3592820-new-submodule-domain(the new submodule lives atdomain_config_switcher).Approach (Option A): a per-user selection (private tempstore) is applied as the active domain on administrative routes via DomainNegotiationContext::setDomain(). The browser stays on the current host; only the negotiated domain used by the configuration override layer changes, so the Domain Config UI forms read and write the selected domain's overrides. Selecting "- Current domain -" clears it.
Config translation needs no special handling: it is reached through the standard Translate tab, and domain_config_language_ui already resolves getDomainOverride(activeDomain, langcode), so switching the domain scopes translations to the selected domain automatically. The switcher stays domain-only.
Public API only: built against DomainConfigFactoryOverrideInterface, DomainConfigUIManagerInterface and DomainNegotiationContext. No dependency on @internal classes and no changes to domain or domain_config_ui.
Test coverage:
PHPCS, PHPStan and cspell are clean.
Feedback welcome on Option A versus a form-only variant before this is proposed for a release. I can open a merge request against 3.x if the approach looks right.
Comment #4
mably commentedComment #6
mably commentedComment #8
hanoiiHi! Thanks for all the amazing and speedy work you are doing. I just tried this module. Some feedback:
- ON the project I am testing for now I only overrode two forms: System information and Appearance for a single theme.
#1. The system information works really good. The only thing I wonder is the fact that if that by choosing a domain, as this is stored in a temp store now every form will respect that. I know this is like the remember setting of the 2.x behavior but I have the feeling this is dangerous. I would either keep that as an opt in or go back to something more url-like with the query string. So if you navigate away you lose the domain configuration context. For a configuration of every config along the way, maybe you can go the switch to domain route.
#2. The appearance form doesn't work. I thought because this one was overridden only for some domains, even if I override the current domain it's also not picked up. Not sure if it has to do with not being #config_target-driven.
Comment #9
mably commentedThanks a lot for testing, and for the detailed feedback!
#1 (sticky selection): agreed, the cross-form persistence was a footgun. Fixed in #3592980 - the selected domain now lives in a URL query argument instead of the tempstore, so it is scoped to the form you switched on and dropped as soon as you navigate away. That is the url-like behaviour you suggested. Switching still discards unsaved edits, and Save still targets the selected domain.
#2 (Appearance form): good catch, and your hunch was right - it is not #config_target-driven. The theme settings form exposes its configuration through getEditableConfigNames() and reads it in buildForm(), before the switcher's hook_form_alter() runs, so the switcher cannot retarget the values it displays; that is why the selector is intentionally not shown on it. This is now documented as a known limitation (see #3592980). Supporting such forms would require the global, pre-build approach that broke routing and was deliberately abandoned.
Thanks again!