Problem/Motivation

Whatever advances a piece of work is not always the domain that work belongs to. An operator validates, from a back office domain, an order placed on the public site; cron releases a hold with no active domain at all; a queue worker mails a customer hours later. Rendering then inherits the context of the request doing the work, so the message carries the wrong links, the wrong site name and the wrong per-domain configuration.

Switching the domain negotiation context carries most of the load: per-domain configuration overrides read the active domain from it, and so does DomainPrefixPathProcessor::processOutbound(), so the domain path prefix and the target domain's language prefix both follow. What it does not carry is the host. UrlGenerator::generateFromRoute() reads the scheme, host and port from core's router.request_context, and the only thing in Domain that overrides them is DomainPathProcessor, which returns early unless a domain option is set on the individual URL. Measured on a site with both prefixed and separate-host domains, switching the negotiation context alone yields a URL with the right prefix, the right language and the wrong host.

Every module that sends something out of band ends up repeating the same save, set and restore dance, including Domain itself in DomainPathProcessor::processLanguageNegotiationOutbound(). With DomainNegotiator::setActiveDomain() and setRequestDomain() deprecated for removal in Domain 4.0, each copy is also a separate migration.

Proposed resolution

A new domain_render_context submodule with one service, domain_render_context.renderer:

  • inDomain($domain, callable $callback): runs the callback as if the request had come from the given domain, restoring the previous context on return and on an exception.
  • enter($domain): the lower-level counterpart, returning the closure that restores the previous context, for work that does not fit in one callback such as a queue worker. Safe to call more than once.

It switches exactly two things: the domain negotiation context (per-domain configuration, path prefix, the target domain's language negotiation configuration) and the scheme, host and port of the router request context. It does not switch the theme, the interface language, the session or the current user, and it does not push a request onto the request stack. An unknown machine name is logged and the current context is kept, so a deleted domain never turns a notification into a fatal error.

Outbound only, and documented as such: it must never be called while a page is being routed or rendered for the browser, since the negotiated domain also drives inbound path processing, routing and language negotiation for the page being served.

Compatibility: uses only API available from Domain 3.0 through 4.x, and calls nothing deprecated for removal in 4.0.

Remaining tasks

  • Submodule, service and interface.
  • Kernel coverage: configuration and request origin switch, path prefix follows the switch, restore on a normal return, on an exception, on nested calls, on an unknown domain, on a run that started with nothing negotiated, and an idempotent restore closure.
  • README and a documentation page.
  • Follow-up upstream: DomainNegotiationContext::setDomain() cannot take NULL, so a context that held no domain on entry (cron, the CLI) cannot be emptied again. The restore re-negotiates from the current request instead, which is what the next read would have produced anyway. A nullable signature in Domain would let the restore be exact.

User interface changes

None. Code only, no UI, no configuration.

API changes

New submodule and a new service. Nothing existing changes.

Command icon 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

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review

  • mably committed d4ebc1f2 on 3.x
    feat: #3613716 Add a Domain Render Context submodule to render out-of-...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.