Upgrade Guide
Upgrading to 7.0
This guide covers the breaking changes introduced in version 7.0 and what you need to do to upgrade.
PHP Version
The minimum supported PHP version has been raised from 8.1 to 8.3. PHP 8.2 has reached end-of-life and is no longer supported. Ensure your environment is running PHP 8.3 or later before upgrading.
PHP 8.3 Language Features
Version 7.0 adopts several PHP 8.3 language features:
- Typed class constants: All class constants now have explicit type declarations. If you extend
Routerand redefine theIDENTIFIER_SEPARATORconstant, ensure your value is astring. #[\Override]attributes: Methods implementing interface contracts now use#[\Override]for compile-time safety. This is an internal change and does not affect your code unless you extend library classes and override the same methods.
RouterInterface
A new League\Route\RouterInterface has been introduced that extends PSR-15’s RequestHandlerInterface. Both League\Route\Router and League\Route\Cache\Router implement this interface.
If you have code that type-hints against League\Route\Router directly, consider updating to type-hint against RouterInterface instead to allow switching between implementations:
<?php declare(strict_types=1);
use League\Route\RouterInterface;
function registerRoutes(RouterInterface $router): void
{
$router->map('GET', '/', 'HomeController::index');
}
Route Matching
A new match() method is available on both Router and Cache\Router. It returns a MatchResult value object containing a MatchStatus enum (Found, NotFound, MethodNotAllowed) without executing your handlers.
This is a purely additive change and requires no action unless you want to take advantage of it. See Route Matching for details.
URL Generation
A new UrlGeneratorInterface provides reverse routing from named routes. Both Router and Cache\Router implement this interface.
<?php declare(strict_types=1);
use League\Route\UrlGeneratorInterface;
$router = new League\Route\Router;
$router->get('/users/{id}', 'UserController::show')->setName('users.show');
$url = $router->generateUrl('users.show', ['id' => '42']);
// => /users/42
This is a purely additive change. See Routes for full documentation including query string parameters, optional segments, and error handling.
If you type-hint against UrlGeneratorInterface separately from RouterInterface, your code can generate URLs without depending on the full router.
generateUrl() now supports FastRoute optional segments ([/{param}]). Optional parameters are resolved from defaults set via setVars() or omitted entirely. See Routes for details.
Middleware Groups
Named middleware groups allow you to define a collection of middleware once and apply it by name across multiple routes or groups. See Middleware for full documentation.
<?php declare(strict_types=1);
$router->defineMiddlewareGroup('api', [
Acme\AuthMiddleware::class,
Acme\ThrottleMiddleware::class,
]);
$router->group('/api', function ($group) {
$group->get('/users', 'UserController::index');
})->middlewareGroup('api');
This is a purely additive change and requires no action unless you want to take advantage of it.
Route Freezing
Routes are now frozen (made immutable) after prepareRoutes() completes. Calling configuration setters such as setHost(), setScheme(), setPort(), setName(), setStrategy(), setVars(), or middleware() on a frozen route throws LogicException.
If you modify route objects after dispatch, your code will now throw an exception instead of silently having no effect. Move any route configuration before the first call to dispatch() or match().
Route::setPathVars() is exempted from freezing as it is an internal dispatcher operation. It is now marked @internal and should not be called by application code.
The FreezeableInterface and FreezeableTrait provide the immutability mechanism. If you extend Route, the freezing behaviour is inherited automatically.
Matched Route in Middleware
The matched Route object is now added to the request attributes during dispatch, keyed by Route::class. Middleware can retrieve it to inspect the matched route for authorisation, logging, or analytics. See Middleware for examples.
This is a purely additive change. The route object in the attributes is frozen, so middleware cannot modify route configuration.
MethodNotAllowedException
MethodNotAllowedException now exposes a getAllowedMethods(): array method that returns the allowed HTTP methods as a typed array. Previously, the allowed methods were only accessible by parsing the Allow header string from getHeaders().
<?php declare(strict_types=1);
try {
$router->dispatch($request);
} catch (League\Route\Http\Exception\MethodNotAllowedException $e) {
$allowed = $e->getAllowedMethods();
}
This is a purely additive change. The Allow header continues to be set as before.
Dispatcher Composition
The internal Dispatcher class no longer extends FastRoute\Dispatcher\GroupCountBased. It now wraps the FastRoute dispatcher via composition.
If you extended League\Route\Dispatcher, your code will need updating:
- The class no longer inherits from
GroupCountBasedDispatcher. Calls toparent::dispatch()will fail. - The constructor now requires three arguments:
$routesData,$strategy, and$routeMap. The oldsetRouteMap()method has been removed. Dispatcherno longer implementsRouteConditionHandlerInterface. ThesetHost(),setName(),setPort(), andsetScheme()methods are no longer available on the Dispatcher.
A new DispatcherInterface (marked @internal) has been introduced with dispatchRequest() and matchRequest(). This is an internal interface and may change without notice; do not implement it in your own code.
If you only use the Router class (the typical case), no changes are required.
OPTIONS Callable Changes
The callable returned by OptionsHandlerInterface::getOptionsCallable() now receives (ServerRequestInterface $request, array $vars) when invoked. Previously, the JsonStrategy implementation ignored these parameters.
If you have a custom strategy implementing OptionsHandlerInterface, update the closure returned by getOptionsCallable() to accept these parameters:
<?php declare(strict_types=1);
public function getOptionsCallable(array $methods): callable
{
return function (ServerRequestInterface $request, array $vars) use ($methods): ResponseInterface {
// $request is now available for CORS handling
$origin = $request->getHeaderLine('Origin');
// ...
};
}
The OptionsHandlerInterface method signature itself has not changed. Only the contract of the returned callable has been tightened.
Cached Router
The cached router has been completely redesigned. It now caches only the compiled FastRoute data (plain PHP arrays) rather than serialising the entire Router object. This means:
laravel/serializable-closureis no longer required as a hard dependency. If you relied on it being pulled in transitively, add it to your owncomposer.json.- Existing cache files from version 6.x are not compatible with the new format. Delete any existing cache files before deploying.
- The
FileCache::getMultiple(),setMultiple(), anddeleteMultiple()methods now throwBadMethodCallExceptioninstead of silently returning incorrect values.
setVars Changes
Route::setVars() now sets declaration-time default variables only. Previously it was also used internally by the dispatcher to set path-matched variables, which could cause unexpected behaviour when default vars were overwritten.
If you called setVars() at dispatch time in a custom strategy or extension, use setPathVars() instead for dispatcher-set path parameters.
Route::getVars() continues to return the merged result of default vars and path vars, with path vars taking precedence. No changes are required if you only used getVars() in controllers or middleware.
Removed Features
PHP 8.1 and 8.2 support dropped
PHP 8.1 and 8.2 are no longer supported.
laravel/serializable-closure removed from hard dependencies
The laravel/serializable-closure package has been moved from require to suggest in composer.json. It is no longer installed automatically. If your application serialises closures for caching purposes outside of Route, add the package to your own dependencies:
composer require laravel/serializable-closure
Closure wrapping removed from Route constructor
The Route constructor no longer automatically wraps closure handlers. If you construct Route objects directly (rather than via $router->map()) and pass a closure as the handler, verify the closure is a valid callable without relying on automatic wrapping.
Request-dependent route compilation removed
Routes are now compiled unconditionally, with condition matching (host, scheme, port) performed at dispatch time. This is an internal change and should not affect most applications. If you have a custom dispatcher or strategy that relied on the previous request-dependent filtering in prepareRoutes(), review your implementation against the new Dispatcher source.