Proposal: opt-in Laravel layer on top of the PHP extractor (question: do you want it upstream?) · Issue #4633 · Graphify-Labs/graphify · GitHub
Skip to content

Proposal: opt-in Laravel layer on top of the PHP extractor (question: do you want it upstream?) #4633

Open

Description

@if12is

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):

area relations
routes handled_by, uses_middleware
views renders, uses_component, Blade includes, Livewire view links
inertia / js renders (to page files), calls_route, references_route
data uses_model, maps_to_table, foreign_key_to, casts_to, validates_table, validates_with, Filament links
runtime invokes_command, dispatches_event, listens_to, sends_mail, facade_for, container bindings
authz / keys checks_ability, policy_for, reads_config, uses_translation

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

  1. 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?
  2. 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?
  3. Are new metadata.kind values and relation names fine, or should they map onto the existing
    vocabulary?
  4. Proposed split, each step useful and testable on its own, in this order:
    1. routes (route nodes, handled_by, uses_middleware, the gate and the shared PHP reader);
    2. views (Blade, components, Livewire);
    3. inertia (Inertia pages, props contract, route() -> route node);
    4. data (Eloquent, validation tables, migrations, Filament);
    5. runtime (commands, scheduler, dispatch, events, container);
    6. 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.
  5. 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.

Related generic fixes already opened as separate PRs: #4626, #4627, #4628, #4629, #4630, #4631, #4632. Code: https://github.com/CompertMe/graphify/tree/laravel

Activity

  1. github-actions commented on Oct 21, 2026

    @github-actions

    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.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions