Follow-up to #3615915: Provide entity routes through EntityRouteProviderInterface, where idebr asked for the routes to carry a primary entity.[entity_type].* name.

This is a backwards-compatibility break, and it fails silently. Renaming the route changes the name that local tasks, local actions, menu links and hook_help() compare against, and nothing in core resolves a route alias for those comparisons. A tab declared with base_route: domain.admin simply stops rendering: no exception, no warning, no log entry. Six contributed projects declare exactly that. The API changes section below lists every reference found, what happens to each, and why no combination that Composer can currently install is affected.

Problem/Motivation

The domain listing page at /admin/config/domain is served by domain.admin, a route named after the module rather than after the entity type. The domain entity type declares a collection link template, so core derives the name entity.domain.collection from the link relation, and #3613844: The collection link template of the domain entity type points to a route that does not exist declared that name as an alias of domain.admin. The convention-conforming name therefore resolves, but it is not the name the router matches, and the matched name is what every consumer of a route name compares against.

domain_alias.admin is deliberately out of scope. It serves /admin/config/domain/alias/{domain}, a listing scoped to one parent domain, and the domain_alias entity type declares no collection link template. Core declares 61 collection link templates and not one of them is parameterized, because EntityBase::urlRouteParameters() supplies no route parameters for the collection relation. Naming a parameterized listing entity.domain_alias.collection would make toUrl('collection') fail on the missing {domain} the day the matching link template is added. field_ui, which has the same shape of problem, calls its per-bundle field listing entity.[entity_type].field_ui_fields rather than entity.field_config.collection.

Proposed resolution

Rename the listing route to entity.domain.collection in domain.routing.yml and declare domain.admin as an alias of it, reversing the direction set in #3613844: The collection link template of the domain entity type points to a route that does not exist. The route stays in the routing file: DefaultHtmlRouteProvider::getCollectionRoute() emits at most two permissions and this route requires three.

Update every reference in this project, which is where the work actually is: route_name and base_route in domain.links.task.yml, route_name in domain.links.menu.yml, appears_on in domain.links.action.yml, configure in domain.info.yml, the hook_help() case in DomainHooks, the redirect in DomainController::ajaxOperation(), the redirect in DomainForm::save(), and the assertions in DomainUrlTest, DomainFormsTest and DomainEntityRouteProviderTest.

The menu link plugin id domain.admin in domain.links.menu.yml must not be renamed along with the route: six other projects declare parent: domain.admin, which is a menu link plugin id, not a route name.

Remaining tasks

Do not mark domain.admin as a deprecated alias yet, and do not build a compatibility layer for local tasks, local actions and menu links. Two reasons.

First, core deferred this exact flip. node.routing.yml and block_content.routing.yml keep the module-named route serving the page and declare the entity.* name as a plain, non-deprecated alias, with a @todo to remove those aliases once #3506653: Add an API for comparing the (current) route name that takes into account deprecated routes is done. That issue adds an API for comparing the current route name that takes deprecated routes into account, and it is the precondition here, because nothing in core resolves an alias for the consumers that key on a matched route name:

  • LocalTaskManager::getLocalTasksForRoute() compares a tab's base_route against the matched route name as a plain string.
  • LocalActionManager::getActionsForRoute() compares appears_on the same way.
  • MenuActiveTrail::getActiveLink() loads menu links by the matched route name, so the active trail follows it.
  • hook_help() is keyed by the matched route name.
  • RouteProviderInterface::getRouteAliases() exists since Drupal 11.2, but nothing in core calls it.

The mechanism itself is in place and needs no help from us: YamlRouteDiscovery reads the deprecated: key on an alias, and RouteProvider::getRouteByName() fires the @trigger_error() when it resolves one. The only deprecated: route alias anywhere in core is in the router_test module.

