DomainStorage currently depends on RequestStack for hostname inference, scheme detection, and path prefix matching. Entity storage handlers should be pure data layers with no request coupling.

Changes

  • New DomainResolver service owning all request-dependent and hostname-normalization logic: getRequestHostname(), getRequestScheme(), getRequestPath(), normalizeHostname(), loadByHttpHost(), isRegisteredHttpHost(), resolveDomain(), matchByPathPrefix(), matchPathPrefix(). Holds the RequestStack, domain.www_prefix, domain.allow_non_ascii, domain.path_prefix parameters.
  • New Drupal\domain\Utility\MachineName::fromString() helper replaces DomainStorage::createMachineName() static.
  • DomainStorage pure data layer: RequestStack, ignoreWwwPrefix, container properties become @deprecated; prepareHostname() / createHostname() / createMachineName() / getDefaultScheme() become deprecated wrappers delegating to domain.resolver or MachineName::fromString().
  • Storage method names unchanged (loadByHostname() / loadMultipleByHostname()) — docblocks clarified that they expect normalized hostnames.
  • Move matchPathPrefix() from Domain entity to DomainResolver, deprecating the entity method.
  • DomainNegotiationContext gains DOMAIN_MATCHED_NONE/EXACT/ALIAS constants as the canonical home (the constants conceptually belong on the negotiation outcome holder, not on the negotiator service). The constants on DomainNegotiatorInterface become deprecated aliases referencing the new location — class-constant reference is fully transparent BC.
  • DomainNegotiator thin orchestrator: negotiateActiveDomain() collapses to a single domainResolver->resolveDomain($context) call. Legacy entry points (setRequestDomain, setActiveDomain, negotiateActiveHostname, negotiateByPathPrefix, setHttpHost, getHttpHost, isRegisteredDomain) become deprecated wrappers with @trigger_error().
  • Switch read-only consumers to DomainNegotiationContext: DomainCacheContext, DomainAccessManager, DomainAccessNodeHooks, DomainAccessEntityHooks, DomainConfigUiFormHooks, DomainSourceFormHooks drop the full DomainNegotiatorInterface dependency in favor of the lighter outcome holder. Reduces service-graph dependencies and circular-wiring risk. DomainAliasHooks drops its DomainNegotiatorInterface import entirely.
  • Domain::preCreate() absorbs auto-fill logic (hostname / name / scheme / is_default) that used to live in DomainStorage::create().
  • Consistent variable naming: $http_host for raw HTTP hostnames, $hostname for normalized (www-stripped).
  • Deprecation cycle: deprecated in domain:3.1.0, removed in domain:4.0.0.

Bug fixes in passing

  • $_SERVER['https'] (lowercase, never set by anything) becomes $_SERVER['HTTPS'] in the request-scheme detection path.
  • is_default default for the first domain flips from (int)($default === FALSE) (always 0 because loadDefaultId() returns NULL, not FALSE) to (int) is_null($default) (correct: 1 when no default exists yet).

Test coverage

New DomainResolverTest kernel test covers each public method on DomainResolverInterface in isolation: getRequestHostname, getRequestScheme (http/https), getRequestPath (with and without an active request), normalizeHostname (www_prefix on/off), loadByHttpHost (known/unknown), isRegisteredHttpHost (known/unknown), resolveDomain (exact-match and default-fallback). 13 tests, 59 assertions. Closes the test-coverage gap for the resolver service. The integration paths (DomainHookTest, DomainPrefixTest, etc.) keep covering the full negotiator chain.

Updated callers

DomainNegotiator, DomainAliasHooks, DomainServerBlock, DomainPrefixPathProcessor, Domain entity, DomainForm, DomainRedirectResponse, Drush commands (domain + domain_alias), DomainAliasValidator, DomainAliasPatternConstraintValidator, six read-only consumers switched to DomainNegotiationContext, tests, docs.

Behavior changes

Strict superset for normal flows; deprecation warnings on legacy usage. Two notable changes:

  1. DomainNegotiator::setRequestDomain($hostname, $reset) now ignores both arguments. Existing callers passing a specific hostname silently negotiate against the live request. The deprecation message explicitly calls this out and points at the migration path.
  2. is_default default for the first domain is now correctly 1 instead of 0 (incidental bug fix).

Coordination

