studoo / edu-route
Fast request router for PHP 8.4+ (modernized fork of nikic/fast-route)
Requires
- php: ~8.4.0 || ~8.5.0
- psr/http-message: ^2.0
- psr/simple-cache: ^2.0 || ^3.0
Requires (Dev)
- lcobucci/coding-standard: ^12.1
- nyholm/psr7: ^1.8.2
- phpbench/phpbench: ^1.7
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.3
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.1
- phpstan/phpstan-strict-rules: ^2.1
- phpunit/phpunit: ^13.4
- psr/http-factory: ^1.1
- psr/http-server-middleware: ^1.0.2
- rector/rector: ^2.8
Suggests
- psr/http-factory: Pour utiliser Studoo\EduRoute\Http\RoutingMiddleware (réponses 404/405 PSR-17)
- psr/http-server-middleware: Pour utiliser Studoo\EduRoute\Http\RoutingMiddleware (PSR-15)
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-06 20:42:32 UTC
README
studoo/edu-route associe une URL et une méthode HTTP à un handler (un nom de contrôleur,
une closure, n'importe quelle donnée). C'est la couche de routage d'edu-framework.
Il s'agit d'un fork modernisé de nikic/FastRoute, dont il
reprend l'algorithme à base d'expressions régulières combinées (explication de son
fonctionnement), réécrit pour PHP 8.4 : énumérations, objets immuables, attributs
#[\Override], analyse statique au niveau maximal.
Installation
composer require studoo/edu-route
PHP 8.4 ou 8.5 requis.
Utilisation rapide
<?php require 'vendor/autoload.php'; use Studoo\EduRoute\ConfigureRoutes; use Studoo\EduRoute\Dispatcher\Result\Matched; use Studoo\EduRoute\Dispatcher\Result\MethodNotAllowed; use Studoo\EduRoute\Dispatcher\Result\NotMatched; use Studoo\EduRoute\EduRoute; use Studoo\EduRoute\HttpMethod; $router = EduRoute::recommendedSettings( static function (ConfigureRoutes $r): void { $r->get('/users', 'get_all_users_handler'); // {id} doit être un nombre (\d+) $r->get('/user/{id:\d+}', 'get_user_handler'); // Le suffixe /{title} est optionnel $r->addRoute(HttpMethod::Get, '/articles/{id:\d+}[/{title}]', 'get_article_handler'); }, __DIR__ . '/var/cache/routes.php', // fichier de cache (voir « Cache ») ); $dispatcher = $router->dispatcher(); // Récupère la méthode et l'URI, sans la query string (?foo=bar), puis décode l'URI $httpMethod = $_SERVER['REQUEST_METHOD']; $uri = rawurldecode(strtok($_SERVER['REQUEST_URI'], '?')); $result = $dispatcher->dispatch($httpMethod, $uri); echo match (true) { $result instanceof Matched => call_user_func($result->handler, $result->variables), $result instanceof MethodNotAllowed => '405 – méthodes autorisées : ' . implode(', ', $result->allowedMethods), $result instanceof NotMatched => '404 – page introuvable', };
Définir les routes
Les routes sont déclarées dans une closure qui reçoit un ConfigureRoutes :
$r->addRoute($method, $routePattern, $handler);
Méthodes HTTP
$method accepte un cas de l'énumération HttpMethod, une chaîne en majuscules (y compris une
méthode non standard comme 'PURGE') ou un tableau mélangeant les deux :
$r->addRoute(HttpMethod::Get, '/test', 'handler'); $r->addRoute('GET', '/test', 'handler'); $r->addRoute([HttpMethod::Put, 'PATCH'], '/test', 'handler');
Des raccourcis existent : get(), post(), put(), patch(), delete(), head(),
options() et any(). Cette dernière déclare une route de repli (HttpMethod::Any, soit
'*'), utilisée quand aucune route n'existe pour la méthode demandée. Une requête HEAD sans
route dédiée retombe automatiquement sur la route GET.
Motifs d'URL
{foo} est un paramètre nommé foo qui accepte [^/]+ (tout sauf /). On précise un autre
motif avec {nom:regex} :
// /user/42 → OK, /user/xyz → non $r->get('/user/{id:\d+}', 'handler'); // /user/foobar → OK, /user/foo/bar → non $r->get('/user/{name}', 'handler'); // /user/foo/bar → OK $r->get('/user/{name:.+}', 'handler');
Un motif ne peut pas contenir de groupe capturant : {lang:(en|de)} est refusé, utilisez
{lang:en|de} ou {lang:(?:en|de)}.
Les parties entre [...] sont optionnelles, et peuvent s'imbriquer, mais uniquement en fin
de route :
// /user/foobar et /user/foobar/42 $r->get('/user/{name}[/{id:\d+}]', 'handler');
Le handler peut être n'importe quelle donnée : un nom de contrôleur
('App\Controller\UserController::show'), un tableau, une closure… Le routeur indique seulement
quel handler correspond ; c'est à votre application de l'exécuter.
Avec un cache (activé par défaut par
recommendedSettings()), les handlers doivent être exportables : chaînes, tableaux, scalaires. Une closure ou un objet lèveCannotCacheRoutes; ils ne fonctionnent qu'avec->disableCache().
Groupes
Toutes les routes d'un groupe partagent un préfixe ; les groupes peuvent s'imbriquer :
$r->addGroup('/admin', static function (ConfigureRoutes $r): void { $r->get('/do-something', 'handler'); // /admin/do-something $r->get('/do-another-thing', 'handler'); // /admin/do-another-thing });
Routes nommées et génération d'URL
Une route nommée (paramètre ConfigureRoutes::ROUTE_NAME) permet de construire son URL plutôt
que de l'écrire en dur dans les vues :
$r->get('/articles/{year}[/{month}]', 'archive', [ConfigureRoutes::ROUTE_NAME => 'blog.archive']); $uris = $router->uriGenerator(); echo $uris->forRoute('blog.archive', ['year' => '2026']); // /articles/2026 echo $uris->forRoute('blog.archive', ['year' => '2026', 'month' => '11']); // /articles/2026/11
Les paramètres en trop deviennent une query string avec ->asUri($psr7Uri). Une exception
UriCouldNotBeGenerated signale une route inconnue, un paramètre manquant ou une valeur qui ne
respecte pas le motif.
Routes par attributs
Plutôt que de lister les routes dans une closure, on peut les déclarer directement sur les
méthodes des contrôleurs avec l'attribut #[Route] (répétable), et un préfixe commun avec
#[RouteGroup] :
use Studoo\EduRoute\Attribute\Route; use Studoo\EduRoute\Attribute\RouteGroup; use Studoo\EduRoute\HttpMethod; #[RouteGroup('/admin/product', namePrefix: 'admin.product.')] final class ProductController { #[Route('', name: 'index')] // GET /admin/product public function index(): string { /* ... */ } #[Route('/{id:\d+}', methods: [HttpMethod::Put, 'PATCH'], name: 'update')] #[Route('/{id:\d+}/edit', methods: HttpMethod::Post)] public function update(): string { /* ... */ } }
Les routes sont enregistrées par AttributeRouteLoader, dans l'ordre des contrôleurs puis des
méthodes :
use Studoo\EduRoute\Attribute\AttributeRouteLoader; $router = EduRoute::recommendedSettings( static function (ConfigureRoutes $r): void { AttributeRouteLoader::register($r, HomeController::class, ProductController::class); }, __DIR__ . '/var/cache/routes.php', );
Le handler de chaque route est la chaîne 'App\Controller\ProductController::update' : elle
reste compatible avec le cache.
Règles, vérifiées à l'enregistrement (BadRouteException avec le contrôleur et la méthode en cause) :
- un contrôleur est une classe instanciable (ni abstraite, ni interface, ni enum) ;
- une méthode routée est publique, non statique, non magique (sauf
__invoke) et déclarée dans le contrôleur lui-même ou dans un de ses traits ; les méthodes héritées d'une classe parente sont refusées (enregistrez une route sur une méthode redéclarée) ; #[RouteGroup]ne s'applique qu'à la classe qui le porte : il n'est pas hérité ;- ordre d'enregistrement : les méthodes propres du contrôleur dans l'ordre du fichier, puis celles de ses traits.
Résultat du dispatch
dispatch(HttpMethod|string $method, string $uri) renvoie l'un de ces trois objets immuables :
| Objet | Signification | Données |
|---|---|---|
Matched |
une route correspond | handler, variables (['id' => '42']), extraParameters |
MethodNotAllowed |
l'URI existe, pas pour cette méthode (réponse 405) | allowedMethods, à renvoyer dans l'en-tête Allow |
NotMatched |
aucune route (réponse 404) | — |
L'accès historique sous forme de tableau (
$result[0],[1],[2], comparé aux constantesDispatcher::FOUND…) fonctionne encore mais est déprécié : préférezinstanceof.
Middleware PSR-15
Dans une application PSR-7/PSR-15 (installez psr/http-server-middleware et
psr/http-factory), RoutingMiddleware fait le routage :
use Studoo\EduRoute\Http\RoutingMiddleware; $middleware = new RoutingMiddleware($router->dispatcher(), $psr17ResponseFactory);
- route trouvée : la requête reçoit l'attribut
RoutingMiddleware::ROUTE_ATTRIBUTE(l'objetMatched, donc le handler) et un attribut par variable ($request->getAttribute('id')), puis passe au middleware suivant, qui exécute le contrôleur ; - méthode non autorisée : réponse 405 avec l'en-tête
Allow(qui inclutHEADdès queGETest autorisé,HEADétant servi par les routesGET) ; - aucune route : réponse 404.
Cache
Analyser les routes et construire les expressions régulières a un coût. Avec
recommendedSettings($routes, $cacheKey), le résultat est mis en cache dans le fichier
$cacheKey (FileCache, mis en cache par OPcache).
Le cache n'est jamais invalidé automatiquement : supprimez le fichier quand vos routes changent ou quand vous changez de stratégie de dispatch (le format des données dépend de la stratégie), ou utilisez une clé différente par stratégie.
// Développement : pas de cache $router = EduRoute::recommendedSettings($routes, 'unused')->disableCache(); // Autre stockage : tout cache PSR-16 (Redis, APCu…) use Studoo\EduRoute\Cache\Psr16Cache; $router = EduRoute::recommendedSettings($routes, 'routes') ->withCache(new Psr16Cache($psr16Cache), 'routes');
Chargement paresseux
lazyDispatcher() et lazyUriGenerator() renvoient des objets paresseux (lazy objects de
PHP 8.4) : les routes ne sont lues dans le cache, ou calculées, qu'au premier appel. Pratique
quand une partie des requêtes ne route rien (commande CLI, réponse servie par un middleware en
amont) :
$dispatcher = $router->lazyDispatcher(); // rien n'est chargé ici $result = $dispatcher->dispatch($method, $uri); // les routes sont chargées ici, une seule fois
Une fois initialisé, l'objet est un dispatcher ordinaire, sans surcoût. Seule limite : un dispatcher maison qui hérite d'une classe interne de PHP ne peut pas être paresseux ; il est alors construit immédiatement.
Choisir la stratégie de dispatch
Quatre stratégies sont fournies ; elles donnent le même résultat mais combinent différemment les
expressions régulières. Les comparer est un bon exercice (make phpbench).
| Méthode | Stratégie |
|---|---|
useMarkDispatcher() (par défaut) |
verbe PCRE (*MARK) : la plus rapide |
useGroupCountDispatcher() |
nombre de groupes capturants |
useGroupPosDispatcher() |
position du dernier groupe capturant utilisé |
useCharCountDispatcher() |
suffixe de longueur variable |
useCustomDispatcher() et withUriGenerator() permettent de brancher vos propres
implémentations des interfaces DataGenerator, Dispatcher et GenerateUri.
Architecture
Le routage passe par trois composants :
RouteParser(RouteParser\Std) transforme/user/{id:\d+}[/{name}]en segments statiques et paramètres ;DataGeneratorregroupe les routes statiques dans une table de hachage et combine les routes variables en quelques grandes expressions régulières ;Dispatchercherche d'abord une route statique, puis teste les expressions régulières.
Contribuer
make ci # standard de code, PHPStan (niveau max), Rector, PHPUnit make phpbench # benchmarks
Voir CHANGELOG.md et, pour migrer depuis nikic/fast-route, UPGRADE.md.
Crédits et licence
Code d'origine © Nikita Popov et les contributeurs de FastRoute ; modernisation © Studoo. Distribué sous licence BSD-3-Clause (LICENSE).