Second, on this branch a compatibility layer would protect nobody it can reach. Seven base_route: domain.admin declarations exist across six projects: domain_path, domain_entity, domain_menu_access both in its own repository and in the copy bundled with domain_entity, domain_theme_switch, domain_site_settings and domain_group. None of the six allows drupal/domain 4.x in its composer.json, so Composer will not install any of them next to this branch; each one has to cut a release widening its constraint, and that release is where base_route gets fixed. None of the six declares appears_on: domain.admin, none declares a menu link or a local task with route_name: domain.admin, and none implements hook_help() for it, so two of the three alter hooks in the original plan had nothing to rewrite. Two of them call Url::fromRoute('domain.admin') from a form, which keeps working through the alias.

What is needed instead: a change record for the 4.0 release describing the base_route change, and a follow-up, blocked on #3506653: Add an API for comparing the (current) route name that takes into account deprecated routes, to deprecate the alias.

User interface changes

None. The paths, the titles, the tabs and the action links are unchanged.

API changes

The domain listing route is renamed to entity.domain.collection, and domain.admin becomes an alias of it. A route name is a public API surface, so this is a BC break for anything that names the old one.

What keeps working. Url::fromRoute('domain.admin') and RouteProviderInterface::getRouteByName('domain.admin') both resolve through the alias. The alias is not marked deprecated:, so no deprecation notice is emitted and no downstream test suite turns red. Paths, titles, tabs, action links and permissions are unchanged for a site running only this project.

What breaks, silently. Everything that compares the route name that the router MATCHED, because an alias is never matched against a path and no core API resolves one for these comparisons:

  • base_route: domain.admin in a .links.task.yml: the tab detaches and stops rendering.
  • appears_on: domain.admin in a .links.action.yml: the action link stops rendering.
  • A menu link declaring route_name: domain.admin: it loses its active trail.
  • A hook_help() implementation keyed on domain.admin: its help text stops appearing.

Every reference found in the contributed projects that integrate with Domain. Affected, one base_route: domain.admin declaration each: domain_path, domain_entity, domain_menu_access (both in its own project and in the copy bundled with domain_entity), domain_theme_switch, domain_site_settings, domain_group. That is seven declarations across six projects, and each one loses a tab until it names the new route.

Not affected, with the reason for each: parent: domain.admin in a .links.menu.yml is a menu link PLUGIN id and not a route name, so the declarations in domain_path, domain_entity, domain_menu_access, domain_theme_switch, domain_site_settings, domain_group, domain_shorturl and the domain_config_entity_ui, domain_early_negotiation and domain_alias_extras submodules of domain_extras all keep working, which is why the plugin id domain.admin in this project's own .links.menu.yml is deliberately NOT renamed. The Url::fromRoute('domain.admin') calls in domain_theme_switch and domain_group resolve through the alias. No project checked declares appears_on: domain.admin, or a menu link or local task with route_name: domain.admin, or a hook_help() implementation for it. domain_redirect has no reference at all. Views tabs are safe by construction: ViewsLocalTask derives base_route by matching the path rather than from a hardcoded name.

Why no installable combination breaks today. None of the six affected projects allows drupal/domain 4.x in its composer.json: the constraints are ^3.0.0-beta8, ^2.0@beta || ^3, ^1.0 || ^2.0, ^3.0, ^1.0 || ^2.0 and ~1.0, and there is no 4.x release. Composer will not install any of them next to this branch, so every one of them has to publish a constraint-widening release before the two can meet, and that release is where its base_route gets fixed. The genuinely exposed cases are a project installed outside Composer and site-specific custom code, which is what the change record and the release note are for.

Release notes snippet

The domain listing route is now named entity.domain.collection. domain.admin remains available as a route alias, so Url::fromRoute('domain.admin') still works. This is a backwards-compatibility break for anything that compares the route name the router matched: a local task declaring base_route: domain.admin, a local action declaring appears_on: domain.admin, a menu link declaring route_name: domain.admin, or a hook_help() implementation keyed on it. All of them have to name entity.domain.collection, and all of them fail silently until they do. A menu link declaring parent: domain.admin needs no change, because that is a menu link plugin id and not a route name.

