You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Proposal: opt-in Laravel layer on top of the PHP extractor (question: do you want it upstream?) #4633
Draft only. Not filed. Intended as a GitHub issue or discussion on Graphify-Labs/graphify.
Summary
We run graphify on Laravel applications and ended up building a Laravel-aware layer on a fork
(CompertMe/graphify, branch laravel). Before sending anything we want to know whether maintainers
want this upstream at all, and if so how you would like it split. The generic (non-Laravel) PHP and
JS bug fixes it uncovered are prepared as separate small PRs; this issue is only about the
framework-specific part.
Problem
On a Laravel codebase the generic PHP extractor sees imports, classes and calls, but much of the
structure of a Laravel app is not expressed as calls:
a route is Route::get('/x', [XController::class, 'show']): there is no route node, and the
controller method is never reached from anywhere;
Blade views, <x-component> tags, Livewire components, Inertia pages and route('name') calls
in Vue are linked by string names;
Eloquent relations, casts, Model::query() statics, validation rules (exists:table,col),
migrations, jobs/events/listeners, the scheduler, the service container, policies and gates are
wired by convention or by config rather than by direct references.
On real apps this gives controllers with no inbound edges, views and pages as isolated nodes, and
god-node rankings driven by noise.
What the layer is
graphify/extractors/laravel/ (34 modules), registered through the existing graphify.resolver_registry, plus a small hook in extract.extract() that passes the scan root.
Activation gate: it runs only for a Laravel project, defined as artisan at the scan root or laravel/framework in composer.json (require or require-dev). Any other PHP corpus,
including one that merely calls Route::get, comes out byte-identical to what the generic
extractor produces. Non-PHP corpora are not touched.
Registered resolvers, in order: laravel_routes (route nodes, then the view/Blade/Livewire pass), laravel_web (Inertia pages, props contract, named routes), laravel_data (Eloquent, validation,
migrations, Filament), laravel_runtime (commands, schedule, dispatch, events, container), laravel_authz_keys (gates/policies, config and translation keys).
Node kinds added (metadata.kind): laravel_route, laravel_command, laravel_schedule, laravel_ability, laravel_config_key, laravel_translation_key, filament_panel, plus
Blade/Livewire/Inertia metadata on existing file nodes. Edge relations added (EXTRACTED only where
the source names the target; INFERRED where a convention or config table supplies it):
Design constraints kept: deterministic (no model calls, no network, sorted traversal); nothing
guessed when a name is dynamic, ambiguous or unresolved (the edge is dropped, not stubbed); every
heuristic boundary and unsupported case is listed in ARCHITECTURE.md.
Measured results
Benchmark: 6 real Laravel apps. Ground truth is parsed from the source by small regex parsers that
are independent of graphify (bench/bench.py); the graph is read from graphify-out/graph.json.
Cold builds, same machine. baseline-v8 is upstream v8 at our fork point; phase3-merged is the
fork with the layer and the generic fixes. A cell is correct edges / ground-truth sites, summed
over the apps that have sites for the metric.
metric
apps
baseline v8
with layer
route -> controller method
6
0 / 1807
1807 / 1807
Inertia page rendered from PHP
4
0 / 490
490 / 490
route('name') call -> route node
4
0 / 1681
1674 / 1681
Blade <x-...> component use
4
0 / 4765
4762 / 4765
PHP view() references
5
0 / 412
411 / 412
Eloquent relation -> related model
5
23 / 1181
1181 / 1181
Foo::class / class-constant references
6
169 / 3190
3132 / 3190
enum case references
4
220 / 2100
1997 / 2100
job dispatch -> job class
5
0 / 157
155 / 157
typed-receiver calls resolved
6
3353 / 4606
3938 / 4606
typed-receiver calls bound to the wrong method (lower is better)
6
557
5
global helper calls bound to a same-named symbol (lower is better)
4
198
0
Graph size over the six apps: nodes 90,006 -> 102,007, edges 220,516 -> 280,356, sourceless stub
nodes 2,139 -> 1,516 (two apps got slightly more: dent +7, rapidphish +9). Summed build time went
from 450 s to 319 s (dominated by one app, cyberportal, 330 s -> 220 s; we did not investigate why). The full table
(about 40 metrics x 6 apps; 148 cells improved, 2 regressed, both the stub-node counts above) is in bench/results/ (baseline-v8.json, phase3-merged.json; compare.py prints the diff). The six
corpora are private applications. We can share the harness and results and would write synthetic
public fixtures for upstream tests.
Some rows are produced by the generic PRs and not by the layer: Foo::class, enum case references,
typed-receiver rows and the helper-call row. The layer proper is the routes, Inertia, route(),
Blade, view(), Eloquent and job rows plus the data, runtime and authz metrics that have no
baseline because v8 cannot express them.
Size
Measured on our fork as upstream/v8...laravel:
graphify/extractors/laravel/: 34 modules, about 10.5k added lines (an earlier internal
estimate was about 9.5k);
other changes under graphify/ (resolver registration, hook, cache schema): about 0.7k lines;
tests: about 7.3k added lines, plus fixture apps under tests/fixtures/laravel_*;
ARCHITECTURE.md: one section per layer.
That is too large to review as one PR.
Questions for maintainers
Do you want framework-specific layers in the core repository, or would you rather have this as a
separate package that plugs in through the resolver registry?
If in core: is the activation gate (explicit project detection, byte-identical output otherwise)
an acceptable contract? Do you want a .graphifyrc switch as well?
Are new metadata.kind values and relation names fine, or should they map onto the existing
vocabulary?
Proposed split, each step useful and testable on its own, in this order:
routes (route nodes, handled_by, uses_middleware, the gate and the shared PHP reader);
authz (gates, policies, config and translation keys).
We would send step 1 first and wait for feedback on style and scope before sending the rest.
Tests: we use small synthetic fixture apps per layer, no real application code. Is that the shape
you want under tests/fixtures/?
Related work already prepared
Generic fixes found along the way, each its own PR with no Laravel-specific code: PHP class-name
resolution (Foo::class, use aliases, same namespace), typed-receiver member calls, enum-case and
class-constant references, plain function calls no longer binding to enum cases or class methods,
static-call scope resolution, and a JS barrel re-export fix.
Activity
github-actions commentedon Oct 21, 2026
Thanks for opening this issue, @if12is. A maintainer will take a look soon.
If you would like to discuss it in real time, come say hi on our Discord server. For longer-form questions and ideas there is also GitHub Discussions.
To help us triage, please make sure the report includes what you expected, what actually happened, and the steps (and a small sample) to reproduce it.