Skip to content

Router

composer require quillstack/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

shell
composer require quillstack/router

Usage

Registering

php
$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:

php
$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:

php
$route = $dispatcher->dispatch($request);

$route->getParameters();          // ['user' => '42', 'post' => '7']
$route->getParameter('user');     // '42'
$route->getParameter('page', '1') // '1' — nothing matched, so the default

A literal segment always wins over a parameter, whichever was registered first:

php
$router->get('/users/me', MeController::class);
$router->get('/users/:id', UserController::class);

// GET /users/me   → MeController
// GET /users/42   → UserController

Naming, and finding by name

php
$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:

php
$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:

RouteMeans
NotFoundRoutenothing is registered for this path
MethodNotAllowedRoutethe 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

ClassWhat it is
Routerwhere routes are registered, and what holds them
Dispatchermatches a PSR-7 request against them
Routeone registered route
Routes\NotFoundRoute, Routes\MethodNotAllowedRoutewhat matching nothing means
RouteTree\RouteTreeBuilder, RouteTree\RouteTreeFinderthe 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

shell
composer test
composer test:coverage
composer stan

Docker

shell
docker-compose up -d
docker exec -w /var/www/html -it quillstack_router sh

License

MIT. See LICENSE.

Released under the MIT License.