AI-Generated: Yes (Claude Code was used to help draft this issue summary. I reviewed it before posting; there is no code on this issue yet.)

Issue fork domain-3616784

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

Title: Use entity.domain.collection and entity.domain_alias.collection as the primary listing route names » Use entity.domain.collection as the primary name of the domain listing route
Issue summary: View changes

I checked the plan against core 11.4.4 and against the current downstream code before writing any of it, and the result changed the scope enough that I have reworded the summary and the title. Three findings.

1. Core deferred this exact flip. The keys work as advertised: YamlRouteDiscovery reads alias: and deprecated:, and RouteProvider::getRouteByName() fires the @trigger_error() itself when it resolves a deprecated alias, so no manual trigger is needed for the route-name half. But no core module ships a deprecated route alias; the only one is in the router_test test module. Real core modules go the other way: node.routing.yml and block_content.routing.yml keep node.add_page, node.add, block_content.add_page and block_content.type_add serving their pages and declare the entity.* names as plain aliases, with a @todo to remove those aliases when #3506653: Add an API for comparing the (current) route name that takes into account deprecated routes is done. That issue, "Add an API for comparing the (current) route name that takes into account deprecated routes", is at Needs work, and it is the precondition: LocalTaskManager, LocalActionManager, MenuActiveTrail and hook_help() all compare the matched name as a plain string, and getRouteAliases() has no caller in core. Deprecating domain.admin now would ask contrib to absorb what core has postponed for itself.

2. entity.domain_alias.collection is the wrong name, so I dropped it from the scope. domain_alias.admin serves /admin/config/domain/alias/{domain}, a listing scoped to one parent domain, and domain_alias declares no collection link template. Core declares 61 collection link templates and none of them is parameterized, because EntityBase::urlRouteParameters() deliberately adds no parameters for the collection relation. field_ui, which has the same shape of problem, calls its per-bundle field listing entity.[entity_type].field_ui_fields rather than entity.field_config.collection. Naming ours collection would set up a failure in toUrl('collection') for whoever adds the link template later.

3. The compatibility layer would protect nobody on this branch. Rechecked today: seven base_route: domain.admin declarations across six projects (domain_path, domain_entity, domain_menu_access both in its own repository and in the copy bundled with domain_entity, domain_theme_switch, domain_site_settings, domain_group). Not one of the six allows drupal/domain 4.x in its composer.json: the constraints are ^3.0.0-beta8, ^2.0@beta || ^3, ^1.0 || ^2.0, ^3.0, ^1.0 || ^2.0 and ~1.0. Composer will not install any of them next to this branch, so each one has to cut a release widening its constraint, and that release is where base_route gets fixed. Beyond that, none of the six declares appears_on: domain.admin, none declares a menu link or a local task with route_name: domain.admin, and none implements hook_help() for it, so hook_menu_local_actions_alter() and hook_menu_links_discovered_alter() would have had nothing to rewrite. What the deprecation notice would reliably do is turn those six projects' own test suites red through the Symfony deprecation bridge.

So the reworded plan is: rename the domain listing route on this unreleased major, keep domain.admin as a plain alias so Url::fromRoute() keeps working, document the base_route change in a change record and in the 4.0 release notes, leave domain_alias.admin alone, and open a follow-up blocked on #3506653: Add an API for comparing the (current) route name that takes into account deprecated routes for the deprecation. If you would rather keep the current direction until core lands its comparison API, then this issue can simply be postponed on #3506653: Add an API for comparing the (current) route name that takes into account deprecated routes instead.

AI-Generated: Yes (Claude Code was used to help draft this comment and the reworded issue summary, including the checks against core 11.4.4 and against the six downstream projects that it reports. I reviewed the findings before posting; there is no code on this issue yet.)

mably’s picture

Status: Active » Needs work

mably’s picture

Issue summary: View changes