From cd5e3cab4b204563aceb9f300efee4976edf3924 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Tue, 14 Jul 2026 08:31:23 +0100 Subject: [PATCH 1/3] test: cover named-route ordering regression from #362 The merged regression test only asserted getRoutes() ordering and never exercised the dispatch path, where the fatal BadRouteException was actually thrown. It also used non-conflicting static paths, so it could not reproduce the shadowing. Add a match-level test registering a named static route followed by an unnamed variable route, which fails with BadRouteException on the pre-fix code and passes on the fix. Also record the fix under a new Fixed section in the changelog (#362, #363). --- CHANGELOG.md | 3 +++ tests/RouterTest.php | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b054c54..0768960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - `Dispatcher::setRouteMap()` removed (route map now set via constructor). - Duplicate `Router::processGroups()` method removed (consolidated into `collectGroupRoutes()`). +### Fixed +- Naming a route no longer alters route registration order, which could cause a `FastRoute\BadRouteException` when a named static route was shadowed by a later variable route (#362, #363). + ## [6.2.0] 2024-11 ### Changed diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 2a1bab9..006a72b 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -151,6 +151,26 @@ expect($routes[2]->getPath())->toBe('/baz'); }); +test('naming a static route does not let a later variable route shadow it', function () { + /** @var UriInterface&MockInterface $uri */ + $uri = Mockery::mock(UriInterface::class); + $uri->allows('getPath')->andReturn('/some-path'); + + /** @var ServerRequestInterface&MockInterface $request */ + $request = Mockery::mock(ServerRequestInterface::class); + $request->allows('getMethod')->andReturn('GET'); + $request->allows('getUri')->andReturn($uri); + + $router = new Router(); + $router->map('GET', '/some-path', static function () {})->setName('someName'); + $router->map('GET', '/{path}', static function () {}); + + $result = $router->match($request); + + expect($result->isFound())->toBeTrue(); + expect($result->getStatus())->toBe(MatchStatus::Found); +}); + test('getRoutes includes routes registered inside a group', function () { $router = new Router(); $router->get('/top', static function () {}); From fe522a3d2103a24e7544a0410a3c9ca5d6b2c07b Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Tue, 14 Jul 2026 08:52:22 +0100 Subject: [PATCH 2/3] feat: add query() helper for the HTTP QUERY method QUERY (draft-ietf-httpbis-safe-method-w-body) is a safe, idempotent method like GET that permits a request body. Expose it as a standalone verb helper alongside the existing verbs; it delegates through map() like every other verb, so no dispatcher or allowlist change is needed. --- src/Http/Request.php | 1 + src/RouteCollectionInterface.php | 3 +++ src/RouteCollectionTrait.php | 6 ++++++ tests/RouterTest.php | 11 ++++++++++- 4 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Http/Request.php b/src/Http/Request.php index 00b22e7..547aa72 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -13,4 +13,5 @@ final class Request public const string METHOD_DELETE = 'DELETE'; public const string METHOD_HEAD = 'HEAD'; public const string METHOD_OPTIONS = 'OPTIONS'; + public const string METHOD_QUERY = 'QUERY'; } diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index 8570353..5c13908 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -38,4 +38,7 @@ public function post(string $path, callable|array|string|RequestHandlerInterface /** @param callable|array|string|RequestHandlerInterface $handler */ public function put(string $path, callable|array|string|RequestHandlerInterface $handler): Route; + + /** @param callable|array|string|RequestHandlerInterface $handler */ + public function query(string $path, callable|array|string|RequestHandlerInterface $handler): Route; } diff --git a/src/RouteCollectionTrait.php b/src/RouteCollectionTrait.php index 68a6c70..0aecdd1 100644 --- a/src/RouteCollectionTrait.php +++ b/src/RouteCollectionTrait.php @@ -56,4 +56,10 @@ public function put(string $path, callable|array|string|RequestHandlerInterface { return $this->map(Request::METHOD_PUT, $path, $handler); } + + /** @param callable|array|string|RequestHandlerInterface $handler */ + public function query(string $path, callable|array|string|RequestHandlerInterface $handler): Route + { + return $this->map(Request::METHOD_QUERY, $path, $handler); + } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 006a72b..cf85343 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use League\Route\MatchStatus; +use League\Route\Route; use League\Route\Router; use League\Route\Strategy\ApplicationStrategy; use Mockery\MockInterface; @@ -14,7 +15,7 @@ $path = '/something'; $callable = function () {}; - foreach (['get', 'post', 'put', 'patch', 'delete', 'head', 'options'] as $method) { + foreach (['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'query'] as $method) { $route = $router->map($method, $path, $callable); expect($route->getMethod())->toBe($method); expect($route->getPath())->toBe($path); @@ -22,6 +23,14 @@ } }); +test('query verb helper returns route with QUERY method', function () { + $router = new Router(); + $route = $router->query('/path', fn() => null); + + expect($route)->toBeInstanceOf(Route::class); + expect($route->getMethod())->toBe('QUERY'); +}); + test('router maps and returns a route group with correct prefix', function () { $router = new Router(); $prefix = '/something'; From 4ff69de3d3ec8f3df4b672aa0c8a0ddf8df0bda6 Mon Sep 17 00:00:00 2001 From: Phil Bennett Date: Tue, 14 Jul 2026 08:54:54 +0100 Subject: [PATCH 3/3] docs: document query() helper for the HTTP QUERY method Add the query verb to the Request Verbs sample and a CHANGELOG entry citing the IETF draft the method originates from. --- CHANGELOG.md | 1 + docs/unstable/routes.md | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0768960..d866b46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p - Route freezing: routes become immutable after `prepareRoutes()`, preventing silent post-compilation misconfiguration. - `FreezeableInterface`, `FreezeableTrait`, and `FreezeGuard` for route immutability lifecycle. - Optional segment support in `generateUrl()`: routes using FastRoute `[/{param}]` syntax resolve correctly with defaults from `setVars()` or omit the segment when unset. +- `query()` route helper on `Router` and `RouteGroup` (via `RouteCollectionInterface`) for the HTTP QUERY method (draft-ietf-httpbis-safe-method-w-body). ### Changed - Minimum PHP version raised to 8.3. diff --git a/docs/unstable/routes.md b/docs/unstable/routes.md index 4a6ce7a..d1a6e3e 100644 --- a/docs/unstable/routes.md +++ b/docs/unstable/routes.md @@ -26,10 +26,13 @@ $router->patch('/acme/route', 'Acme\Controller::patchMethod'); $router->delete('/acme/route', 'Acme\Controller::deleteMethod'); $router->head('/acme/route', 'Acme\Controller::headMethod'); $router->options('/acme/route', 'Acme\Controller::optionsMethod'); +$router->query('/acme/route', 'Acme\Controller::queryMethod'); ~~~ Each of the above routes will respond to the same URI but will invoke a different callable based on the HTTP request method. +The `query` verb registers a route for the HTTP QUERY method, a safe, idempotent method that, unlike GET, permits a request body. + ## Route Conditions There are times when you may wish to add further conditions to route matching other than the request verb and URI. Route allows this by chaining further conditions to the route definition.