Repository navigation
Description
A reference Drupal project for testing is available at: https://github.com/syntlyx/drupal-example
How Drupal is structured
A standard Drupal Composer project looks like this:
project/
├── composer.json
├── composer.lock
├── vendor/
└── web/ # web root (varies, see below)
├── core/ # Drupal core
├── modules/
│ ├── contrib/ # contributed modules
│ └── custom/ # project-specific modules
├── themes/
│ ├── contrib/
│ └── custom/
├── profiles/ # install profiles
└── sites/
Detecting a Drupal project
PHPantom can reliably detect Drupal from composer.json:
"require": {
"drupal/core-recommended": "^10.0"
}Relevant package names to detect: drupal/core, drupal/core-recommended, drupal/core-dev.
Resolving the web root
The web root is not always web/. It is defined in composer.json under:
"extra": {
"drupal-scaffold": {
"locations": {
"web-root": "docroot/"
}
}
}Common values: web/, docroot/, public/, html/. If the key is absent, fall back to checking which of these directories contains core/lib/Drupal.php.
Paths to index
Once the web root is resolved, the following directories should be scanned:
| Path | Contains |
|---|---|
{web-root}/core/ |
Drupal core classes, interfaces, hooks |
{web-root}/modules/contrib/ |
Contributed modules |
{web-root}/modules/custom/ |
Project-specific modules |
{web-root}/themes/contrib/ |
Contributed themes |
{web-root}/themes/custom/ |
Project themes |
{web-root}/profiles/ |
Install profiles |
{web-root}/sites/ |
Site-specific code |
PHP file extensions
Drupal uses several non-.php extensions for valid PHP files:
| Extension | Purpose |
|---|---|
.module |
Hook implementations, main module file |
.install |
hook_schema(), update hooks, hook_install() |
.theme |
Theme hook implementations |
.profile |
Install profile hooks |
.inc |
Shared include files |
.engine |
Theme engine files (rare) |
All of these start with <?php and are plain PHP.
Paths to exclude
Test directories should be excluded from indexing — they add noise and can contain duplicate class definitions:
| Path | Reason |
|---|---|
{web-root}/core/tests/ |
Drupal core test classes |
{web-root}/core/modules/*/tests/ |
Per-module core tests |
{web-root}/modules/contrib/*/tests/ |
Contrib module tests |
{web-root}/modules/custom/*/tests/ |
Custom module tests (optional, project preference) |
{web-root}/themes/contrib/*/tests/ |
Contrib theme tests |
vendor/*/tests/ |
Vendor package tests |
vendor/*/Tests/ |
Some packages use capital T |
Note that both tests/ and Tests/ variants exist in the wild — Drupal core and most contrib use lowercase, but some Composer packages (Symfony, Doctrine) use uppercase.
The .gitignore problem
Drupal's default .gitignore excludes the web root scaffold files:
/web/core
/web/modules/contrib
/web/themes/contrib
/vendor
These directories are excluded from version control because they are managed by Composer — but they must be indexed by PHPantom. web/core in particular contains all of Drupal's base interfaces, hooks, and services that every module depends on.
Using .gitignore to determine what to skip will silently drop the most important parts of the codebase.
Activity
syntlyx commentedon Mar 28, 2026
Getting indexing right is the prerequisite for deeper Drupal-specific intelligence that could be tackled separately in the future:
\Drupal::service('my_service')→ resolving the concrete class from*.services.yml*.routing.yml@Block,@Field,@EntityType, etc.)hook_*.phpstub fileshook_theme()implementationsJust leaving this here as a reference — not requesting any of this now.
AJenbo commentedon Mar 28, 2026
Looks like we will need a yaml parser for this
ekes commentedon Apr 1, 2026
Some immediate things jump to my mind looking at the above:
The
contribcustomdirectories are only common practice. You can place the modules anywhere under/modules. Directly in that directory, or commonly in sub-directories likecompany_name.For as long as they continue to exist, have the same sub-directory structure as the modules and themes directories mentioned elsewhere.
By not indexing them I have incorrect errors all over the place when editing tests. Starting from
class MyTest extends BrowserTestBasewith error `Class 'Drupal\Tests\BrowserTestBase' not found and onward.I'm not sure I understand, what is intended by: "can contain duplicate class definitions" either.
DarkGhostHunter commentedon Jul 25, 2026
I believe this should be more of a Drupal LSP than PHPantom-specific feature (much like why the Laravel LSP exist).
AJenbo commentedon Jul 25, 2026
PHPantom has modules for frameworks, it's got a pretty extensive one for Laravel, there is no reason not to add one for Drupal.
I feel that making individual LSPs for each framework is often serving poor results as the frameworks are often tightly coupled with the language and you end up with very limited framework support by making one that knows about the framework but not about the language.
The Laravel LSP is largely based on string matching and hardcoded lists rather then actually parsing the framework or application, meaning that it doesn't get fully correct results.
DarkGhostHunter commentedon Jul 25, 2026
This one, right?
AJenbo commentedon Jul 25, 2026
that and the vs code one
ekes commentedon Jul 25, 2026
As I'm reminded by being ping'd on this. Adding updated information on the feature list from syntlyx above:
Most - but not all - core, and following contrib, services are in the most recent version of Drupal loaded by
\Drupal::service(ClassInterface::class)which of those at least would make mapping them to the interface easier.Also with recent versions: moved to PHP Attributes.
And hooks are now moving OOP in classes (with Attributes).
syntlyx commentedon Aug 1, 2026
Detection, web-root resolution, the extra PHP extensions, and directory indexing from this issue landed in #52, so the indexing layer is mostly covered. Supplementing this issue with the remaining indexing follow-ups and a proposal for the semantic layer, based on a feature-parity analysis with the Drupal Extended PhpStorm plugin.
Indexing follow-ups
src/indexing/init.rs).class_origins, so they miss completion-origin classification./tests/,/Tests/,/Fixture(s)/path segments). Drupal core ships thousands of fixture modules whose dummy*.services.yml/*.routing.ymlwould otherwise pollute completion.docs/todo/indexing.md, exclude globs + file associations in.phpantom.toml). Excluding tests from indexing entirely would break completion inside test files, so it must remain opt-in.Semantic layer proposal
Ordered by value per effort; each item is its own PR (or split into separate issues if preferred).
Phase 1
*.services.yml(id → class, including autowired definitions where the id is the FQCN); completion, go-to-definition, and return-type resolution for\Drupal::service('…')/$container->get('…').#[ContentEntityType]/#[ConfigEntityType]attributes and legacy annotations feedentityTypeManager()->getStorage('node')→NodeStorage→load()/create()/loadByProperties()→Node, plus\Drupal::entityQuery('…').*.routing.ymland*.links.*.yml, synthesize dynamic routes (entity.<type>.canonical,view.<id>.<display>,<front>); completion/goto forUrl::fromRoute(),setRedirect(), etc.field.storage.*.ymlplus static analysis ofbaseFieldDefinitions();$entity->get('field_x')completion,FieldItemListtyping vialist_class,->entityviatarget_type.Phase 2 — config objects and key paths (
\Drupal::config('system.site')->get('page.front')),#[Hook('…')]name completion from*.api.php, plugin id ↔ class resolution from config YAML, render arrays (#theme/#type).Phase 3 — form callback references (
'#submit' => ['::handler']),getSubscribedEvents()method references, translation placeholder diagnostics fort()/formatPlural(),*.info.ymlvalidation.Explicitly out of scope: Drupal coding standards — already covered by the existing
[phpcs]integration with--standard=Drupal. Twig — separate track, analogous to Blade.Framework seams
Rather than an up-front abstraction refactor, each PR above generalizes exactly the seam it needs:
is_laravel: boolon the resolved-class cache → aDetectedFrameworksstruct (detect_drupal_web_rootalready exists but its result is discarded after scanning; there is nois_drupal()).SymbolKind::LaravelStringKey→ a framework-generic string-key kind. Most Drupal features are string-keyed (services, routes, config names, permissions, fields, libraries), so Laravel's existing completion/hover/GTD/diagnostics pipeline for string keys transfers almost 1:1.VirtualMemberProviderregistry keyed on detected frameworks instead of the hardcodedif is_laravel.build_framework_indexes()lifecycle hook shared by server /analyse/fix(currently three duplicated call sites).This lines up with the motivation already recorded in
docs/todo/external-stubs.md— making the next framework (Symfony, WordPress) cheaper than the first.Open questions
*.services.yml,*.routing.yml,*.permissions.yml,*.info.yml,*.libraries.yml,field.storage.*, config exports) so the cost stays bounded, but the crate choice deserves a maintainer call.[drupal]config section following the[laravel]pattern (e.g. toggling fixture-index exclusion, config-sync directory location).The reference project linked in the issue description works for integration tests. Happy to start with the indexing follow-ups and X9, then Phase 1 in order.
feat: exclude globs and extra PHP extensions for indexing
fix: index Drupal test directories so test base classes resolve
AJenbo commentedon Nov 2, 2026
Drupal support is now tracked in https://github.com/PHPantom-dev/phpantom_lsp/blob/main/docs/todo/drupal.md
giuseppe-esposito-87 commentedon Nov 2, 2026
Thanks for turning this into a roadmap. I need Drupal support in PHPantom for my work, so I worked on it with an AI agent on my fork yesterday and today.
That was before the roadmap update and before this issue was closed.
Nothing had moved here since August, so I assumed nobody was working on it.
These items are implemented:
examples/drupal/demo site with runtime assertions, and ananalyzebaseline on theredirectandflagcontrib modules.*.services.yml. They type, complete and navigate in\Drupal::service(),$container->get()and@arguments in the YAML.getStorage()typed through toload(),loadMultiple()andcreate().#[Hook]implementations link to their*.api.phpdocumentation and to the calls that fire them, with completion and parameter types from the documentation.Much of this is ported from PHPlumb by @lussoluca, with his agreement.
The branches were also written before today's changes on
main. They need a rebase and some rework on the latestmainbefore they can become PRs: today's Symfony and Twig changes touch the code DR2 and DR5 build on.They also need to be brought in line with the roadmap you posted today, which adds requirements to items I had already implemented: DR1's "what already works" check, entity queries in DR3, and path parameter upcasting in DR5.
I haven't opened PRs yet. The PR template asks that anything AI tools produced be reviewed by the author, who can then explain the changes, and I haven't reviewed this code yet.
Now that there is a roadmap, @AJenbo two questions:
I may have time this weekend to review what's done so far.
I don't know yet when I'll get to the rest of the roadmap.
AJenbo commentedon Nov 2, 2026
@giuseppe-esposito-87 thanks for letting me know, I had just started poking DR1 :)
Or you can just not check that box so that I know you can't. I do expect that you will test the end result once things are merged in that case.
Post them when you feel they are ready, one way or the other.
No. As someone who works with Drupal you are more qualified to work on it then I. I would prefer that DR1, DR2, DR5, DR6, DR7, DR8, DR9 gets finished first as that's what would get us to parity with other tools based on the info I could gather.
@lussoluca do you feel PHPantom is slow 😅
giuseppe-esposito-87 commentedon Nov 2, 2026
Ok, I think I'll manage to post tomorrow the PRs the agent have so far developed, with rebase and possible synced with your roadmap - at worst, I'll report what is missing in each PR, in relation of your roadmap.
I've already tested the result of those PRs with Zed editor, but not VSCode.
I'll update you if this week-end I'll do some more coding session or not.
giuseppe-esposito-87 commentedon Nov 5, 2026
The PRs are up, a couple of days later than I said: DR1, DR2, DR3, DR5 and DR6. They are stacked in that order, each building on the previous one.
main.The PRs also add a few backlog items for what I left out on purpose (DR16 to DR20), and DR3 notes a possible refactor of how same-named methods on different classes are dispatched.
I haven't started DR7, DR8 or DR9 yet. I'll say here before I eventually start one.
AJenbo commentedon Nov 5, 2026
Happens a lot to me as well, no worries, I found some things to keep me occupied. I'm excited to land this so will get started right away.
AJenbo commentedon Nov 5, 2026
All 5 PRs are now merged, I only did some light editing no major issues found when going though them. Thanks for the contribution.