Router
The routing library working with PSR-7 requests.
Routes are registered once and matched against the request. A path without parameters is found by a straight lookup; one with them walks a tree, so adding routes does not make matching slower in the way a list of patterns tried in turn does.
Requirements
- PHP 8.1 or newer
Installation
composer require quillstack/routerUsage
Registering
$router->get('/', HomeController::class)->name('home');
$router->post('/users', CreateUserController::class)->name('users.create');
$router->delete('/users/:id', DeleteUserController::class)->name('users.delete');get(), post(), put(), patch(), delete(), options() and head() register one method. map('GET', …) takes the method as an argument, match(['PUT', 'PATCH'], …) takes a few of them, and any() takes them all.
Parameters
A segment written as :id or as {id} is a parameter:
$router->get('/users/:user/posts/:post', UserPostController::class)->name('user.post');The values come back on the route, and the framework puts them on the request as attributes:
$route = $dispatcher->dispatch($request);
$route->getParameters(); // ['user' => '42', 'post' => '7']
$route->getParameter('user'); // '42'
$route->getParameter('page', '1') // '1' — nothing matched, so the defaultA literal segment always wins over a parameter, whichever was registered first:
$router->get('/users/me', MeController::class);
$router->get('/users/:id', UserController::class);
// GET /users/me → MeController
// GET /users/42 → UserControllerNaming, and finding by name
$router->get('/users/:id', UserController::class)->name('users.show');
$router->getRoute('users.show')->getPath(); // '/users/:id'
$router->getRoutes(); // every route, keyed by `METHOD /path`Guarding a route
A route says what reaching it requires, and one place enforces it — a rule kept in each controller instead is a rule which is one day not kept:
$router->get('/orders', OrdersController::class)->requireAuthentication();
$router->delete('/orders/:id', DeleteOrderController::class)->requireAuthentication('admin');
$router->match(['PUT', 'PATCH'], '/orders/:id', UpdateOrderController::class)
->requireAuthentication('admin', 'support');Any one of the roles will do. Nothing is guarded unless it says so, so this changed no route anybody had already written.
A route which says this implements GuardedRouteInterface, which is kept apart from RouteInterface for the same reason: something implementing a route without answering these is a route nobody guards, which is what every route was before.
Enforcing it is quillstack/auth's job — this package only carries what was asked for.
Dispatching
Dispatcher::dispatch() answers with the route that matched, or with one of two standing for having matched nothing:
| Route | Means |
|---|---|
NotFoundRoute | nothing is registered for this path |
MethodNotAllowedRoute | the path is registered, but not for this method |
Both say false to isSuccess(), so anything only asking whether something matched keeps working. MethodNotAllowedRoute::getAllowedMethods() names the methods the path does answer to, which is what a 405 has to carry.
A path registered for GET also answers HEAD, which is what a server offering GET is expected to do — registering head() for the path still wins where it was done.
Dispatching reads the path out of the URI, so a query string does not turn a known route into a 404.
Technical documentation
| Class | What it is |
|---|---|
Router | where routes are registered, and what holds them |
Dispatcher | matches a PSR-7 request against them |
Route | one registered route |
Routes\NotFoundRoute, Routes\MethodNotAllowedRoute | what matching nothing means |
RouteTree\RouteTreeBuilder, RouteTree\RouteTreeFinder | the tree paths with parameters are matched in |
Router::normalisePath() brings a path to the form used as a key: a leading slash and no trailing one, so /users/ and users are the same route.
RouteNameNotSetException is thrown by name() when there is no route to name — that is, when it is called before any routing method.
Unit tests
composer test
composer test:coverage
composer stanDocker
docker-compose up -d
docker exec -w /var/www/html -it quillstack_router shLicense
MIT. See LICENSE.