Rebased onto post-merge 3.x. Recent merges (#3588155, #3588168, #3588169, #3588175, #3588176) all touched the alias-side of the pipeline; this issue's domain-side resolver/negotiator refactor doesn't conflict with any of them.

Out of scope (separate issues)

  • Removing the legacy deprecated wrappers on DomainNegotiator / DomainStorage and the deprecated constant aliases on DomainNegotiatorInterface. To be done in domain:4.0.0.
  • Updating any external modules that still depend on DomainNegotiatorInterface for read-only access. They keep working through the deprecated path; they should migrate to DomainNegotiationContext at their convenience.

Supersedes #3583389.

Issue fork domain-3583423

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

Version: 3.0.0-rc1 » 3.x-dev
Status: Active » Needs review
mably’s picture

Issue summary: View changes
mably’s picture

Issue summary: View changes
mably’s picture

Summary of changes

This MR introduces a new DomainHelper service that centralizes all request-dependent logic previously scattered across DomainStorage, DomainNegotiator, and the Domain entity class. The goal is to make DomainStorage a pure data layer with no RequestStack dependency, which is how Drupal entity storage handlers are designed to work.

What changed

New DomainHelper service (domain.helper) owns:

  • Request hostname retrieval and normalization (www. stripping)
  • HTTP scheme detection
  • Domain resolution with path prefix disambiguation
  • Module hook invocation (hook_domain_request_alter)
  • Convenience methods: loadByHttpHost(), isRegisteredHttpHost(), resolveDomain()
  • Static createMachineName()

DomainStorage is now a pure data layer:

  • No more RequestStack or $ignoreWwwPrefix dependencies
  • loadByHostname() / loadMultipleByHostname() expect normalized hostnames (documented in phpdoc)
  • Deprecated methods (prepareHostname, createHostname, createMachineName, getDefaultScheme) delegate to DomainHelper via BC wrappers with @trigger_error()

DomainNegotiator is now a thin orchestrator:

  • setRequestDomain() reduced from ~40 lines to 4 lines of delegation to DomainHelper::resolveDomain()
  • New getContext() method on the interface returns DomainNegotiationContext
  • Deprecated methods (setHttpHost, getHttpHost, negotiateActiveHostname, negotiateByPathPrefix, isRegisteredDomain) delegate to DomainHelper

DomainAliasHooks no longer depends on DomainNegotiatorInterface:

  • Replaced with DomainNegotiationContext + DomainHelper
  • Eliminates the reentrancy risk: the old code depended on the full negotiator, which could trigger re-negotiation during hook_domain_request_alter. Now it reads from the lightweight context object and calls DomainHelper for path matching only — it never triggers negotiation.

Bug fix included: $_SERVER['https'] (lowercase) was silently broken in the old DomainStorage::getDefaultScheme(). Fixed to $_SERVER['HTTPS'].

Concrete benefits

  1. TestabilityDomainStorage can now be tested without a request stack. DomainHelper can be mocked independently for unit tests.
  2. No more reentrancy riskDomainAliasHooks no longer holds a reference to the negotiator, so it cannot accidentally trigger re-negotiation during alter hooks.
  3. Clear API contractsloadByHostname() expects normalized input, loadByHttpHost() accepts raw input. No more guessing whether a method normalizes internally.
  4. Single responsibility — each service has one job: DomainStorage loads/saves, DomainHelper handles request logic, DomainNegotiator orchestrates.
  5. Domain resolution in one place — the full resolution algorithm (load, match type, alter hook, fallback to default) lives in DomainHelper::resolveDomain() instead of being spread across negotiator and storage.

BC layer

All deprecated methods have @trigger_error() notices and delegate to the new service. Existing code calling deprecated methods will continue to work without changes. Default values for hostname, name, and scheme are now set in Domain::preCreate(), so $storage->create() with an empty array still works as before.

Deprecation cycle: deprecated in domain:3.1.0, removed in domain:4.0.0.

28 files changed across domain, domain_alias, and their tests/docs.

mably’s picture

Title: Introduce DomainHelper service, make DomainStorage a pure data layer » Introduce DomainResolver service, make DomainStorage a pure data layer
Issue summary: View changes

Renamed DomainHelper to DomainResolver — "Helper" was too vague for a class whose 9 out of 10 methods resolve HTTP requests to domain entities. DomainResolver accurately describes its responsibility and follows Symfony/Drupal naming conventions (ArgumentResolver, ControllerResolver, etc.).

Updated across 26 files: class name, service ID (domain.helperdomain.resolver), all variables and docblocks, deprecation messages, and documentation (EN/FR/ES).

mably’s picture

Issue summary: View changes
mably’s picture

Issue summary: View changes

Update issue body to reflect the latest scope on !358: new DomainResolverTest kernel test (closes test-coverage gap for the resolver service); DOMAIN_MATCHED_* constants moved to DomainNegotiationContext as canonical home, with deprecated aliases on DomainNegotiatorInterface for transparent BC; Domain::preCreate() auto-fill move; two bug fixes in passing ($_SERVER[https] casing, is_default default).

mably’s picture

Issue summary: View changes

Fix: replace stray > HTML entity with literal > in body.

  • mably committed c569eb22 on 3.x
    task: #3583423 Introduce DomainResolver service, make DomainStorage a...
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.