Files
codespaces/.agents/designs/php-support.md
2026-06-22 08:12:55 +03:00

127 lines
5.5 KiB
Markdown

PHP Language Support for Belief Map
=====================================
Context and motivation
----------------------
The belief map previously supported Python and TypeScript/TSX. PHP projects
(Laravel, Symfony, legacy codebases) had no graph representation, meaning
agents querying the belief map would get zero results for `.php` files --
even in monorepos containing both TS and PHP services.
This design adds PHP as a first-class language alongside Python and TS,
covering the same entity/edge model: classes, interfaces, traits, enums,
imports, heritage, trait usage, and constructor dependency injection.
Implementation approach
-----------------------
**tree-sitter-php** is the primary parser, mirroring the TypeScript approach.
A regex fallback (`parse_php_regex`) ensures PHP files remain visible even
when `tree-sitter-php` is not installed, at the cost of reduced precision.
**PSR-4 autoload resolution** reads `composer.json` (`autoload.psr-4` and
`autoload-dev.psr-4`) to map FQCNs in `use` statements to file paths. This
mirrors the TypeScript `tsconfig.json` path-alias resolver. When no
`composer.json` is found, a walk-up heuristic converts namespace separators
to path separators (like the Python resolver).
**Constructor DI extraction** mirrors the NestJS heuristic from the TS
parser: typed `__construct` parameters with PascalCase type-hints are
treated as service dependencies, producing `calls-api` edges. This works
for Symfony autowiring and Laravel service container injection.
**Trait usage** (`use TraitX;` inside a class body) is a PHP-specific
relationship with no direct equivalent in Python or TS. Traits are
registered as abstract providers (like interfaces), and classes that use
them get `calls-api` edges to the trait's defining module.
**LSP enrichment** uses `phpactor language-server` (JSON-RPC over stdio),
the open-source PHP language server. It integrates with the existing
`LspClient` infrastructure without changes to the transport layer.
Supported entities
------------------
| PHP construct | Entity kind | sexp tag | Notes |
|---------------|-------------|----------|-------|
| `class Foo` | class | `cls` | abstract modifier, heritage |
| `interface Foo` | interface | `ifc` | auto-abstract |
| `trait Foo` | trait | `trt` | auto-abstract; usable via `use` |
| `enum Foo` (PHP 8.1) | enum | `enm` | backed enums (`: string/int`) |
| `function foo()` | function | `fn` | top-level functions only |
| `#[Attribute]` | decorator | `:deco` | PHP 8 attributes on classes/fns |
| `class { use TraitX; }` | trait usage | edge | `calls-api` to trait module |
| `__construct(Foo $f)` | DI dependency | edge | `calls-api` to service module |
Edge types for PHP
------------------
Same model as other languages:
- `imports` -- resolved via PSR-4 from `use` statements
- `calls-api` -- implements/extends + trait usage + constructor DI
- `data-flow` -- to modules with validation (Symfony Validator, Laravel)
- `refs` -- entity-level cross-file references
- `calls` (LSP only) -- precise function-to-function call edges
Skipped / out of scope (v1)
---------------------------
- **Blade templates** (`.blade.php`) -- skipped like `.d.ts`
- **Anonymous classes** (`new class { ... }`) -- no name, no ID
- **Trait conflict resolution** (`insteadof` / `as`) -- edges on all used traits
- **Dynamic imports** (`include`, `require`) -- not tracked
- **Runtime service discovery** -- no container introspection
- **`use function` / `use const`** -- tracked as imports but not as entity refs
File discovery
--------------
- `.php` extension (excluding `.blade.php`)
- `vendor/` and `var/` directories are skipped (composer deps, Symfony cache)
- `bootstrap/` and `storage/` are NOT skipped (may contain real source)
- `composer.json` defines PSR-4 autoload rules and LSP project boundaries
Fallback behavior
-----------------
When `tree-sitter-php` is not installed:
- `parse_php_regex` handles classes, interfaces, traits, enums (with heritage)
- No PHP 8 attribute extraction
- No constructor DI extraction
- Method extraction is best-effort (regex within a window)
- Trait usage inside class bodies IS detected (regex)
- The graph is still usable; precision improves when `tree-sitter-php` is added
When `phpactor` is not installed:
- `--lsp` mode skips PHP projects with a warning
- AST-based edges (imports, calls-api, refs) are unaffected
- Only `calls` (LSP call-hierarchy) edges are missing
Acceptance criteria
-------------------
1. Given a Laravel/Symfony project with `composer.json`, when running
`build_belief_map.py`, then `(node ... php ...)` facts are emitted for
each `.php` file with correct PSR-4-resolved `imports` edges.
2. Given a class implementing an interface, when parsing, then a
`calls-api` edge connects them.
3. Given a class using a trait (`use TraitX;`), when parsing, then a
`calls-api` edge connects the class module to the trait module.
4. Given a class with constructor type-hints, when parsing, then
`calls-api` edges connect the class to each dependency module.
5. Given PHP 8 attributes (`#[AsService]`), when parsing with
tree-sitter-php, then the attribute name appears in `:deco`.
6. Given `composer.json` with `autoload.psr-4`, when resolving imports,
then `use App\Service\Foo` resolves to `src/Service/Foo.php`.
7. Given `tree-sitter-php` is not installed, when parsing, then
`parse_php_regex` produces visible entities (reduced precision).
8. `belief_search.py analyze <php-class-id>` returns boundary/deps/rdeps
without modifications to the query engine.