From 240543d79cddae943aefc94c637cfbc8d701327f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Cobucci?= Date: Mon, 8 Jan 2024 22:56:23 +0100 Subject: [PATCH] Implement extra parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This introduces the possibility of providing extra parameters during route registration (such as route name) and by default registers the unparsed route an item on the list. Signed-off-by: Luís Cobucci --- src/DataGenerator.php | 8 +-- src/DataGenerator/CharCountBased.php | 2 +- src/DataGenerator/GroupCountBased.php | 2 +- src/DataGenerator/GroupPosBased.php | 2 +- src/DataGenerator/MarkBased.php | 2 +- src/DataGenerator/RegexBasedAbstract.php | 25 +++++---- src/Dispatcher/CharCountBased.php | 3 +- src/Dispatcher/GroupCountBased.php | 3 +- src/Dispatcher/GroupPosBased.php | 3 +- src/Dispatcher/MarkBased.php | 3 +- src/Dispatcher/RegexBasedAbstract.php | 9 ++-- src/Dispatcher/Result/Matched.php | 12 ++++- src/Route.php | 23 +++++--- src/RouteCollector.php | 61 ++++++++++++++------- test/Dispatcher/DispatcherTestCase.php | 68 +++++++++++++----------- test/DummyRouteCollector.php | 4 +- test/RouteCollectorTest.php | 62 ++++++++++----------- 17 files changed, 178 insertions(+), 114 deletions(-) diff --git a/src/DataGenerator.php b/src/DataGenerator.php index f3e8d266..c551c244 100644 --- a/src/DataGenerator.php +++ b/src/DataGenerator.php @@ -4,8 +4,9 @@ namespace FastRoute; /** - * @phpstan-type StaticRoutes array> - * @phpstan-type DynamicRouteChunk array{regex: string, suffix?: string, routeMap: array}>} + * @phpstan-type ExtraParameters array + * @phpstan-type StaticRoutes array> + * @phpstan-type DynamicRouteChunk array{regex: string, suffix?: string, routeMap: array, ExtraParameters}>} * @phpstan-type DynamicRouteChunks list * @phpstan-type DynamicRoutes array * @phpstan-type RouteData array{StaticRoutes, DynamicRoutes} @@ -21,8 +22,9 @@ interface DataGenerator * matches. * * @param array $routeData + * @param ExtraParameters $extraParameters */ - public function addRoute(string $httpMethod, array $routeData, mixed $handler): void; + public function addRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters = []): void; /** * Returns dispatcher data in some unspecified format, which diff --git a/src/DataGenerator/CharCountBased.php b/src/DataGenerator/CharCountBased.php index cc342fa0..d14a1ee2 100644 --- a/src/DataGenerator/CharCountBased.php +++ b/src/DataGenerator/CharCountBased.php @@ -27,7 +27,7 @@ protected function processChunk(array $regexToRoutesMap): array $suffix .= "\t"; $regexes[] = '(?:' . $regex . '/(\t{' . $suffixLen . '})\t{' . ($count - $suffixLen) . '})'; - $routeMap[$suffix] = [$route->handler, $route->variables]; + $routeMap[$suffix] = [$route->handler, $route->variables, $route->extraParameters]; } $regex = '~^(?|' . implode('|', $regexes) . ')$~'; diff --git a/src/DataGenerator/GroupCountBased.php b/src/DataGenerator/GroupCountBased.php index 0641a26c..f658c3bd 100644 --- a/src/DataGenerator/GroupCountBased.php +++ b/src/DataGenerator/GroupCountBased.php @@ -26,7 +26,7 @@ protected function processChunk(array $regexToRoutesMap): array $numGroups = max($numGroups, $numVariables); $regexes[] = $regex . str_repeat('()', $numGroups - $numVariables); - $routeMap[$numGroups + 1] = [$route->handler, $route->variables]; + $routeMap[$numGroups + 1] = [$route->handler, $route->variables, $route->extraParameters]; ++$numGroups; } diff --git a/src/DataGenerator/GroupPosBased.php b/src/DataGenerator/GroupPosBased.php index aaa3f885..1dad5d6e 100644 --- a/src/DataGenerator/GroupPosBased.php +++ b/src/DataGenerator/GroupPosBased.php @@ -21,7 +21,7 @@ protected function processChunk(array $regexToRoutesMap): array $offset = 1; foreach ($regexToRoutesMap as $regex => $route) { $regexes[] = $regex; - $routeMap[$offset] = [$route->handler, $route->variables]; + $routeMap[$offset] = [$route->handler, $route->variables, $route->extraParameters]; $offset += count($route->variables); } diff --git a/src/DataGenerator/MarkBased.php b/src/DataGenerator/MarkBased.php index b060f287..6fbd186c 100644 --- a/src/DataGenerator/MarkBased.php +++ b/src/DataGenerator/MarkBased.php @@ -21,7 +21,7 @@ protected function processChunk(array $regexToRoutesMap): array foreach ($regexToRoutesMap as $regex => $route) { $regexes[] = $regex . '(*MARK:' . $markName . ')'; - $routeMap[$markName] = [$route->handler, $route->variables]; + $routeMap[$markName] = [$route->handler, $route->variables, $route->extraParameters]; ++$markName; } diff --git a/src/DataGenerator/RegexBasedAbstract.php b/src/DataGenerator/RegexBasedAbstract.php index dbf0c3ab..b094e6e4 100644 --- a/src/DataGenerator/RegexBasedAbstract.php +++ b/src/DataGenerator/RegexBasedAbstract.php @@ -21,6 +21,7 @@ * @phpstan-import-type DynamicRouteChunk from DataGenerator * @phpstan-import-type DynamicRoutes from DataGenerator * @phpstan-import-type RouteData from DataGenerator + * @phpstan-import-type ExtraParameters from DataGenerator */ abstract class RegexBasedAbstract implements DataGenerator { @@ -40,12 +41,12 @@ abstract protected function getApproxChunkSize(): int; abstract protected function processChunk(array $regexToRoutesMap): array; /** @inheritDoc */ - public function addRoute(string $httpMethod, array $routeData, mixed $handler): void + public function addRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters = []): void { if ($this->isStaticRoute($routeData)) { - $this->addStaticRoute($httpMethod, $routeData, $handler); + $this->addStaticRoute($httpMethod, $routeData, $handler, $extraParameters); } else { - $this->addVariableRoute($httpMethod, $routeData, $handler); + $this->addVariableRoute($httpMethod, $routeData, $handler, $extraParameters); } } @@ -88,8 +89,11 @@ private function isStaticRoute(array $routeData): bool return count($routeData) === 1 && is_string($routeData[0]); } - /** @param array $routeData */ - private function addStaticRoute(string $httpMethod, array $routeData, mixed $handler): void + /** + * @param array $routeData + * @param ExtraParameters $extraParameters + */ + private function addStaticRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters): void { $routeStr = $routeData[0]; assert(is_string($routeStr)); @@ -106,13 +110,16 @@ private function addStaticRoute(string $httpMethod, array $routeData, mixed $han } } - $this->staticRoutes[$httpMethod][$routeStr] = $handler; + $this->staticRoutes[$httpMethod][$routeStr] = [$handler, $extraParameters]; } - /** @param array $routeData */ - private function addVariableRoute(string $httpMethod, array $routeData, mixed $handler): void + /** + * @param array $routeData + * @param ExtraParameters $extraParameters + */ + private function addVariableRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters): void { - $route = Route::fromParsedRoute($httpMethod, $routeData, $handler); + $route = Route::fromParsedRoute($httpMethod, $routeData, $handler, $extraParameters); $regex = $route->regex; if (isset($this->methodToRegexToRoutesMap[$httpMethod][$regex])) { diff --git a/src/Dispatcher/CharCountBased.php b/src/Dispatcher/CharCountBased.php index d8949d2f..e89557f2 100644 --- a/src/Dispatcher/CharCountBased.php +++ b/src/Dispatcher/CharCountBased.php @@ -21,7 +21,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche continue; } - [$handler, $varNames] = $data['routeMap'][end($matches)]; + [$handler, $varNames, $extraParameters] = $data['routeMap'][end($matches)]; $vars = []; $i = 0; @@ -32,6 +32,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche $result = new Matched(); $result->handler = $handler; $result->variables = $vars; + $result->extraParameters = $extraParameters; return $result; } diff --git a/src/Dispatcher/GroupCountBased.php b/src/Dispatcher/GroupCountBased.php index dbae53d0..8d8bba0f 100644 --- a/src/Dispatcher/GroupCountBased.php +++ b/src/Dispatcher/GroupCountBased.php @@ -18,7 +18,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche continue; } - [$handler, $varNames] = $data['routeMap'][count($matches)]; + [$handler, $varNames, $extraParameters] = $data['routeMap'][count($matches)]; $vars = []; $i = 0; @@ -29,6 +29,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche $result = new Matched(); $result->handler = $handler; $result->variables = $vars; + $result->extraParameters = $extraParameters; return $result; } diff --git a/src/Dispatcher/GroupPosBased.php b/src/Dispatcher/GroupPosBased.php index 3476533c..4e2e8147 100644 --- a/src/Dispatcher/GroupPosBased.php +++ b/src/Dispatcher/GroupPosBased.php @@ -25,7 +25,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche assert(isset($i)); - [$handler, $varNames] = $data['routeMap'][$i]; + [$handler, $varNames, $extraParameters] = $data['routeMap'][$i]; $vars = []; foreach ($varNames as $varName) { @@ -35,6 +35,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche $result = new Matched(); $result->handler = $handler; $result->variables = $vars; + $result->extraParameters = $extraParameters; return $result; } diff --git a/src/Dispatcher/MarkBased.php b/src/Dispatcher/MarkBased.php index ffbd1702..0e676600 100644 --- a/src/Dispatcher/MarkBased.php +++ b/src/Dispatcher/MarkBased.php @@ -17,7 +17,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche continue; } - [$handler, $varNames] = $data['routeMap'][$matches['MARK']]; + [$handler, $varNames, $extraParameters] = $data['routeMap'][$matches['MARK']]; $vars = []; $i = 0; @@ -28,6 +28,7 @@ protected function dispatchVariableRoute(array $routeData, string $uri): ?Matche $result = new Matched(); $result->handler = $handler; $result->variables = $vars; + $result->extraParameters = $extraParameters; return $result; } diff --git a/src/Dispatcher/RegexBasedAbstract.php b/src/Dispatcher/RegexBasedAbstract.php index e2befb13..889eb885 100644 --- a/src/Dispatcher/RegexBasedAbstract.php +++ b/src/Dispatcher/RegexBasedAbstract.php @@ -37,7 +37,8 @@ public function dispatch(string $httpMethod, string $uri): Matched|NotMatched|Me { if (isset($this->staticRouteMap[$httpMethod][$uri])) { $result = new Matched(); - $result->handler = $this->staticRouteMap[$httpMethod][$uri]; + $result->handler = $this->staticRouteMap[$httpMethod][$uri][0]; + $result->extraParameters = $this->staticRouteMap[$httpMethod][$uri][1]; return $result; } @@ -53,7 +54,8 @@ public function dispatch(string $httpMethod, string $uri): Matched|NotMatched|Me if ($httpMethod === 'HEAD') { if (isset($this->staticRouteMap['GET'][$uri])) { $result = new Matched(); - $result->handler = $this->staticRouteMap['GET'][$uri]; + $result->handler = $this->staticRouteMap['GET'][$uri][0]; + $result->extraParameters = $this->staticRouteMap['GET'][$uri][1]; return $result; } @@ -69,7 +71,8 @@ public function dispatch(string $httpMethod, string $uri): Matched|NotMatched|Me // If nothing else matches, try fallback routes if (isset($this->staticRouteMap['*'][$uri])) { $result = new Matched(); - $result->handler = $this->staticRouteMap['*'][$uri]; + $result->handler = $this->staticRouteMap['*'][$uri][0]; + $result->extraParameters = $this->staticRouteMap['*'][$uri][1]; return $result; } diff --git a/src/Dispatcher/Result/Matched.php b/src/Dispatcher/Result/Matched.php index 475294bc..7fa4bf45 100644 --- a/src/Dispatcher/Result/Matched.php +++ b/src/Dispatcher/Result/Matched.php @@ -4,11 +4,15 @@ namespace FastRoute\Dispatcher\Result; use ArrayAccess; +use FastRoute\DataGenerator; use FastRoute\Dispatcher; use OutOfBoundsException; use RuntimeException; -/** @implements ArrayAccess> */ +/** + * @phpstan-import-type ExtraParameters from DataGenerator + * @implements ArrayAccess> + */ final class Matched implements ArrayAccess { /** @readonly */ @@ -20,6 +24,12 @@ final class Matched implements ArrayAccess */ public array $variables = []; + /** + * @readonly + * @var ExtraParameters + */ + public array $extraParameters = []; + public function offsetExists(mixed $offset): bool { return $offset >= 0 && $offset <= 2; diff --git a/src/Route.php b/src/Route.php index 8e721985..36c70011 100644 --- a/src/Route.php +++ b/src/Route.php @@ -7,19 +7,27 @@ use function preg_match; use function preg_quote; +/** @phpstan-import-type ExtraParameters from DataGenerator */ class Route { - /** @param array $variables */ + /** + * @param array $variables + * @param ExtraParameters $extraParameters + */ public function __construct( - public string $httpMethod, - public mixed $handler, - public string $regex, - public array $variables, + public readonly string $httpMethod, + public readonly mixed $handler, + public readonly string $regex, + public readonly array $variables, + public readonly array $extraParameters, ) { } - /** @param array $routeData */ - public static function fromParsedRoute(string $httpMethod, array $routeData, mixed $handler): self + /** + * @param array $routeData + * @param ExtraParameters $extraParameters + */ + public static function fromParsedRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters = []): self { [$regex, $variables] = self::extractRegex($routeData); @@ -28,6 +36,7 @@ public static function fromParsedRoute(string $httpMethod, array $routeData, mix $handler, $regex, $variables, + $extraParameters, ); } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 2b80bc28..bf4b0a87 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -3,7 +3,10 @@ namespace FastRoute; -/** @phpstan-import-type RouteData from DataGenerator */ +/** + * @phpstan-import-type RouteData from DataGenerator + * @phpstan-import-type ExtraParameters from DataGenerator + */ class RouteCollector { protected string $currentGroupPrefix = ''; @@ -18,14 +21,18 @@ public function __construct(protected RouteParser $routeParser, protected DataGe * The syntax used in the $route string depends on the used route parser. * * @param string|string[] $httpMethod + * @param ExtraParameters $extraParameters */ - public function addRoute(string|array $httpMethod, string $route, mixed $handler): void + public function addRoute(string|array $httpMethod, string $route, mixed $handler, array $extraParameters = []): void { $route = $this->currentGroupPrefix . $route; $parsedRoutes = $this->routeParser->parse($route); + + $extraParameters = ['_route' => $route] + $extraParameters; + foreach ((array) $httpMethod as $method) { foreach ($parsedRoutes as $parsedRoute) { - $this->dataGenerator->addRoute($method, $parsedRoute, $handler); + $this->dataGenerator->addRoute($method, $parsedRoute, $handler, $extraParameters); } } } @@ -47,80 +54,96 @@ public function addGroup(string $prefix, callable $callback): void * Adds a fallback route to the collection * * This is simply an alias of $this->addRoute('*', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function any(string $route, mixed $handler): void + public function any(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('*', $route, $handler); + $this->addRoute('*', $route, $handler, $extraParameters); } /** * Adds a GET route to the collection * * This is simply an alias of $this->addRoute('GET', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function get(string $route, mixed $handler): void + public function get(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('GET', $route, $handler); + $this->addRoute('GET', $route, $handler, $extraParameters); } /** * Adds a POST route to the collection * * This is simply an alias of $this->addRoute('POST', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function post(string $route, mixed $handler): void + public function post(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('POST', $route, $handler); + $this->addRoute('POST', $route, $handler, $extraParameters); } /** * Adds a PUT route to the collection * * This is simply an alias of $this->addRoute('PUT', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function put(string $route, mixed $handler): void + public function put(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('PUT', $route, $handler); + $this->addRoute('PUT', $route, $handler, $extraParameters); } /** * Adds a DELETE route to the collection * * This is simply an alias of $this->addRoute('DELETE', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function delete(string $route, mixed $handler): void + public function delete(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('DELETE', $route, $handler); + $this->addRoute('DELETE', $route, $handler, $extraParameters); } /** * Adds a PATCH route to the collection * * This is simply an alias of $this->addRoute('PATCH', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function patch(string $route, mixed $handler): void + public function patch(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('PATCH', $route, $handler); + $this->addRoute('PATCH', $route, $handler, $extraParameters); } /** * Adds a HEAD route to the collection * * This is simply an alias of $this->addRoute('HEAD', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function head(string $route, mixed $handler): void + public function head(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('HEAD', $route, $handler); + $this->addRoute('HEAD', $route, $handler, $extraParameters); } /** * Adds an OPTIONS route to the collection * * This is simply an alias of $this->addRoute('OPTIONS', $route, $handler) + * + * @param ExtraParameters $extraParameters */ - public function options(string $route, mixed $handler): void + public function options(string $route, mixed $handler, array $extraParameters = []): void { - $this->addRoute('OPTIONS', $route, $handler); + $this->addRoute('OPTIONS', $route, $handler, $extraParameters); } /** diff --git a/test/Dispatcher/DispatcherTestCase.php b/test/Dispatcher/DispatcherTestCase.php index be3eb7f8..eb66c7d2 100644 --- a/test/Dispatcher/DispatcherTestCase.php +++ b/test/Dispatcher/DispatcherTestCase.php @@ -16,6 +16,7 @@ use function FastRoute\simpleDispatcher; +/** @phpstan-import-type ExtraParameters from DataGenerator */ abstract class DispatcherTestCase extends TestCase { /** @@ -45,7 +46,10 @@ private function generateDispatcherOptions(): array ]; } - /** @param array $argDict */ + /** + * @param array $argDict + * @param ExtraParameters $extraParameters + */ #[PHPUnit\Test] #[PHPUnit\DataProvider('provideFoundDispatchCases')] public function foundDispatches( @@ -54,6 +58,7 @@ public function foundDispatches( callable $callback, string $handler, array $argDict = [], + array $extraParameters = [], ): void { $dispatcher = simpleDispatcher($callback, $this->generateDispatcherOptions()); $info = $dispatcher->dispatch($method, $uri); @@ -61,6 +66,7 @@ public function foundDispatches( self::assertInstanceOf(Matched::class, $info); self::assertSame($handler, $info->handler); self::assertSame($argDict, $info->variables); + self::assertSame($extraParameters, $info->extraParameters); // BC-compatibility checks self::assertSame($dispatcher::FOUND, $info[0]); @@ -178,7 +184,7 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('GET', '/resource/123/456', 'handler0'); }; - yield 'single static route' => ['GET', '/resource/123/456', $callback, 'handler0']; + yield 'single static route' => ['GET', '/resource/123/456', $callback, 'handler0', [], ['_route' => '/resource/123/456']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/handler0', 'handler0'); @@ -186,7 +192,7 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('GET', '/handler2', 'handler2'); }; - yield 'multiple static routes' => ['GET', '/handler2', $callback, 'handler2']; + yield 'multiple static routes' => ['GET', '/handler2', $callback, 'handler2', [], ['_route' => '/handler2']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/user/{name}/{id:[0-9]+}', 'handler0'); @@ -194,10 +200,10 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('GET', '/user/{name}', 'handler2'); }; - yield 'parameter matching precedence {/user/rdlowrey/12345}' => ['GET', '/user/rdlowrey/12345', $callback, 'handler0', ['name' => 'rdlowrey', 'id' => '12345']]; - yield 'parameter matching precedence {/user/12345}' => ['GET', '/user/12345', $callback, 'handler1', ['id' => '12345']]; - yield 'parameter matching precedence {/user/rdlowrey}' => ['GET', '/user/rdlowrey', $callback, 'handler2', ['name' => 'rdlowrey']]; - yield 'parameter matching precedence {/user/NaN}' => ['GET', '/user/NaN', $callback, 'handler2', ['name' => 'NaN']]; + yield 'parameter matching precedence {/user/rdlowrey/12345}' => ['GET', '/user/rdlowrey/12345', $callback, 'handler0', ['name' => 'rdlowrey', 'id' => '12345'], ['_route' => '/user/{name}/{id:[0-9]+}']]; + yield 'parameter matching precedence {/user/12345}' => ['GET', '/user/12345', $callback, 'handler1', ['id' => '12345'], ['_route' => '/user/{id:[0-9]+}']]; + yield 'parameter matching precedence {/user/rdlowrey}' => ['GET', '/user/rdlowrey', $callback, 'handler2', ['name' => 'rdlowrey'], ['_route' => '/user/{name}']]; + yield 'parameter matching precedence {/user/NaN}' => ['GET', '/user/NaN', $callback, 'handler2', ['name' => 'NaN'], ['_route' => '/user/{name}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/user/{id:[0-9]+}', 'handler0'); @@ -205,7 +211,7 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('GET', '/user/{id:[0-9]+}.{extension}', 'handler2'); }; - yield 'dynamic file extensions' => ['GET', '/user/12345.svg', $callback, 'handler2', ['id' => '12345', 'extension' => 'svg']]; + yield 'dynamic file extensions' => ['GET', '/user/12345.svg', $callback, 'handler2', ['id' => '12345', 'extension' => 'svg'], ['_route' => '/user/{id:[0-9]+}.{extension}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/user/{name}', 'handler0'); @@ -215,17 +221,17 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('HEAD', '/static1', 'handler4'); }; - yield 'fallback to GET on HEAD route miss {/user/rdlowrey}' => ['HEAD', '/user/rdlowrey', $callback, 'handler0', ['name' => 'rdlowrey']]; - yield 'fallback to GET on HEAD route miss {/user/rdlowrey/1234}' => ['HEAD', '/user/rdlowrey/1234', $callback, 'handler1', ['name' => 'rdlowrey', 'id' => '1234']]; - yield 'fallback to GET on HEAD route miss {/static0}' => ['HEAD', '/static0', $callback, 'handler2']; - yield 'registered HEAD route is used' => ['HEAD', '/static1', $callback, 'handler4']; + yield 'fallback to GET on HEAD route miss {/user/rdlowrey}' => ['HEAD', '/user/rdlowrey', $callback, 'handler0', ['name' => 'rdlowrey'], ['_route' => '/user/{name}']]; + yield 'fallback to GET on HEAD route miss {/user/rdlowrey/1234}' => ['HEAD', '/user/rdlowrey/1234', $callback, 'handler1', ['name' => 'rdlowrey', 'id' => '1234'], ['_route' => '/user/{name}/{id:[0-9]+}']]; + yield 'fallback to GET on HEAD route miss {/static0}' => ['HEAD', '/static0', $callback, 'handler2', [], ['_route' => '/static0']]; + yield 'registered HEAD route is used' => ['HEAD', '/static1', $callback, 'handler4', [], ['_route' => '/static1']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/user/{name}', 'handler0'); $r->addRoute('POST', '/user/{name:[a-z]+}', 'handler1'); }; - yield 'more specific routes are not shadowed by less specific of another method' => ['POST', '/user/rdlowrey', $callback, 'handler1', ['name' => 'rdlowrey']]; + yield 'more specific routes are not shadowed by less specific of another method' => ['POST', '/user/rdlowrey', $callback, 'handler1', ['name' => 'rdlowrey'], ['_route' => '/user/{name:[a-z]+}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/user/{name}', 'handler0'); @@ -233,15 +239,15 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('POST', '/user/{name}', 'handler2'); }; - yield 'more specific routes are used, according to the registration order {/user/rdlowrey}' => ['POST', '/user/rdlowrey', $callback, 'handler1', ['name' => 'rdlowrey']]; - yield 'more specific routes are used, according to the registration order {/user/rdlowrey1}' => ['POST', '/user/rdlowrey1', $callback, 'handler2', ['name' => 'rdlowrey1']]; + yield 'more specific routes are used, according to the registration order {/user/rdlowrey}' => ['POST', '/user/rdlowrey', $callback, 'handler1', ['name' => 'rdlowrey'], ['_route' => '/user/{name:[a-z]+}']]; + yield 'more specific routes are used, according to the registration order {/user/rdlowrey1}' => ['POST', '/user/rdlowrey1', $callback, 'handler2', ['name' => 'rdlowrey1'], ['_route' => '/user/{name}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/user/{name}', 'handler0'); $r->addRoute('GET', '/user/{name}/edit', 'handler1'); }; - yield 'route with constant suffix' => ['GET', '/user/rdlowrey/edit', $callback, 'handler1', ['name' => 'rdlowrey']]; + yield 'route with constant suffix' => ['GET', '/user/rdlowrey/edit', $callback, 'handler1', ['name' => 'rdlowrey'], ['_route' => '/user/{name}/edit']]; $callback = static function (RouteCollector $r): void { $r->addRoute(['GET', 'POST'], '/user', 'handlerGetPost'); @@ -250,7 +256,7 @@ public static function provideFoundDispatchCases(): iterable }; foreach (['GET' => 'handlerGetPost', 'POST' => 'handlerGetPost', 'DELETE' => 'handlerDelete'] as $method => $handler) { - yield 'multiple methods with the same handler {' . $method . '}' => [$method, '/user', $callback, $handler]; + yield 'multiple methods with the same handler {' . $method . '}' => [$method, '/user', $callback, $handler, [], ['_route' => '/user']]; } $callback = static function (RouteCollector $r): void { @@ -258,34 +264,34 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('GET', '/{entity}.json', 'handler1'); }; - yield 'fallback to dynamic routes when method does not match' => ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user']]; + yield 'fallback to dynamic routes when method does not match' => ['GET', '/user.json', $callback, 'handler1', ['entity' => 'user'], ['_route' => '/{entity}.json']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '', 'handler0'); }; - yield 'match empty route' => ['GET', '', $callback, 'handler0']; + yield 'match empty route' => ['GET', '', $callback, 'handler0', [], ['_route' => '']]; $callback = static function (RouteCollector $r): void { $r->addRoute('HEAD', '/a/{foo}', 'handler0'); $r->addRoute('GET', '/b/{foo}', 'handler1'); }; - yield 'fallback to GET route on HEAD miss {dynamic routes}' => ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar']]; + yield 'fallback to GET route on HEAD miss {dynamic routes}' => ['HEAD', '/b/bar', $callback, 'handler1', ['foo' => 'bar'], ['_route' => '/b/{foo}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('HEAD', '/a', 'handler0'); $r->addRoute('GET', '/b', 'handler1'); }; - yield 'fallback to GET route on HEAD miss {static routes}' => ['HEAD', '/b', $callback, 'handler1']; + yield 'fallback to GET route on HEAD miss {static routes}' => ['HEAD', '/b', $callback, 'handler1', [], ['_route' => '/b']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/foo', 'handler0'); $r->addRoute('HEAD', '/{bar}', 'handler1'); }; - yield 'fallback to GET route on HEAD miss {dynamic/static routes}' => ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo']]; + yield 'fallback to GET route on HEAD miss {dynamic/static routes}' => ['HEAD', '/foo', $callback, 'handler1', ['bar' => 'foo'], ['_route' => '/{bar}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('*', '/user', 'handler0'); @@ -293,15 +299,15 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('GET', '/user', 'handler2'); }; - yield 'fallback method is used when needed {GET,static}' => ['GET', '/user', $callback, 'handler2']; - yield 'fallback method is used when needed {HEAD,static}' => ['HEAD', '/user', $callback, 'handler2']; + yield 'fallback method is used when needed {GET,static}' => ['GET', '/user', $callback, 'handler2', [], ['_route' => '/user']]; + yield 'fallback method is used when needed {HEAD,static}' => ['HEAD', '/user', $callback, 'handler2', [], ['_route' => '/user']]; - yield 'fallback method is used when needed {GET,dynamic}' => ['GET', '/foo', $callback, 'handler1', ['user' => 'foo']]; - yield 'fallback method is used when needed {HEAD,dynamic}' => ['HEAD', '/foo', $callback, 'handler1', ['user' => 'foo']]; + yield 'fallback method is used when needed {GET,dynamic}' => ['GET', '/foo', $callback, 'handler1', ['user' => 'foo'], ['_route' => '/{user}']]; + yield 'fallback method is used when needed {HEAD,dynamic}' => ['HEAD', '/foo', $callback, 'handler1', ['user' => 'foo'], ['_route' => '/{user}']]; foreach (['POST', 'DELETE', 'OPTIONS', 'PUT', 'PATCH'] as $method) { - yield 'fallback method is used when needed {' . $method . ',static}' => [$method, '/user', $callback, 'handler0']; - yield 'fallback method is used when needed {' . $method . ',dynamic}' => [$method, '/foo', $callback, 'handler1', ['user' => 'foo']]; + yield 'fallback method is used when needed {' . $method . ',static}' => [$method, '/user', $callback, 'handler0', [], ['_route' => '/user']]; + yield 'fallback method is used when needed {' . $method . ',dynamic}' => [$method, '/foo', $callback, 'handler1', ['user' => 'foo'], ['_route' => '/{user}']]; } $callback = static function (RouteCollector $r): void { @@ -309,20 +315,20 @@ public static function provideFoundDispatchCases(): iterable $r->addRoute('*', '/foo', 'handler1'); }; - yield 'fallback method is used as last resource' => ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo']]; + yield 'fallback method is used as last resource' => ['GET', '/foo', $callback, 'handler0', ['bar' => 'foo'], ['_route' => '/{bar}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('GET', '/user', 'handler0'); $r->addRoute('*', '/{foo:.*}', 'handler1'); }; - yield 'fallback method can capture arguments' => ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar']]; + yield 'fallback method can capture arguments' => ['POST', '/bar', $callback, 'handler1', ['foo' => 'bar'], ['_route' => '/{foo:.*}']]; $callback = static function (RouteCollector $r): void { $r->addRoute('OPTIONS', '/about', 'handler0'); }; - yield 'options method is supported' => ['OPTIONS', '/about', $callback, 'handler0']; + yield 'options method is supported' => ['OPTIONS', '/about', $callback, 'handler0', [], ['_route' => '/about']]; } /** @return iterable */ diff --git a/test/DummyRouteCollector.php b/test/DummyRouteCollector.php index 1aaaa8e2..fdac238e 100644 --- a/test/DummyRouteCollector.php +++ b/test/DummyRouteCollector.php @@ -18,9 +18,9 @@ public function __construct() /** * {@inheritDoc} */ - public function addRoute($httpMethod, string $route, mixed $handler): void + public function addRoute(string|array $httpMethod, string $route, mixed $handler, array $extraParameters = []): void { $route = $this->currentGroupPrefix . $route; - $this->routes[] = [$httpMethod, $route, $handler]; + $this->routes[] = [$httpMethod, $route, $handler, ['_route' => $route] + $extraParameters]; } } diff --git a/test/RouteCollectorTest.php b/test/RouteCollectorTest.php index cc1dd6fd..950ad18f 100644 --- a/test/RouteCollectorTest.php +++ b/test/RouteCollectorTest.php @@ -23,14 +23,14 @@ public function shortcutsCanBeUsedToRegisterRoutes(): void $r->options('/options', 'options'); $expected = [ - ['*', '/any', 'any'], - ['DELETE', '/delete', 'delete'], - ['GET', '/get', 'get'], - ['HEAD', '/head', 'head'], - ['PATCH', '/patch', 'patch'], - ['POST', '/post', 'post'], - ['PUT', '/put', 'put'], - ['OPTIONS', '/options', 'options'], + ['*', '/any', 'any', ['_route' => '/any']], + ['DELETE', '/delete', 'delete', ['_route' => '/delete']], + ['GET', '/get', 'get', ['_route' => '/get']], + ['HEAD', '/head', 'head', ['_route' => '/head']], + ['PATCH', '/patch', 'patch', ['_route' => '/patch']], + ['POST', '/post', 'post', ['_route' => '/post']], + ['PUT', '/put', 'put', ['_route' => '/put']], + ['OPTIONS', '/options', 'options', ['_route' => '/options']], ]; self::assertSame($expected, $r->routes); @@ -77,29 +77,29 @@ public function routesCanBeGrouped(): void }); $expected = [ - ['DELETE', '/delete', 'delete'], - ['GET', '/get', 'get'], - ['HEAD', '/head', 'head'], - ['PATCH', '/patch', 'patch'], - ['POST', '/post', 'post'], - ['PUT', '/put', 'put'], - ['OPTIONS', '/options', 'options'], - ['DELETE', '/group-one/delete', 'delete'], - ['GET', '/group-one/get', 'get'], - ['HEAD', '/group-one/head', 'head'], - ['PATCH', '/group-one/patch', 'patch'], - ['POST', '/group-one/post', 'post'], - ['PUT', '/group-one/put', 'put'], - ['OPTIONS', '/group-one/options', 'options'], - ['DELETE', '/group-one/group-two/delete', 'delete'], - ['GET', '/group-one/group-two/get', 'get'], - ['HEAD', '/group-one/group-two/head', 'head'], - ['PATCH', '/group-one/group-two/patch', 'patch'], - ['POST', '/group-one/group-two/post', 'post'], - ['PUT', '/group-one/group-two/put', 'put'], - ['OPTIONS', '/group-one/group-two/options', 'options'], - ['GET', '/admin-some-info', 'admin-some-info'], - ['GET', '/admin-more-info', 'admin-more-info'], + ['DELETE', '/delete', 'delete', ['_route' => '/delete']], + ['GET', '/get', 'get', ['_route' => '/get']], + ['HEAD', '/head', 'head', ['_route' => '/head']], + ['PATCH', '/patch', 'patch', ['_route' => '/patch']], + ['POST', '/post', 'post', ['_route' => '/post']], + ['PUT', '/put', 'put', ['_route' => '/put']], + ['OPTIONS', '/options', 'options', ['_route' => '/options']], + ['DELETE', '/group-one/delete', 'delete', ['_route' => '/group-one/delete']], + ['GET', '/group-one/get', 'get', ['_route' => '/group-one/get']], + ['HEAD', '/group-one/head', 'head', ['_route' => '/group-one/head']], + ['PATCH', '/group-one/patch', 'patch', ['_route' => '/group-one/patch']], + ['POST', '/group-one/post', 'post', ['_route' => '/group-one/post']], + ['PUT', '/group-one/put', 'put', ['_route' => '/group-one/put']], + ['OPTIONS', '/group-one/options', 'options', ['_route' => '/group-one/options']], + ['DELETE', '/group-one/group-two/delete', 'delete', ['_route' => '/group-one/group-two/delete']], + ['GET', '/group-one/group-two/get', 'get', ['_route' => '/group-one/group-two/get']], + ['HEAD', '/group-one/group-two/head', 'head', ['_route' => '/group-one/group-two/head']], + ['PATCH', '/group-one/group-two/patch', 'patch', ['_route' => '/group-one/group-two/patch']], + ['POST', '/group-one/group-two/post', 'post', ['_route' => '/group-one/group-two/post']], + ['PUT', '/group-one/group-two/put', 'put', ['_route' => '/group-one/group-two/put']], + ['OPTIONS', '/group-one/group-two/options', 'options', ['_route' => '/group-one/group-two/options']], + ['GET', '/admin-some-info', 'admin-some-info', ['_route' => '/admin-some-info']], + ['GET', '/admin-more-info', 'admin-more-info', ['_route' => '/admin-more-info']], ]; self::assertSame($expected, $r->routes);