strukt / router
Typed HTTP router with parameter matching, a middleware pipeline, reflection action resolution, and form validation.
Requires
- php: ^8.1
- strukt/contract: v1.1.1-alpha
- symfony/http-foundation: 7.3.x-dev
Requires (Dev)
- phpcompatibility/php-compatibility: dev-develop
- phpcsstandards/phpcsutils: ^1.0@dev
- phpunit/phpunit: *
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-25 15:18:42 UTC
README
A typed HTTP router with parameter matching, a middleware pipeline, reflection resolved actions, and form validation — built on the strukt4 packages.
Routes are declared against patterns such as /user/{id:int}, matched by a
compiled regex with typed parameter extraction, guarded by a middleware chain,
and dispatched by reflecting the action callable to inject the request, the
response, services, or a validated form.
use Strukt\Contract\Http\RequestInterface; $router = new Strukt\Router\Router(); $router->get('/', fn () => 'home'); $router->get('/user/{id:int}', fn (int $id) => "user[{$id}]"); $router->post('/login', fn (RequestInterface $request, Strukt\Contract\FormInterface $form) => $form->value('email')) ->form(LoginForm::class); $response = $router->kernel()->handle($request); $response->send();
Install
composer require strukt/router
Documentation
The full guide lives in docs/:
| Page | Covers |
|---|---|
| Architecture | Layers, the dispatch pipeline, matching, exception mapping. |
| Routing | Verbs, patterns, typed parameters, groups, named routes. |
| Middleware | The pipeline, global vs route, the built-in sess/auth/authz. |
| Authentication | verify(), permissions(), log in / log out flow, 401/403. |
| Forms | Rule table, custom rules, custom messages, the 422 payload. |
| Responses | Action return values, redirects, downloads, error mapping. |
| Dependency injection | Parameter resolution, bindings, middleware constructors. |
| HTTP layer | Strukt\Http classes and the contracts they implement. |
| Global helpers | router(), request(), response(), route(), matcher(). |
| The route CLI | Listing registered routes with strukt-routes. |
| Testing | Dispatching routes in Pest and asserting statuses. |
| Troubleshooting | The failure catalogue and debugging toolkit. |
| API reference | Signatures for every public class and method. |
Examples
Runnable scripts covering routing, parameters, groups, middleware, forms,
authentication, responses, service injection, and the helper API live in
examples/:
php examples/01-basic-routing.php
Routing
Use one verb method per route, or any() to accept every common method.
get(), post(), put(), patch(), delete(), options(), and head() all
share the same shape.
$router->get('/list', fn () => 'list'); $router->post('/save', fn () => 'saved'); $router->any('/hook', fn () => 'any method');
Groups
Wrap a callback with group() to share a path prefix.
$router->group('/api/v1', function (Strukt\Router\Router $router) { $router->get('/users', fn () => 'users'); $router->get('/users/{id:int}', fn (int $id) => "user[{$id}]"); });
Named routes
name() assigns a route name for reverse generation with path().
$router->get('/user/{id:int}', fn (int $id) => "user[{$id}]")->name('user.show'); $router->path('user.show', ['id' => 7]); // '/user/7'
Route options — name(), middleware(), permissions(), requiresAuth(),
form(), and defaults() — mutate the registered route fluently and can be
stacked in any order.
Parameters
A pattern is slash separated segments. A segment may be literal, a plain placeholder, a typed placeholder, a wildcard, or an optional placeholder.
| Pattern | Matches | Type |
|---|---|---|
{name} |
any segment | string |
{id:int} |
digits | int |
{price:float} |
decimal | float |
{on:bool} |
true / false |
bool |
{word:alpha} |
letters | string |
{key:alphanum} |
letters, digits, _, - |
string |
{slug} |
slug words | string |
{on:date} |
YYYY-MM-DD |
string |
{id:uuid} |
UUID | string |
{rest:*} |
remaining segments | string |
$router->get('/user/{id:int}', fn (int $id) => "user[{$id}]"); $router->get('/blog/{year:int}/{?month:int}', fn (int $year, ?int $month = null) => ''); $router->get('/files/{rest:*}', fn (string $rest) => $rest);
Matched values are cast, so {id:int} hands an int to the action. An unknown
type and a missing required placeholder both throw. Reverse generation through
uri() or Router::path() respects the same rules and route defaults.
Middleware
Global middleware runs before every route; route middleware runs after that. Definitions may be a registered name, a class, an object, or a callable.
$router->middleware(TraceMiddleware::class); $router->get('/admin', fn () => 'admin')->middleware('auth', 'authz');
Three names are registered out of the box:
| Name | Class | Purpose |
|---|---|---|
sess |
Middleware\Session |
Attach the session store to the request. |
auth |
Middleware\Authentication |
Resolve and bind the current user. |
authz |
Middleware\Authorization |
Enforce required permissions. |
Middleware resolves through constructor injection: a middleware class declares
the services it needs (SessionInterface, AuthInterface, a $verify or
$permissions callable) and the router supplies them from the per-request
service map.
Authentication and permissions
verify() and permissions() configure how users and grants are resolved.
Routes opt in with requiresAuth() and permissions([...]).
$router->session(fn (): SessionInterface => new Strukt\Http\Session\Native()); $router->verify(fn (SessionInterface $session): ?UserInterface => $session->get('email') ? new Strukt\User(email: $session->get('email')) : null); $router->permissions(fn (?UserInterface $user): array => $user?->getEmail() === 'admin@example.com' ? ['admin'] : []); $router->get('/dashboard', fn (RequestInterface $request) => $request->user()?->getEmail() ?? 'guest') ->requiresAuth(); $router->get('/admin', fn () => 'admin') ->requiresAuth() ->permissions(['admin']);
A missing user on an authenticated route stops with a 401; a user without the required permission receives a 403.
Forms
Attach a Strukt\Form\Form class with form(). The router submits the request
values and validates before the action runs; a failing validation answers with
a 422 carrying the errors as JSON.
$router->post('/login', fn (Strukt\Contract\FormInterface $form) => 'hi ' . $form->value('email')) ->form(LoginForm::class);
Responses
An action may return a string (body), an array (JSON), a ResponseInterface,
or null. Use the response() helper for typed responses and redirects:
$router->get('/home', fn () => response()->redirect('/home')); $router->get('/data', fn () => response()->json(['ok' => true])); $router->get('/file', fn () => download('/tmp/report.pdf', 'report.pdf'));
The kernel maps exceptions to HTTP errors: 404 for an unknown path, 405 with an
Allow header for a bad method, 422 for failed form validation, and 401 or 403
for the auth gates.
Helpers
| Helper | Purpose |
|---|---|
router() |
The shared Router instance. |
request($input, $method, $uri, $headers) |
Build a Request. |
response($code, $headers) |
A fluent response builder. |
route($path, $method, $input) |
Dispatch and return the response. |
matcher($patterns) |
A standalone UrlMatcher. |
redirect($url, $status = 302) |
A redirect response. |
download($path, $filename) |
A file download. |
HTTP layer
The package ships its own Strukt\Http stack implementing the
strukt4/contract interfaces: Request (with createFromGlobals()),
Response, ResponseBuilder, Download, and UploadedFile. There is no
dependency on a framework HTTP layer.
Session stores live under Strukt\Http\Session: Native persists values and
flashes in $_SESSION, and Cache persists them through a strukt/cache
driver — memcached, the filesystem, or memory — so several processes or servers
can share a session. Bind either with $router->session():
$router->session(fn (): SessionInterface => new Strukt\Http\Session\Native()); $router->session(fn (): SessionInterface => new Strukt\Http\Session\Cache(memc('sess')));
Cache needs the optional strukt/cache package. See
HTTP layer and
installation.