symplify/symfony-route-usage
Symplify Symfony Route Usage helps identify unused routes in Symfony applications by scanning code and templates for route references. Use it to audit routing configs, remove dead endpoints, and keep route definitions clean and maintainable.
Set expectations first: symplify/symfony-route-usage is a Symfony bundle, not a Laravel package. It lives under deprecated-packages, has no dependents, and is unmaintained. You cannot composer require it into a Laravel app and get working behavior — the service provider, route collector, and console command all bind to Symfony's RouterInterface and EventDispatcher. Treat it as a reference implementation of the "dead route detection" pattern rather than a dependency.
Where to look first (in this order):
README.md — confirms the actual console command name and the storage strategy (whether it logs hits to the DB, a file, or decorates the router). This determines whether the concept is portable to Laravel at all.src/ service registration — find the compiler pass / extension. The interesting bit is how it hooks into route matching; that's the part you'd re-implement with Laravel middleware.bin/console list (in a throwaway Symfony sandbox) — the bundle registers a reporting command (something like route:usage / route-usage:report); run it once against a real app to see the output shape.First use case for a Laravel dev: don't install it. Open php artisan route:list side by side with your access logs (or Laravel Telescope's request list) for a week, and eyeball which named routes never appear. That's the entire value proposition this package automates — and 90% of the benefit with 0% of the abandoned dependency.
Pattern 1 — Port the concept as middleware (recommended for Laravel).
Create a route_usage table (route string, hits integer, last_seen_at timestamp) and a lightweight middleware:
// app/Http/Middleware/TrackRouteUsage.php
public function handle(Request $request, Closure $next)
{
$response = $next($request);
if ($route = $request->route()) {
$name = $route->getName() ?? $route->uri();
DB::table('route_usage')->upsert(
[['route' => $name, 'hits' => 1, 'last_seen_at' => now()]],
['route'],
['hits' => DB::raw('hits + 1'), 'last_seen_at' => now()]
);
}
return $response;
}
Register it as terminable or wrap it in dispatchAfterResponse so it never adds latency to the response.
Pattern 2 — A comparison command.
// app/Console/Commands/UnusedRoutes.php
$defined = collect(Route::getRoutes())
->map(fn ($r) => $r->getName() ?? $r->uri())
->filter();
$used = DB::table('route_usage')->pluck('route');
$unused = $defined->diff($used);
$this->table(['Unused route'], $unused->map(fn ($r) => [$r])->all());
Schedule this weekly (->weeklyOn(1, '09:00')) and dump the output into Slack. Anything unused for 30+ days is a deletion candidate.
Pattern 3 — CI gate.
In the Symfony original this is a command that exits non-zero when unused routes exist. Mirror it in Laravel: make the command return self::FAILURE when $unused->isNotEmpty(), then run it in a PR job. Guard it with a baseline file (storage/unused-routes-baseline.json) so you ship the gate after the first cleanup, otherwise every PR fails on your existing debt.
Pattern 4 — If you genuinely need the Symfony version on a Symfony service, register the bundle in config/bundles.php, run the report against staging, and treat the output as a one-off audit. Don't wire it into production request handling — the deprecation means you're adopting an unpatched surface.
Integration tip: whatever you build, key on route name, not URI. URIs change when you add {id} constraints or a prefix; names are the stable identifier you'll actually delete by. Keep the URI as a secondary column so you can still map unnamed routes back to a controller.
composer show symplify/symfony-route-usage resolves against your framework version before you write any code assuming it works._profiler, _wdt, health, horizon.*, telescope.*, storage.local, signed-URL download routes.URL::signedRoute / temporarySignedRoute consumers.fallback routes swallow traffic invisibly — the matched route is the fallback, not the one you intended. Log the controller action too, not just the name.upsert, batch in a queue via dispatchAfterResponse, and sample (e.g. one write per route per minute) if traffic is high.TrackRouteUsage sits before SubstituteBindings / ThrottleRequests, it may never fire on 404/429 responses. Log after $next($request) and inspect $response->getStatusCode() to distinguish "never called" from "always errored".route:cache doesn't touch your usage table, so a fresh deploy against a persisted DB looks stable — but a new environment starts empty and flags everything as unused. Never run the gate before the table has a full deployment cycle of data.RouterInterface::match to record hits without HTTP middleware) is a cleaner design than middleware if you're on Symfony — it catches subrequests and ESI fragments middleware misses.route('name'), to_route('name'), hardcoded URL strings, and href="{{ route(...) }}" in Blade. The tracking only sees inbound traffic, not outbound references that are themselves dead code — remove the caller first, then the route.How can I help you explore Laravel packages today?