Skip to content

Drupal route names, route parameters and upcasting - #535

Merged
AJenbo merged 18 commits into
PHPantom-dev:mainfrom
giuseppe-esposito-87:drupal-dr5
Nov 5, 2026
Merged

AJenbo merged 18 commits into
PHPantom-dev:mainfrom
giuseppe-esposito-87:drupal-dr5

Conversation

@giuseppe-esposito-87

@giuseppe-esposito-87 giuseppe-esposito-87 commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

🤖 This was written by an AI agent on behalf of @giuseppe-esposito-87 (claude-code/claude-opus-5-5).

Adds DR5: Drupal route names and their parameters work wherever Drupal reads them, and controller parameters get the entity Drupal passes them.

Url::fromRoute('acme.greeting', ['name' => 'World']); // route and 'name' complete and jump to acme.routing.yml
$this->redirect('acme.greting');                      // warning: Unknown route

// acme.note: path '/acme/note/{acme_note}'
public function view(EntityInterface $acme_note) {}  // $acme_note is Note
public function title($acme_note) {}                 // the title callback gets the same Note
  • Route names. Routes declared by a routing file, a #[Route] attribute or a route builder complete, jump to their declaration and list their references wherever code, a links file or a template names them: Url::fromRoute(), Link::createFromRoute(), $this->redirect(), $form_state->setRedirect(), route_name: in menu, task and action links, and Twig's path().
  • Unknown routes. A name nothing declares is reported. Names Drupal builds in code (entity.node.canonical, view.content.page_1) are accepted by their shape, so they never warn; completing them is left for DR20.
  • Route parameters. The keys passed by those calls, by route_parameters: and by path() jump to the {placeholder} of the route's path and complete from it.
  • Route defaults. The entity type and service that _entity_form, _entity_view and similar defaults name are navigable.
  • Upcasting. A controller parameter is typed as the entity Drupal passes it, and a type hint that keeps Drupal from upcasting is reported.

Upcasting follows Drupal's rules, not the parameter name alone

The roadmap asks to type an untyped $node from the route. Drupal itself does not do that: EntityResolverManager converts a parameter only when the route declares options.parameters.<name>.type: entity:<type>, uses an _entity_* default, or the controller hints the parameter with the entity class or an interface it implements. An untyped $node on a plain route receives the raw ID.

So this PR types a parameter as the entity only when the route converts it by those rules. Once it does, every method that receives it gets the entity, including an untyped title callback, as in the example above.

The warning is deliberately narrow: it fires only for a parameter named after an entity type, on a controller method PHPantom fully reads, with a hint unrelated to the entity class, on a route that declares nothing about the parameter. Anything less certain (converters, bundle classes, title callbacks, routes it cannot fully read) stays silent.

On drupal/redirect and drupal/flag, the only new report is a real bug: FlagFormBase redirects to flag_list, which is a form ID, not a route. Peak memory grows about 2% from reading core's routing and links files; time is unchanged.

In Zed, the parts that start in a YAML or Twig file need zed-extensions/php#155; other editors already send those files to PHPantom.

Builds on #534.

Checklist

If applicable:

  • I have updated CHANGELOG.md
  • I have updated the documentation (README.md, docs/, examples/)
  • I have updated the config schema (config-schema.json). Not applicable: this PR adds or changes no setting.
  • I have added/updated tests to cover my changes
  • If I used AI tools, I have reviewed what they produced and can explain
    the changes and why they are needed.
  • I have read through any AI-generated text, especially documentation,
    the changelog, and other user-facing content, and removed or corrected
    anything inaccurate, unclear, or unnecessary.

@codecov-commenter

codecov-commenter commented Nov 5, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 96.06433% with 93 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/symbol_map/extraction/drupal.rs 96.38% 30 Missing ⚠️
src/completion/symfony_string_keys.rs 61.90% 16 Missing ⚠️
src/drupal/routing.rs 97.02% 12 Missing ⚠️
src/resource_navigation.rs 79.06% 9 Missing ⚠️
src/backend/startup.rs 0.00% 7 Missing ⚠️
src/drupal/links.rs 96.90% 6 Missing ⚠️
src/completion/drupal_route_params.rs 96.66% 4 Missing ⚠️
src/drupal/mod.rs 96.74% 4 Missing ⚠️
src/drupal/upcasting.rs 98.89% 2 Missing ⚠️
src/completion/laravel_string_keys/mod.rs 50.00% 1 Missing ⚠️
... and 2 more

📢 Thoughts on this report? Let us know!

giuseppe-esposito-87 and others added 18 commits November 5, 2026 15:49
A route declared by a module's, core's, or a contributed extension's routing file completes, jumps to its declaration, and lists its references in Url::fromRoute(), new Url(), Link::createFromRoute(), a form state's setRedirect(), a controller's or form's redirect(), and the URL generator, access manager, and route provider methods that take a route name. Test modules' routing files count only while open.

