Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Route Laravel Package

league/route

League\Route is a fast, flexible routing and dispatch library for PHP. It supports PSR-7/PSR-15 middleware, route groups, named routes, controllers, and custom strategies, making it easy to build clean, performant HTTP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with a PSR-7/PSR-17 implementation — the package ships none of its own:

composer require league/route nyholm/psr7 nyholm/psr7-server

league/route is not a framework: it does not create requests, emit responses, or parse bodies. You bring PSR-7 objects, and you call an emitter yourself.

Where to look first

You want… Look at
The one class that matters src/Router.php (map, group, middleware, dispatch)
How controllers are invoked src/Strategy/ApplicationStrategy.php, JsonStrategy.php, AbstractStrategy.php
The actual matcher nikic/fast-route — route pattern syntax ({id:\d+}, [/{page}]) is FastRoute's, not league/route's
Docs route.thephpleague.com (v5/v6 syntax; anything mentioning RouteCollection is v4)

First use case — a JSON API in a single file:

<?php
// public/index.php
use League\Route\Router;
use League\Route\Strategy\JsonStrategy;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;

require __DIR__ . '/../vendor/autoload.php';

$psr17   = new Psr17Factory();
$request = (new ServerRequestCreator($psr17, $psr17, $psr17, $psr17))->fromGlobals();

$router = new Router();
$router->setStrategy(new JsonStrategy($psr17)); // $psr17 is the PSR-17 response factory

$router->get('/', fn () => ['hello' => 'world']);

$router->get('/users/{id:\d+}', function (ServerRequestInterface $request) {
    return ['id' => (int) $request->getAttribute('id')];
});

$response = $router->dispatch($request);

(new Laminas\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response);

That's the whole loop: Router → map → strategy → dispatch → emit. Everything else in the package is variations on strategy and middleware.

For a server-rendered app, swap JsonStrategy for ApplicationStrategy and let controllers return ResponseInterface objects directly.

Implementation Patterns

Container-resolved controllers

Set a PSR-11 container once; the strategy resolves class-strings and [Class::class, 'method'] arrays through it. Constructor injection then works for free:

$router->setContainer($container);
$router->get('/users/{id:\d+}', [UserController::class, 'show']);

Without a container, ApplicationStrategy falls back to instantiating the class directly, which breaks the moment the controller has dependencies.

Grouped routes with layered middleware

Groups give you a prefix and a middleware bucket. Global → group → route is the nesting order:

$router->middleware(new CorsMiddleware());           // runs for everything
$router->middleware($errorHandlingMiddleware);

$router->group('/api/v1', function (RouteGroup $group) {
    $group->get('/users', [UserController::class, 'index']);
    $group->post('/users', [UserController::class, 'store'])
          ->middleware(new RequireScope('users.write'));
})->middleware(new ResolveTenant());

Because middleware is composed at dispatch time (not at map() time), you can register routes and middleware in any order.

Lazy middleware for anything with dependencies

middleware() takes an instance. If your middleware needs constructor args, use lazyMiddleware() with the class-string and let the container build it at dispatch:

$router->lazyMiddleware(AuthMiddleware::class);
$router->lazyMiddlewares([RateLimitMiddleware::class, AuditMiddleware::class]);

Route-level strategy overrides

Strategy is inherited (router → group → route) and can be swapped per branch — handy for a /api prefix that returns JSON while the rest of the app returns HTML:

$router->group('/api', function (RouteGroup $group) {
    $group->setStrategy(new JsonStrategy($psr17));
    $group->get('/health', fn () => ['ok' => true]);
});

Named routes and URL generation (build it yourself)

There is no route('user.show', $id) helper. Route::setName()/getName() exist, and Route::getPath(['id' => 42]) expands placeholders — so keep a registry when you map:

$route = $router->get('/users/{id:\d+}', [UserController::class, 'show'])->setName('user.show');
$urls['user.show'] = $route;                       // store it, e.g. on the container
// later:
$url = $urls['user.show']->getPath(['id' => 42]); // "/users/42"

Turning exceptions into responses

dispatch() throws on 404/405 and rethrows controller exceptions under ApplicationStrategy. Two clean options:

// 1. Catch the routing exceptions at the edge
try {
    $response = $router->dispatch($request);
} catch (League\Route\Http\Exception\NotFoundException $e) {
    $response = new Response(404, [], 'Not found');
} catch (League\Route\Http\Exception\MethodNotAllowedException $e) {
    $response = new Response(405, ['Allow' => implode(', ', $e->getAllowedMethods())]);
} catch (Throwable $e) {
    $response = new Response(500, [], 'Server error');
}

