A zero-dependency PHP 8.2 router library with URL parameters, route groups, middleware pipeline, named routes, and a static facade.
- PHP 8.2+
composer require bromimo/tiny-router<?php
require 'vendor/autoload.php';
use TinyRouter\Facade\Route;
use TinyRouter\Http\Request;
use TinyRouter\Http\Response;
Route::get('/', fn() => new Response('Hello, world!'));
Route::get('/users/{id:\d+}', fn(Request $req) => new Response($req->params['id']));
Route::dispatch(Request::fromGlobals())->send();Route::get('/path', $handler);
Route::post('/path', $handler);
Route::put('/path', $handler);
Route::patch('/path', $handler);
Route::delete('/path', $handler);
Route::options('/path', $handler);Register the same handler for multiple HTTP methods:
use TinyRouter\Http\Method;
Route::match([Method::GET, Method::POST], '/form', $handler)
->name('form.handle')
->middleware('auth');// Any value
Route::get('/users/{id}', fn(Request $req) => new Response($req->params['id']));
// With regex constraint
Route::get('/users/{id:\d+}', $handler);
Route::get('/posts/{slug:[a-z-]+}', $handler);// Closure
Route::get('/a', fn(Request $req) => new Response('ok'));
// Invokable class instance
Route::get('/b', new MyHandler());
// [ClassName, method] — instantiated by the router
Route::get('/c', [UserController::class, 'index']);Route::get('/users/{id}', $handler)->name('users.show');
$url = Route::url('users.show', ['id' => 42]); // → '/users/42'Implement MiddlewareInterface:
use TinyRouter\Contract\MiddlewareInterface;
use TinyRouter\Http\Request;
use TinyRouter\Http\Response;
class AuthMiddleware implements MiddlewareInterface
{
public function handle(Request $request, callable $next): Response
{
if (empty($request->headers['authorization'])) {
return new Response('Unauthorized', 401);
}
return $next($request);
}
}Register middleware:
// Global — runs for every route
Route::addMiddleware(AuthMiddleware::class);
Route::addMiddleware(new AuthMiddleware());
// Per-route (class name)
Route::get('/profile', $handler)->middleware(AuthMiddleware::class);
// Per-route (instance)
Route::get('/profile', $handler)->middlewareInstance(new AuthMiddleware());Execution order: global → group → route → handler.
Register short names for middleware classes:
Route::addMiddlewareAlias('auth', AuthMiddleware::class);
Route::addMiddlewareAlias('cors', CorsMiddleware::class);
Route::get('/profile', $handler)->middleware('auth');Register middleware factories that accept parameters via : syntax:
Route::addMiddlewareFactory('rate_limit', function (string $params): MiddlewareInterface {
[$requests, $seconds] = explode(',', $params);
return new RateLimitMiddleware((int)$requests, (int)$seconds);
});
Route::get('/api/data', $handler)->middleware('rate_limit:5,60');Middleware resolution order:
MiddlewareInterfaceinstance — used directly- Exact alias match (
addMiddlewareAlias) - Parameterized factory (
alias:params→addMiddlewareFactory) - Class name fallback — instantiated via
new
Route::group('/admin', function () {
Route::get('/dashboard', fn() => new Response('Dashboard'));
Route::get('/users', fn() => new Response('Users'));
}, middlewares: [AuthMiddleware::class]);Groups support nesting:
Route::group('/api', function () {
Route::group('/v1', function () {
Route::get('/status', fn() => new Response('ok'));
// → GET /api/v1/status
});
});Build groups with a chainable API:
Route::prefix('/api')
->middleware('auth')
->group(function () {
Route::get('/users', $handler);
Route::post('/users', $handler);
});
// Middleware only, without prefix
Route::middleware('auth')->group(function () {
Route::get('/profile', $handler);
Route::get('/settings', $handler);
});
// Multiple middleware calls accumulate
Route::prefix('/admin')
->middleware('auth')
->middleware('admin')
->group(function () {
Route::get('/dashboard', $handler);
});Request::fromGlobals() automatically parses request bodies for POST, PUT, PATCH and DELETE methods:
Content-Type: application/json— decoded viajson_decode()Content-Type: application/x-www-form-urlencoded— parsed viaparse_str()
Parsed data is available in $request->body:
Route::put('/users/{id}', function (Request $req) {
$name = $req->body['name'];
// ...
});// Body + status
new Response('Not Found', 404);
// With headers
(new Response('{"ok":true}', 200))
->withHeader('Content-Type', 'application/json');
// Send to browser
$response->send();use TinyRouter\Exception\NotFoundException;
use TinyRouter\Exception\MethodNotAllowedException;
try {
Route::dispatch(Request::fromGlobals())->send();
} catch (NotFoundException $e) {
(new Response('Not Found', 404))->send();
} catch (MethodNotAllowedException $e) {
(new Response('Method Not Allowed', 405))
->withHeader('Allow', implode(', ', $e->getAllowedMethods()))
->send();
}// Get all registered routes
$routes = Route::routes();
foreach ($routes as $route) {
echo $route->method->value . ' ' . $route->pattern;
}Useful for testing or dependency injection:
use TinyRouter\Routing\Router;
use TinyRouter\Http\Request;
use TinyRouter\Http\Method;
$router = new Router();
$router->get('/hello', fn(Request $req) => new Response('world'));
$request = new Request(Method::GET, '/hello', [], [], []);
$response = $router->dispatch($request);In tests, reset the facade between test cases:
use TinyRouter\Facade\Route;
use TinyRouter\Routing\Router;
protected function setUp(): void
{
Route::swap(new Router());
}composer install
./vendor/bin/phpunitsrc/
Contract/
MiddlewareInterface.php — handle(Request, callable $next): Response
Exception/
HttpException.php — base exception
NotFoundException.php — 404
MethodNotAllowedException.php — 405
Facade/
Route.php — static facade over singleton Router
Http/
Method.php — enum: GET POST PUT PATCH DELETE OPTIONS HEAD
Request.php — readonly, factory: fromGlobals() with body parsing
Response.php — body, status, headers + send()
Routing/
Route.php — value object: method, pattern, handler, middlewares
RouteDefinition.php — fluent builder returned by Router::get() etc.
MultiRouteDefinition.php — fluent builder for Router::match()
RouteCollection.php — stores and matches routes
GroupDefinition.php — value object returned by group()
PendingRouteGroup.php — fluent builder for prefix/middleware chains
Router.php — registers routes, dispatches requests
UrlGenerator.php — generates URL from named route + params
MIT