The Problem It Solves
Three situations produce the same need:
- You have just been handed a codebase you did not write and have to estimate work on it.
- You are about to refactor and want to know what depends on the thing you are moving.
- You suspect a third of the project is unreachable but cannot prove it.
Laravel gives you route:list, which is genuinely useful and answers about ten percent of that. Everything else means reading files. X-Ray reads them for you and prints the structure.
Laravel X-Ray is a free, MIT-licensed package that statically analyses controllers, models, routes, views, services, repositories, middleware and form requests, then reports architecture and dead code in five output formats.
Installing It
composer require jaydeep/laravel-xray
php artisan vendor:publish --tag=xray-configIf package auto-discovery is disabled in your project, register the provider by hand:
// config/app.php
'providers' => [
Jaydeep\Xray\LaravelXrayServiceProvider::class,
],| Requirement | Supported |
|---|---|
| PHP | 7.4 – 8.4 |
| Laravel | 8 – 12 |
| License | MIT |
xray:scan — The Health Check
Start here. One command, a terminal summary, and an HTML dashboard.
php artisan xray:scan
php artisan xray:scan --save # also write report files
php artisan xray:scan --json # machine-readable to stdoutThe scan reports component counts, route analysis with named routes and expanded resource routes, model relationships, middleware signatures, form request rule fields, dead code totals, and a per-controller complexity metric.
That last one is the most useful number in the report. Complexity per controller is where the maintenance cost actually lives — the four controllers at the top of that list are almost always the four everyone is afraid to touch.
--path= lets you point the scan at a project other than the current one, which is handy when you keep a tooling install and want to scan several repositories without adding the dependency to each.
php artisan xray:scan --path=/var/www/other-projectxray:architecture — Dependency Trees
This one builds the controller dependency tree and identifies the layers in the project — controllers, services, repositories, models.
php artisan xray:architecture
php artisan xray:architecture --mermaid # print a Mermaid diagram
php artisan xray:architecture --mermaid --save # write architecture.mmdThe Mermaid output is the part I use most, because it pastes straight into a GitHub README, a pull request description, a Notion page or the project docs and renders as a real diagram:
flowchart TD
OrderController --> OrderService
OrderController --> InvoiceService
OrderService --> OrderRepository
OrderService --> PaymentGateway
InvoiceService --> PdfRenderer
OrderRepository --> OrderTwo things fall out of that diagram immediately: a controller wired directly to six services is doing too much, and a service reaching straight past its repository into a model means the layering is not actually being enforced. Both are obvious in a picture and invisible in a file listing.
xray:deadcode — What Is Unused
php artisan xray:deadcode
php artisan xray:deadcode --json
php artisan xray:deadcode --saveIt reports controllers with no route pointing at them, models nothing references, Blade views nothing renders, and service classes nothing instantiates.
Read the output, do not act on it blindly. Static analysis cannot see a controller resolved from a config string, a view name built by concatenation, or a class instantiated through the container by a variable. Treat every entry as “probably unused, go and check” — then delete it in a small commit you can revert.
The safe deletion loop, in order: grep the project for the class name, check it is not referenced in config or a database-driven menu, delete it on a branch, run the test suite, deploy behind a normal review. Doing that for twenty files is still an order of magnitude faster than finding them by hand.
xray:report — Files for CI
php artisan xray:report --format=all
php artisan xray:report --format=markdown
php artisan xray:report --format=mermaid| File | Contents |
|---|---|
xray-report.html | Self-contained Bootstrap 5 dashboard |
scan-report.json | Full scan data, for scripting |
scan-report.md | Markdown, for a docs folder or a PR |
architecture.json | Dependency trees and detected layers |
architecture.mmd | Mermaid flowchart source |
deadcode.json | Dead code results |
Everything lands in storage/app/project-xray by default. A scheduled weekly run that commits the Markdown report gives you architecture documentation that cannot drift out of date, because nobody maintains it by hand:
// routes/console.php
Schedule::command('xray:report --format=all')->weeklyOn(1, '02:00');Configuring the Paths
The defaults assume a conventional Laravel layout. If your project puts services somewhere else — or uses a domain-driven structure — point the config at the right directories:
// config/xray.php
return [
'paths' => [
'controllers' => 'app/Http/Controllers',
'models' => 'app/Models',
'services' => 'app/Services',
'repositories' => 'app/Repositories',
'views' => 'resources/views',
'routes' => 'routes/',
'middleware' => 'app/Http/Middleware',
'form_requests' => 'app/Http/Requests',
],
'output_path' => 'storage/app/project-xray',
'ignore' => ['Controller.php'],
];ignore excludes files by basename. The default drops the abstract base Controller.php, which would otherwise show up as dead code in every project.
How I Actually Use It
- Day one on a new codebase.
xray:scan --save, then open the HTML report and read the complexity table. That is a faster orientation than an hour of clicking through files. - Before quoting a piece of work. The architecture diagram tells me whether “add a field to orders” touches two files or twenty.
- Before a refactor. The dependency tree shows what breaks if I move a service.
- During a cleanup sprint. The dead code list, verified entry by entry, is usually worth several thousand lines.
- In project documentation. The Mermaid diagram in the README, regenerated by a scheduled job.
Caveats
- Static analysis has a ceiling. Anything resolved dynamically — a class name from config, a view name built by concatenation, a container binding chosen at runtime — will not be traced.
- The layer detection is convention-based. A project that puts everything in
app/with no structure will get a report that accurately reflects that, which is itself a finding. - Complexity is a heuristic. It flags where to look, not what is wrong. A large controller that is entirely CRUD is fine; a small one with five nested conditionals may not be.
- Dead code needs verification. Worth repeating: the list is a starting point for investigation, never an instruction to delete.
Links
- Packagist: jaydeep/laravel-xray
- GitHub: JaydeepGadhiya/laravel-xray
- More of my work: jaydeepgadhiya.netlify.app