Skip to content

Laravel support#294

Draft
janedbal wants to merge 14 commits intomasterfrom
laravel-support
Draft

Laravel support#294
janedbal wants to merge 14 commits intomasterfrom
laravel-support

Conversation

@janedbal
Copy link
Member

@janedbal janedbal commented Mar 1, 2026

No description provided.

janedbal added 12 commits March 1, 2026 16:11
…ler heuristic

Replace the broad reflection heuristic that marked all public controller
methods as used with precise AST-based detection of:

- Route::get/post/put/patch/delete/any/match — extract [Class, 'method'] from action arg
- Route::resource — mark 7 CRUD methods (index/create/store/show/edit/update/destroy)
- Route::apiResource — mark 5 CRUD methods (index/store/show/update/destroy)
- Event::listen — mark handle() + __construct() on listener class
- Event::subscribe — mark subscribe() + __construct() on subscriber class
- Schedule::job — mark handle() + __construct() on job class

All other reflection-based checks (Eloquent, commands, jobs, middleware,
notifications, form requests, factories, seeders, policies, mailables,
broadcast events, JSON resources, validation rules) remain unchanged.
…route __construct, invokable controllers

- Move routeNotificationFor* detection from Notification to Notifiable/RoutesNotifications trait users
- Replace deprecated HandlesAuthorization trait check with *Policy naming convention for policy detection
- Mark __construct as used for controllers referenced in Route:: registrations
- Support invokable controllers (Route::get('/path', Controller::class) marking __invoke + __construct)
- Detect migration classes (up/down methods)
- Detect observers via Model::observe() calls and #[ObservedBy] attribute
- Mark middleware __construct as used alongside handle/terminate
- Detect $this->authorize('ability', $model) in controllers using AuthorizesRequests trait
- Resolve model → policy class using Laravel naming convention
- Support Gate::define() for explicit ability registration
- Support Gate::policy() to mark all public methods on registered policy classes
- Convert kebab-case ability names to camelCase (e.g. 'force-download' → 'forceDownload')
Other providers (Symfony, Doctrine, Twig, Nette) install real packages
as dev dependencies. Laravel was the only one using ~210 lines of inline
stubs. Install laravel/framework and use real Illuminate classes instead.
Laravel 11+ requires PHP ^8.2, which breaks CI on PHP 8.1.
Laravel 10 requires PHP ^8.1 and all tests pass with it.
Eloquent (illuminate/database) can be used standalone without
laravel/framework. The new EloquentUsageProvider auto-enables when
illuminate/database is installed, covering Model methods, Factory,
Seeder, Migration, and Observer detection.
Methods declared in vendor interfaces/parent classes are already handled
by VendorUsageProvider, so the Laravel provider doesn't need to mark them:

- broadcastOn: declared in ShouldBroadcast interface
- toArray, with, additional: declared in JsonResource parent class
- validate: declared in ValidationRule interface
- passes, message: declared in Rule interface

The remaining checks cover methods called via method_exists() magic
(broadcastWith, broadcastAs, broadcastWhen, paginationInformation)
which VendorUsageProvider cannot detect.
The isPolicyMethod() fallback previously matched any class ending in
"Policy" (e.g. RetentionPolicy, CachePolicy), which could suppress
legitimate dead code warnings on non-authorization classes that happen
to have methods named view, create, update, delete, etc.

Laravel's own guessPolicyName() always generates candidates within a
\Policies\ namespace segment. Policies registered outside that
convention (via Gate::policy() or #[UsePolicy]) are already handled
by the precise detection layer. So we can safely require \Policies\
in the class name for the heuristic fallback.
- Route::match with invokable controller (class string at arg index 2)
- Job methods uniqueVia and displayName
- FormRequest methods failedValidation and failedAuthorization
- #[ObservedBy] with array syntax
The previous implementation only checked {rootNamespace}\Policies\{Model}Policy,
which missed models in sub-namespaces (e.g. App\Models\Admin\User would only
try App\Policies\UserPolicy, missing App\Policies\Admin\UserPolicy).

Now mirrors Laravel's Gate::guessPolicyName() candidate list:
- \Models\ → \Models\Policies\ replacement
- \Models\ → \Policies\ replacement
- Segment-based candidates from most to least specific

All existing candidates where the class exists are emitted as usages,
rather than just the first match.
janedbal added 2 commits March 1, 2026 20:11
Laravel supports 'Controller@method' strings in route definitions.
Parse these alongside array callables and class-string invokables
in Route::get/post/put/patch/delete/any and Route::match calls.
Laravel automatically discovers event listeners by scanning the
Listeners directory for classes with public handle*/__invoke methods
that have a class-typed first parameter (the event). This happens
without any explicit Event::listen() registration.

Previously, only explicitly registered listeners (via Event::listen
AST calls) had their handle() marked as used. Auto-discovered
listeners would be falsely reported as dead code.

The detection mirrors Laravel's DiscoverEvents logic:
- Public methods matching handle* or __invoke
- First parameter must have a non-builtin class type-hint
- Union types (e.g. EventA|EventB) are supported
- Constructor is also marked when the class qualifies

Placed last in the shouldMarkAsUsed chain so it only triggers for
classes not already identified as Commands, Jobs, Middleware, etc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant