rareloop/router
Rareloop Router is a lightweight, fast PHP router built around PSR-7 and PSR-15. It supports route parameters, groups, middleware, named routes and route caching, making it easy to add clean, expressive routing to any PHP application without a full framework.
rareloop/router is a FastRoute-backed, Laravel-flavoured router that speaks PSR-7 in and PSR-7 out. It is a routing library, not a framework: it gives you route registration, groups, middleware, named routes and URL generation, and expects you to bring your own PSR-7 implementation, PSR-17 response factory and (optionally) PSR-11 container.
composer require rareloop/router \
nyholm/psr7 nyholm/psr7-server # or laminas-diactoros
Minimal bootstrap:
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;
use Rareloop\Router\Router;
$factory = new Psr17Factory(); // PSR-17 response factory
$router = new Router($factory); // optionally: new Router($factory, $container)
$router->get('/', fn () => 'Hello world');
$router->get('/posts/{id}', 'PostController@show');
$request = (new ServerRequestCreator($factory, $factory, $factory, $factory))
->fromGlobals();
$response = $router->handle($request);
// emit $response with whatever emitter you already use (laminas-httphandlerrunner, etc.)
Where to look first:
src/Router.php — registration (get/post/put/patch/delete/options/any), group(), url(), handle().src/Route.php and src/RouteGroup.php — everything returned by a registration call is fluent, which is where middleware/names live.src/RouteParams.php and the callable resolver — this is how handler arguments get injected.First real use case: you have an existing PSR-15 stack (Relay, Laminas Mezzio, a hand-rolled front controller) or a small JSON API, and you want Laravel-style route ergonomics without pulling in a framework. Drop the router at the end of your middleware pipeline and start registering routes.
Handlers are closures, Controller@method strings, [Class::class, 'method'] arrays, or invokables. When a PSR-11 container is passed to the constructor, controller strings are resolved through it, so constructor injection works normally:
$router = new Router($factory, $container);
$router->get('/posts/{id}', 'PostController@show');
Route parameters arrive as handler arguments and via RouteParams. Name your method parameters after the placeholders, or type-hint RouteParams to read them explicitly:
$router->get('/posts/{id:\d+}', function (RouteParams $params) {
return $params->get('id');
});
class PostController {
public function show(RouteParams $params, PostRepository $posts) { /* ... */ }
}
Inline regex constraints ({id:\d+}) and FastRoute optional segments (/posts[/{page}]) both work — use them instead of validating in the handler.
Middleware is PSR-15, attached per route or per group, and resolved from the container when given a class string. Order matters: group middleware wraps route middleware, and both run before your handler.
$router->get('/admin', 'AdminController@index')
->middleware(AuthMiddleware::class)
->middleware(AdminOnlyMiddleware::class);
Groups handle prefixes, shared middleware and name prefixes — this is the pattern you will use most day-to-day:
$router->group('/api/v1', function (Router $router) {
$router->get('/users', 'UserController@index')->name('users.index');
$router->post('/users', 'UserController@store');
})->middleware(ApiAuth::class)
->name('api.');
Named routes + URL generation keeps redirects and links from hardcoding paths:
$router->get('/posts/{id}', 'PostController@show')->name('posts.show');
$url = $router->url('posts.show', ['id' => 42]); // -> /posts/42
For a Laravel developer this is the route() helper equivalent; wrap it in a small helper or inject the router into your view layer.
Return values are flexible. Return a ResponseInterface for full control, or a string/array-ish scalar and let the configured response factory build the response. For JSON APIs, a tiny base controller that wraps json_encode into a Response beats relying on implicit conversion.
Sub-routers as a composition tool. Because Router is a PSR-15 participant, you can mount one router inside another group, or keep a router per module and chain them — useful for splitting a large route file without a service provider abstraction.
Debugging and introspection. $router->getRoutes() gives you the registered collection; dumping it in a test or a routes:list-style command is the fastest way to see names, methods and patterns. $router->currentRoute() after a match exposes the matched Route (name, params, middleware) for logging.
Router without a PSR-17 factory (or without setting one), returning a string from a handler will fail. Pass a factory in the constructor and forget about it./posts and /posts/ as different patterns. Pick a convention, normalise in front-controller middleware, and don't be surprised by 404s during development.POST to a GET-only path. If you need Laravel's 405 semantics, wrap the router and translate FastRoute's "allowed methods" result.route:cache, the dispatcher is built per request. For high-traffic apps, wire FastRoute's cached dispatcher yourself, or accept the cost (it is small — measure before optimising).Controller@method is instantiated with new, so any constructor dependency breaks. If you use controllers at all, pass a container.RouteParams avoids the coupling.GET route automatically, and CORS preflight is middleware's job. Register any() or explicit options() routes and add a CORS middleware if you serve browsers.$handler->handle(), is the most common bug in PSR-15 code — the router itself won't catch it.Router so swapping to Slim, FastRoute directly, or Laravel later is a one-file change.How can I help you explore Laravel packages today?