// 2. Or wrap dispatch in a PSR-15-style error middleware added with $router->middleware()

With JsonStrategy, thrown HttpExceptionInterface exceptions are already converted into JSON error payloads carrying the right status code — you mostly get error handling for free on API routes.

Testing without a web server

Routes are just a function from request to response, so tests are trivial:

$router = require __DIR__ . '/../routes.php';  // return the configured Router
$response = $router->dispatch(new ServerRequest('GET', '/users/1'));
$this->assertSame(200, $response->getStatusCode());
$this->assertJsonStringEqualsJsonString('{"id":1}', (string) $response->getBody());

Return the configured Router from a bootstrap file rather than building it inside a controller; you'll reuse that same file for public/index.php and the tests.

Gotchas and Tips

Every tutorial you'll find is v4. RouteCollection was folded into Router in v5. If a snippet starts with new RouteCollection() or $router->setStrategy(...)->dispatch(...) on a v4-shaped object, translate it: Router is the entry point, and dispatch() takes exactly one ServerRequestInterface.

Trailing slashes are significant. FastRoute is an exact matcher: /users and /users/ are different routes and /users/1/ will 404 against /users/{id}. Add a tiny middleware that rewrites the URI (or register both), and never rely on implicit normalisation.

Group prefixes are concatenated verbatim. group('/api/', ...) + '/users' produces //api//users. Pick a convention ('/api' + '/users') and stick to it.

Route params are strings, always. {id:\d+} matches digits but $request->getAttribute('id') is a string. Cast in the controller. (Older v4 code passed params as a second array $args argument; v5+ puts them on the request as attributes — check whichever strategy you copy.)

JsonStrategy needs a PSR-17 response factory. Forget it and the JSON error path blows up with a container error while the happy path works, which is a confusing way to find out. Provide it in the constructor or bind ResponseFactoryInterface in the container.

Middleware ordering is registration order within each layer, and the layer order is global → group → route. If an auth check needs to run before a body-parsing middleware, both must live in the same layer, in the right order. A throwaway middleware that appends to a log array is the fastest way to confirm the chain you actually built.

Don't swallow 405s into 404s. MethodNotAllowedException extends the same hierarchy as NotFoundException; a careless catch (RuntimeException $e) turns "you used the wrong verb" into "this route doesn't exist" and makes client debugging miserable. Always expose the Allow header via getAllowedMethods().

No automatic HEAD/OPTIONS, no method override. Register HEAD explicitly if clients need it, and implement CORS preflight as a middleware that short-circuits OPTIONS before routing — an unhandled OPTIONS request is just a 405.

You must emit the response. dispatch() returns a ResponseInterface and stops. If the page is blank, you're missing the emitter, not the route.

PSR-7 stream position bites on hand-built responses. After $response->getBody()->write(...), rewind() before returning (or build the body through the stream factory) — some emitters and most (string) $body casts in tests will silently return an empty string otherwise.

Long-running workers (RoadRunner, Swoole, Octane-style setups). A Router is single-use state: routes accumulate. Build and discard a router per request, or keep one immutable router and never call map() inside the request cycle. Same rule for RequestContext-based URI building.

Route compilation is per-process. FastRoute compiles its data on the first dispatch(). A few hundred routes cost nothing; tens of thousands in a cold process is worth benchmarking, and it's the first place to look if boot time regresses after a route-file refactor.

Debug a route table by dumping Route objects, not by adding echo statements to controllers: every map()/get() returns the Route, and getPath(), getName(), getMethod() and the middleware list tell you what you actually registered.

Extension points worth knowing: implement League\Route\Strategy\StrategyInterface (or extend AbstractStrategy) when you want a different return contract — e.g. "controllers return arrays, we render them via Twig"; subclass Route/RouteGroup and override Router::map()/group() to return your own types when you need per-route metadata; and use RouteConditionHandlerInterface (setHost(), setScheme(), setPort()) for host-scoped routes instead of hand-parsing Host headers in every controller.

Config quirks: global middleware registered after dispatch() has started is ignored, and lazyMiddleware() resolves through the container — if you never called setContainer(), lazy middleware fails at dispatch time rather than at registration, so a passing unit test on route registration proves nothing.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console