Skip to content

Drupal project support #48

Closed
Closed

Description

@syntlyx

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

  1. syntlyx commented on Mar 28, 2026

    @syntlyx
    ContributorAuthor

    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
    • Route parameter type hints from *.routing.yml
    • Plugin definitions from annotations (@Block, @Field, @EntityType, etc.)
    • Hook autocomplete based on hook_*.php stub files
    • Theme hook suggestions from hook_theme() implementations

    Just leaving this here as a reference — not requesting any of this now.

  2. AJenbo commented on Mar 28, 2026

    @AJenbo
    Contributor

    Looks like we will need a yaml parser for this

  3. ekes commented on Apr 1, 2026

    @ekes

    Some immediate things jump to my mind looking at the above:

    {web-root}/modules/contrib/ 	Contributed modules
    {web-root}/modules/custom/ 	Project-specific modules
    

    The contrib custom directories are only common practice. You can place the modules anywhere under /modules. Directly in that directory, or commonly in sub-directories like company_name.

    {web-root}/profiles/
    

    For as long as they continue to exist, have the same sub-directory structure as the modules and themes directories mentioned elsewhere.

    Test directories should be excluded from indexing — they add noise and can contain duplicate class definitions:

    By not indexing them I have incorrect errors all over the place when editing tests. Starting from class MyTest extends BrowserTestBase with 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.

  4. DarkGhostHunter commented on Jul 25, 2026

    @DarkGhostHunter
    Contributor

    I believe this should be more of a Drupal LSP than PHPantom-specific feature (much like why the Laravel LSP exist).

  5. AJenbo commented on Jul 25, 2026

    @AJenbo
    Contributor

    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.

  6. DarkGhostHunter commented on Jul 25, 2026

    @DarkGhostHunter
    Contributor

    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.

    This one, right?

  7. AJenbo commented on Jul 25, 2026

    @AJenbo
    Contributor

    that and the vs code one

  8. ekes commented on Jul 25, 2026

    @ekes

    As I'm reminded by being ping'd on this. Adding updated information on the feature list from syntlyx above:

    \Drupal::service('my_service') → resolving the concrete class from *.services.yml

    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.

    Route parameter type hints from *.routing.yml
    Plugin definitions from annotations (@block, @field, @EntityType, etc.)

    Also with recent versions: moved to PHP Attributes.

    Hook autocomplete based on hook_*.php stub files
    Theme hook suggestions from hook_theme() implementations

    And hooks are now moving OOP in classes (with Attributes).

  9. syntlyx commented on Aug 1, 2026

    @syntlyx
    ContributorAuthor

    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

    • The Drupal scan currently runs only in the single-project init path — monorepo and no-composer workspaces skip it (src/indexing/init.rs).
    • Drupal-scanned symbols are merged into the FQN index without class_origins, so they miss completion-origin classification.
    • Test exclusion needs two distinct mechanisms:
      • Framework artifact indexes (services, routes, etc. — see below) should skip test fixtures by default (/tests/, /Tests/, /Fixture(s)/ path segments). Drupal core ships thousands of fixture modules whose dummy *.services.yml / *.routing.yml would otherwise pollute completion.
      • Indexing-wide excludes should stay user-configurable rather than hardcoded — that is backlog item X9 (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 — index *.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('…').
    • Entity type chain — #[ContentEntityType] / #[ConfigEntityType] attributes and legacy annotations feed entityTypeManager()->getStorage('node') → NodeStorage → load() / create() / loadByProperties() → Node, plus \Drupal::entityQuery('…').
    • Routes — index *.routing.yml and *.links.*.yml, synthesize dynamic routes (entity.<type>.canonical, view.<id>.<display>, <front>); completion/goto for Url::fromRoute(), setRedirect(), etc.
    • Fields — field.storage.*.yml plus static analysis of baseFieldDefinitions(); $entity->get('field_x') completion, FieldItemList typing via list_class, ->entity via target_type.
    • Permissions, libraries, module machine names — small string-key features, cheap once the string-key infrastructure is shared (see seams below).

    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 for t() / formatPlural(), *.info.yml validation.

    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:

    • Detection state: is_laravel: bool on the resolved-class cache → a DetectedFrameworks struct (detect_drupal_web_root already exists but its result is discarded after scanning; there is no is_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.
    • The VirtualMemberProvider registry keyed on detected frameworks instead of the hardcoded if is_laravel.
    • A single 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

    • Drupal semantics need a YAML parser dependency. Parsing would be file-name-gated (*.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.
    • A [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.

  10. added this to the Backlog milestone on Aug 15, 2026
  11. AJenbo commented on Nov 2, 2026

    @AJenbo
    Contributor
  12. giuseppe-esposito-87 commented on Nov 2, 2026

    @giuseppe-esposito-87
    Contributor

    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:

    • DR1. The examples/drupal/ demo site with runtime assertions, and an analyze baseline on the redirect and flag contrib modules.
    • DR2. Service IDs from core, contrib and custom *.services.yml. They type, complete and navigate in \Drupal::service(), $container->get() and @ arguments in the YAML.
    • DR3. Entity type IDs from attributes and annotations, with getStorage() typed through to load(), loadMultiple() and create().
    • DR5. Route names from routing files, route attributes and route builders, used in PHP, links files and Twig, with an unknown-route diagnostic.
    • DR6. Hooks: procedural and #[Hook] implementations link to their *.api.php documentation 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 latest main before 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:

    • can the PRs wait for my review, or would you rather I open them now, marked as not yet reviewed by me?
    • would you prefer to take over this work, starting from my branches, or are you fine with me continue to work on it?

    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.

  13. AJenbo commented on Nov 2, 2026

    @AJenbo
    Contributor

    @giuseppe-esposito-87 thanks for letting me know, I had just started poking DR1 :)

    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.

    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.

    can the PRs wait for my review, or would you rather I open them now, marked as not yet reviewed by me?

    Post them when you feel they are ready, one way or the other.

    would you prefer to take over this work, starting from my branches, or are you fine with me continue to work on it?

    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 😅

  14. giuseppe-esposito-87 commented on Nov 2, 2026

    @giuseppe-esposito-87
    Contributor

    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.

    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.

    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.

    I'll update you if this week-end I'll do some more coding session or not.

  15. giuseppe-esposito-87 commented on Nov 5, 2026

    @giuseppe-esposito-87
    Contributor

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

    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.

    • Rebased on the current main.
    • Synced with the roadmap. Each PR fully closes its item, including what the roadmap added: DR1's "what already works" check, entity queries in DR3, and route parameters with upcasting in DR5. DR5 follows Drupal's own upcasting rules rather than the parameter name alone; the PR explains why.
    • DR3 is not on your priority list, but DR5's upcasting and DR6's entity hooks need it, so it comes first.
    • Testing. I tested the result in Zed. VS Code is not tested yet.
    • AI review box left unticked, as you suggested.

    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.

  16. AJenbo commented on Nov 5, 2026

    @AJenbo
    Contributor

    a couple of days later than I said

    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.

  17. AJenbo commented on Nov 5, 2026

    @AJenbo
    Contributor

    All 5 PRs are now merged, I only did some light editing no major issues found when going though them. Thanks for the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions