Skip to content

Drupal service IDs from *.services.yml - #533

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

AJenbo merged 12 commits into
PHPantom-dev:mainfrom
giuseppe-esposito-87:drupal-dr2

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 DR2: in a Drupal site, service IDs work the way they do in a Symfony project.

  • Completion, go to definition and references for the service IDs and container parameters that core, contributed and custom modules declare in *.services.yml, wherever a services file, $container->get() or \Drupal::service() names them.
  • Typed lookups. \Drupal::service('entity_type.manager') and $container->get(…) are typed by the class the service is built from; a lookup by ::class is typed as that class or interface.
  • Rename leaves core and contributed extensions alone, as it does vendor code, and refuses a namespace move that would leave them behind.
  • Other frameworks unchanged. A Drupal site in the same workspace as a Symfony, Laravel or WordPress project leaves their behaviour as it was; tests cover the mixed workspace.

On drupal/redirect and drupal/flag, typed services remove three false positives and expose a few correct errors that untyped lookups hid.

A refactor in the first commit

The first commit, "String-key kinds name their framework through one exhaustive match", changes no behaviour.

Today. Four call sites decide which framework a string key belongs to with an if-chain, each in its own order. A kind that no check catches silently falls through to Laravel:

if kind.is_twig() {
    is_twig
} else if kind.is_yii() {
    is_yii
} else if kind.is_symfony() {
    is_symfony
} else {
    is_laravel
}

Why Drupal needs a change. Drupal uses Symfony's service container keys, but none of Symfony's routes, translations or events. With the chains, that means a new check placed before is_symfony() in all four chains, and a wrong order fails silently.

After. Each kind names its framework once, in a match with no catch-all, and every call site becomes a flat match:

match kind.framework() {
    KeyFramework::Laravel => is_laravel,
    KeyFramework::ServiceContainer | KeyFramework::Symfony => is_symfony,
    KeyFramework::Twig => is_twig,
    KeyFramework::Yii => is_yii,
    KeyFramework::WordPress => false,
}

A new kind that is not placed now fails to compile instead of becoming Laravel. WordPress, Twig and Middleware keys keep their current behaviour; every caller was traced to confirm it.

What DR2 then changes. A later commit gives the container keys their own switch, which is on in both Symfony and Drupal projects, without touching the other arms:

match kind.framework() {
    KeyFramework::Laravel => is_laravel,
    KeyFramework::ServiceContainer => reads_container,
    KeyFramework::Symfony => is_symfony,
    KeyFramework::Twig => is_twig,
    KeyFramework::Yii => is_yii,
    KeyFramework::WordPress => false,
}

The Drupal entity types PR adds a KeyFramework::Drupal arm the same way.

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 #532.

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

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 98.98580% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/drupal/mod.rs 97.56% 2 Missing ⚠️
src/indexing/init.rs 96.42% 1 Missing ⚠️
src/reference_index.rs 87.50% 1 Missing ⚠️
src/symbol_map/extraction/symfony.rs 98.38% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

giuseppe-esposito-87 and others added 12 commits November 5, 2026 15:21
Service IDs and container parameters form their own service-container group, which every project building a Symfony container reads. No behaviour change.

Assisted-by: claude-code/claude-opus-5-5
A Drupal project is detected from any of its Composer projects, and its web roots are kept. Inside a web root, services.yml and every *.services.yml declare service IDs and container parameters, so container lookups are typed, completed, and navigated. Drupal turns on the service container only: Symfony's routes, translations, events, and the rest stay off.

Assisted-by: claude-code/claude-opus-5-5
A setParameter() call in Drupal is far more often a validator's or a query's than the container's, so PHP-side declarations stay Symfony's. The unknown service and parameter checks judge Symfony's app. names only, so they no longer run in a Drupal project, where they forced a parse of the whole workspace for nothing.

Assisted-by: claude-code/claude-opus-5-5
Composer installs core and contrib into folders the project's .gitignore leaves out. The Drupal walk, which already indexes their PHP, now also collects their services files for the workspace index. Files under a tests folder belong to test modules no site enables, and are read only while open.

Assisted-by: claude-code/claude-opus-5-5
\Drupal::service('id') and \Drupal::hasService('id') name a service ID like a container lookup does: the ID completes, goes to its declaration, and lists its references, and service() is typed by the declared class. Only the global Drupal class counts.

Assisted-by: claude-code/claude-opus-5-5
Composer installs core and contrib into the web root rather than vendor/, and overwrites them on the next update. A rename whose symbol or service ID is declared there is refused, the way one declared under vendor/ is.

Assisted-by: claude-code/claude-opus-5-5
The demo module declares a parameter, looks its own service up through \Drupal::service(), and a core one through a container in create(). The assertions check each name against Drupal's own loader, core's services included. DR2 leaves the backlog.

Assisted-by: claude-code/claude-opus-5-5
default.services.yml, core's scaffold sources, and the sites functional tests generate are never loaded as they stand, so they no longer declare services. Moving a class and renaming a namespace now leave core and contributed extensions alone too, as rename did. A call chained straight off \Drupal::service() is typed like an assigned one.

Assisted-by: claude-code/claude-opus-5-5
In a Drupal site, moving a namespace that core or a contributed extension declares is refused, as moving one of its classes is, instead of rewriting only the custom side; elsewhere a namespace a vendor package shares is still renamed on the project's side. A rename's reference edits skip core and contributed extensions. \Drupal::service(id: 'x') is read like the positional form, assigned or chained.

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

One workspace holds a Laravel application at its root and a Drupal site, a Symfony application, and a WordPress plugin installed beside it. Each framework keeps working in its own project, and a Symfony application with no Drupal site gets nothing from \Drupal::service().

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

\Drupal::service(FooInterface::class) is typed through core's conditional return type, also when the services file aliases the interface to a service built from another class. The playground looks the greeter up by its class name too.

Assisted-by: claude-code/claude-opus-5-5
Confirming a file's typed receiver calls now runs in that file's own
Composer project scope. A `\Drupal::service()` lookup in a Drupal site
reads a Symfony app's PHP configurator in the same workspace; resolved
under the Drupal site's scope, Symfony's configurator classes were
hidden, the configurator's `set()` calls went unconfirmed, and the empty
answer was cached for the file. This made the mixed-workspace test
flaky, depending on which thread confirmed the file first.

The NoteCounter "Try:" comment promised that hovering get() shows the
service class; hover shows ContainerInterface::get() instead, so drop
that part. Borrow the vendor prefixes in the class-move guard instead
of cloning them.

File B732 (#[\AllowDynamicProperties] is not honoured) and B733 (a
service a ServiceProvider re-classes keeps its YAML class), both
surfaced by typed service lookups on Drupal core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AJenbo
AJenbo merged commit 25a4232 into PHPantom-dev:main Nov 5, 2026
18 of 20 checks passed
@AJenbo

AJenbo commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Thanks

@giuseppe-esposito-87
giuseppe-esposito-87 deleted the drupal-dr2 branch November 5, 2026 15:00
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