Assisted-by: claude-code/claude-opus-5-5
The route_name and base_route of a menu, task, action, or contextual link, and the routes an action appears_on, navigate to the route, show up in its references, and complete from the declared routes. A parent link or task is not read as a route.

Assisted-by: claude-code/claude-opus-5-5
A #[Route] on a controller under an extension's Controller namespace declares its route the way Drupal's attribute route discovery names it: the class attribute's name prefixes the method routes, an invokable class with no method routes takes the class's own, and a method route without a name declares nothing. path() and url() in a Drupal template complete and navigate route names.

Assisted-by: claude-code/claude-opus-5-5
A class whose file uses Symfony's Route or RouteCollection, as route callbacks, route subscribers, and entity route providers do, adds the route that $collection->add() and $routes[...] = new Route() name. A name written out is declared there and navigable; one built from parts is kept as a pattern of the names it can take, for the unknown-route check to come.

Assisted-by: claude-code/claude-opus-5-5
In a routing file, the entity type of an _entity_form, _entity_view, or _entity_list and the service of a service:method _controller or _title_callback jump to their declarations and show up in their references.

Assisted-by: claude-code/claude-opus-5-5
A route name read in PHP that no routing file, route attribute, or route builder declares is reported. Names nothing can tell missing are left alone: the entity.* and view.* routes entity route providers and Views build, names matching the shape of a route a builder builds from parts, route names in test code, and every name in a project whose routing files were not read. A Class::method route alias is judged by whether the method exists.

Assisted-by: claude-code/claude-opus-5-5
The playground's controller redirects and links to its route, a menu link and the greeting template name it too, and the assertions check that every name they use is one the routing file declares. DR5 leaves the backlog; the route parameters it pointed at are filed as their own item.

Assisted-by: claude-code/claude-opus-5-5
Route builders in traits, routes held in a variable or returned in an array literal, and builder files that import the routing component as a group are read. A name a builder builds with no written-out text in front, such as "$name.$method", stands for the extension's own routes instead of any name with a dot in it, so the unknown-route check keeps judging every other route. A route on an abstract controller is declared, and a Class::method or Class route name is accepted only when attribute route discovery gives a controller that alias.

Assisted-by: claude-code/claude-opus-5-5
…lists

path('<fr in a Drupal template completes <front> and the other angle-bracket routes, and an appears_on list whose items sit at the key's indentation is read and completed.

Assisted-by: claude-code/claude-opus-5-5
A route builder's add() declares a route only when its second argument is a new Route or a variable, so an add() on any other collection in the same file declares no route and does not stand for the extension's routes.

Assisted-by: claude-code/claude-opus-5-5
The example code that documents hooks in an extension's *.api.php names invented routes, like test code, so its route names are not reported.

Assisted-by: claude-code/claude-opus-5-5
The mixed workspace checks that a Symfony template and Laravel's route() keep naming their own routes, that a Drupal template names Drupal's and still completes an extension's Twig filters, and the receiver unit test covers the route methods Drupal reads.

Assisted-by: claude-code/claude-opus-5-5
Each placeholder of a routing file's path or a #[Route] path declares a parameter of its route, and the keys of the parameters Url::fromRoute(), new Url(), Link::createFromRoute(), the redirect and URL generator calls, and a links file's route_parameters pass go to it, find its references, hover, and complete. A parameter no path declares is not reported, since Drupal sends it as the query string.

Assisted-by: claude-code/claude-opus-5-5
A parameter of a route's controller or title callback is the entity class when Drupal's entity converter loads it: a type the route's options declare, a parameter named after an entity type and hinted with a class its entity class fits, or the entity type an _entity_view or _entity_form names. An untyped parameter nothing upcasts keeps no type.

Assisted-by: claude-code/claude-opus-5-5
A controller or title callback parameter named after an entity type and hinted with a class its entity class is not receives the raw ID, which every route that reaches it passes, so it is reported. Untyped and builtin-typed parameters, and those a route declares in its options, are not.

Assisted-by: claude-code/claude-opus-5-5
The keys of the hash a Twig path() or url() passes after a Drupal route name go to the placeholders of that route's path and count among their references.

Assisted-by: claude-code/claude-opus-5-5
…changelog

The playground's routing file, controllers, links file, and template carry Try: sites for route parameters and an upcast controller parameter, with runtime checks, and DR5 leaves the backlog.

Assisted-by: claude-code/claude-opus-5-5
… docblocks

Read the routes an entity route provider renames with $routes->add()
even when its file never imports Symfony's routing classes, so core's
node.add and node.add_page are declared, and count a route returned by
a call as added. Keep a declared parameter type that already names a
subclass of the upcast entity class, such as a bundle class in a
docblock, instead of widening it to the entity class. Sort the template
spans of a Drupal project that is not a Twig project. Note in DR20 the
route names the shape rules leave unjudged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AJenbo
AJenbo merged commit d8a5f2e into PHPantom-dev:main Nov 5, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants