Search by

phil / route-access-check

phil

Checks in continuous integration that every route of an application has an access rule: #[IsGranted], access_control entries read with Symfony's request matchers, firewalls without security, and #[PublicRoute] or a route option for the routes public on purpose; with conflicts, unused entries, a base

0.1.0-alpha 2026-11-11 06:11 UTC

This package is auto-updated.

Last update: 2026-11-11 13:32:24 UTC


README

Every route has an access rule, or says why it is public: checked in continuous integration, before it ships.

phil/route-access-check reads the routes of an application and, for each one, finds the rule that decides who may reach it: a #[IsGranted] on its controller or action, the first access_control entry that matches it, a firewall with security: false, or an explicit public declaration — #[PublicRoute(reason: …)] or a route option. A route none of them covers fails the check. So does a route that contradicts itself: declared public but protected, or protected behind a firewall without security, where the check always denies.

It is for any Symfony application whose routes multiply faster than anyone reviews them: a new controller without an attribute, an access_control path that no longer matches after a prefix changed, a debug route left open. The report lists every route with its resolved rule, so that a reviewer sees what protects it, and the access_control entries that reach no route, so that dead rules are removed. A baseline tolerates the failures of a large application while it adopts the check, and only ever shrinks.

It is a plain PHP library built on symfony/routing and symfony/http-foundation. The access_control entries and the firewalls are matched with the request matchers of symfony/http-foundation, the ones Symfony's security uses, on a sample request built for each route. The attributes are read by name through reflection: symfony/security-http is not a dependency. Nothing runs in production; the check is a dev and CI tool.

Status

Part State
#[IsGranted] on class and action, with its methods; IsGranted('PUBLIC_ACCESS') Implemented and tested
#[PublicRoute(reason: …)] and the public route option Implemented and tested
Firewalls and access_control entries matched per method label, first match wins Implemented and tested
Conflicts, uncovered methods, unused access_control entries Implemented and tested
Baseline, text, JSON, GitHub annotations and JUnit reports Implemented and tested
Custom detectors and formats through interfaces Implemented and tested
A Symfony console command over the router and the security configuration Implemented in phil/route-access-check-bundle
Evaluating allow_if expressions and voters Not planned: a check is a check, whatever it decides

Documentation

Document What it covers
Installation Requirements, installing, running a check without Symfony's kernel.
Access rules Each source of rules, how it is read, and how a route's status is decided.
Public routes #[PublicRoute], the route option, and when a route is public without them.
Reports and baseline The four formats, the warnings, the baseline file.
Extension points Detectors and formats of the application's own.
Exceptions RouteAccessCheckException and its three exceptions.
Testing The library's tests, mutation bar, fuzz target and benchmark.
Reference Every type of the library, with its role.

Quick start

composer require --dev phil/route-access-check

In a Symfony application, install phil/route-access-check-bundle and run bin/console phil:route-access:check. Without it:

use Phil\RouteAccessCheck\Detector\PublicRouteDetector;
use Phil\RouteAccessCheck\Detector\SecurityAttributeDetector;
use Phil\RouteAccessCheck\Detector\SecurityConfigDetector;
use Phil\RouteAccessCheck\Format\TextFormatter;
use Phil\RouteAccessCheck\RouteAccessChecker;
use Phil\RouteAccessCheck\Security\SecurityMap;

$map = SecurityMap::fromArray([
    'firewalls' => [['name' => 'dev', 'request' => ['path' => '^/(_profiler|_wdt)'], 'security' => false], ['name' => 'main']],
    'access_control' => [['request' => ['path' => '^/admin'], 'roles' => ['ROLE_ADMIN']]],
]);

$checker = new RouteAccessChecker([new SecurityAttributeDetector(), new PublicRouteDetector(), new SecurityConfigDetector($map)]);
$report = $checker->check($router->getRouteCollection());

echo new TextFormatter()->format($report);
exit($report->hasFailures() ? 1 : 0);
use Phil\RouteAccessCheck\Attribute\PublicRoute;

#[PublicRoute(reason: 'The sign-in form is reached before signing in.')]
public function signIn(): Response

Requirements

PHP 8.5 or later with ext-mbstring, symfony/routing and symfony/http-foundation.

Contributing

Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.

License

MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.