diff --git a/src/RouteCollector.php b/src/RouteCollector.php index ff61838..845f1e9 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -4,29 +4,9 @@ namespace FastRoute; use function array_key_exists; -use function array_reverse; -use function is_string; -/** - * @phpstan-import-type ProcessedData from ConfigureRoutes - * @phpstan-import-type ExtraParameters from DataGenerator - * @phpstan-import-type RoutesForUriGeneration from GenerateUri - * @phpstan-import-type ParsedRoutes from RouteParser - * @final - */ -class RouteCollector implements ConfigureRoutes +final class RouteCollector extends RouteCollectorAbstract { - protected string $currentGroupPrefix = ''; - - /** @var RoutesForUriGeneration */ - private array $namedRoutes = []; - - public function __construct( - protected readonly RouteParser $routeParser, - protected readonly DataGenerator $dataGenerator, - ) { - } - /** @inheritDoc */ public function addRoute(string|array $httpMethod, string $route, mixed $handler, array $extraParameters = []): static { @@ -48,20 +28,7 @@ public function addRoute(string|array $httpMethod, string $route, mixed $handler return $this; } - /** @param ParsedRoutes $parsedRoutes */ - private function registerNamedRoute(mixed $name, array $parsedRoutes): void - { - if (! is_string($name) || $name === '') { - throw BadRouteException::invalidRouteName($name); - } - - if (array_key_exists($name, $this->namedRoutes)) { - throw BadRouteException::namedRouteAlreadyDefined($name); - } - - $this->namedRoutes[$name] = array_reverse($parsedRoutes); - } - + /** @inheritDoc */ public function addGroup(string $prefix, callable $callback): static { $previousGroupPrefix = $this->currentGroupPrefix; @@ -71,73 +38,4 @@ public function addGroup(string $prefix, callable $callback): static return $this; } - - /** @inheritDoc */ - public function any(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('*', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function get(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('GET', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function post(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('POST', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function put(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('PUT', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function delete(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('DELETE', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function patch(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('PATCH', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function head(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('HEAD', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function options(string $route, mixed $handler, array $extraParameters = []): static - { - return $this->addRoute('OPTIONS', $route, $handler, $extraParameters); - } - - /** @inheritDoc */ - public function processedRoutes(): array - { - $data = $this->dataGenerator->getData(); - $data[] = $this->namedRoutes; - - return $data; - } - - /** - * @deprecated - * - * @see ConfigureRoutes::processedRoutes() - * - * @return ProcessedData - */ - public function getData(): array - { - return $this->processedRoutes(); - } } diff --git a/src/RouteCollectorAbstract.php b/src/RouteCollectorAbstract.php new file mode 100644 index 0000000..66fc981 --- /dev/null +++ b/src/RouteCollectorAbstract.php @@ -0,0 +1,111 @@ +namedRoutes)) { + throw BadRouteException::namedRouteAlreadyDefined($name); + } + + $this->namedRoutes[$name] = array_reverse($parsedRoutes); + } + + /** @inheritDoc */ + public function processedRoutes(): array + { + $data = $this->dataGenerator->getData(); + $data[] = $this->namedRoutes; + + return $data; + } + + /** + * @deprecated + * + * @see ConfigureRoutes::processedRoutes() + * + * @return ProcessedData + */ + public function getData(): array + { + return $this->processedRoutes(); + } + + /** @inheritDoc */ + public function any(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('*', $route, $handler, $extraParameters); + } + + /** @inheritDoc */ + public function get(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('GET', $route, $handler, $extraParameters); + } + + /** @inheritDoc */ + public function post(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('POST', $route, $handler, $extraParameters); + } + + /** @inheritDoc */ + public function put(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('PUT', $route, $handler, $extraParameters); + } + + /** @inheritDoc */ + public function delete(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('DELETE', $route, $handler, $extraParameters); + } + + /** @inheritDoc */ + public function patch(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('PATCH', $route, $handler, $extraParameters); + } + + /** @inheritDoc */ + public function head(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('HEAD', $route, $handler, $extraParameters); + } + + /** @inheritDoc */ + public function options(string $route, mixed $handler, array $extraParameters = []): static + { + return $this->addRoute('OPTIONS', $route, $handler, $extraParameters); + } +} diff --git a/src/RouteCollectorImmutable.php b/src/RouteCollectorImmutable.php new file mode 100644 index 0000000..55ff1f9 --- /dev/null +++ b/src/RouteCollectorImmutable.php @@ -0,0 +1,46 @@ +dataGenerator = clone $clone->dataGenerator; + + $route = $clone->currentGroupPrefix . $route; + $parsedRoutes = $clone->routeParser->parse($route); + + $extraParameters = [self::ROUTE_REGEX => $route] + $extraParameters; + + foreach ((array) $httpMethod as $method) { + foreach ($parsedRoutes as $parsedRoute) { + $clone->dataGenerator->addRoute($method, $parsedRoute, $handler, $extraParameters); + } + } + + if (array_key_exists(self::ROUTE_NAME, $extraParameters)) { + $clone->registerNamedRoute($extraParameters[self::ROUTE_NAME], $parsedRoutes); + } + + return $clone; + } + + /** @inheritDoc */ + public function addGroup(string $prefix, callable $callback): static + { + $clone = clone $this; + + $previousGroupPrefix = $clone->currentGroupPrefix; + $clone->currentGroupPrefix = $previousGroupPrefix . $prefix; + $clone = $callback($clone); + $clone->currentGroupPrefix = $previousGroupPrefix; + + return $clone; + } +} diff --git a/test/RouteCollectorImmutableTest.php b/test/RouteCollectorImmutableTest.php new file mode 100644 index 0000000..f45efcf --- /dev/null +++ b/test/RouteCollectorImmutableTest.php @@ -0,0 +1,190 @@ +any('/any', 'any') + ->delete('/delete', 'delete') + ->get('/get', 'get') + ->head('/head', 'head') + ->patch('/patch', 'patch') + ->post('/post', 'post') + ->put('/put', 'put') + ->options('/options', 'options'); + + $expected = [ + ['*', '/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, $immutable->processedRoutes()[0]); + } + + #[PHPUnit\Test] + public function routesCanBeGrouped(): void + { + $r = self::routeCollector(); + + $immutable = $r + ->delete('/delete', 'delete') + ->get('/get', 'get') + ->head('/head', 'head') + ->patch('/patch', 'patch') + ->post('/post', 'post') + ->put('/put', 'put') + ->options('/options', 'options'); + + $immutable = $immutable->addGroup('/group-one', static function (ConfigureRoutes $r1): ConfigureRoutes { + $immutable1 = $r1 + ->delete('/delete', 'delete') + ->get('/get', 'get') + ->head('/head', 'head') + ->patch('/patch', 'patch') + ->post('/post', 'post') + ->put('/put', 'put') + ->options('/options', 'options'); + + return $immutable1->addGroup('/group-two', static function (ConfigureRoutes $r2): ConfigureRoutes { + return $r2 + ->delete('/delete', 'delete') + ->get('/get', 'get') + ->head('/head', 'head') + ->patch('/patch', 'patch') + ->post('/post', 'post') + ->put('/put', 'put') + ->options('/options', 'options'); + }); + }); + + $immutable = $immutable->addGroup('/admin', static function (ConfigureRoutes $r): ConfigureRoutes { + return $r->get('-some-info', 'admin-some-info'); + }); + + $immutable = $immutable->addGroup('/admin-', static function (ConfigureRoutes $r): ConfigureRoutes { + return $r->get('more-info', 'admin-more-info'); + }); + + $expected = [ + ['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, $immutable->processedRoutes()[0]); + } + + #[PHPUnit\Test] + public function namedRoutesShouldBeRegistered(): void + { + $r = self::routeCollector(); + + $immutable = $r->get('/', 'index-handler', ['_name' => 'index']); + $immutable = $immutable->get('/users/me', 'fetch-user-handler', ['_name' => 'users.fetch']); + + self::assertSame(['index' => [['/']], 'users.fetch' => [['/users/me']]], $immutable->processedRoutes()[2]); + } + + #[PHPUnit\Test] + public function cannotDefineRouteWithEmptyName(): void + { + $r = self::routeCollector(); + + $this->expectException(BadRouteException::class); + + $r->get('/', 'index-handler', ['_name' => '']); + } + + #[PHPUnit\Test] + public function cannotDefineRouteWithInvalidTypeAsName(): void + { + $r = self::routeCollector(); + + $this->expectException(BadRouteException::class); + + $r->get('/', 'index-handler', ['_name' => false]); + } + + #[PHPUnit\Test] + public function cannotDefineDuplicatedRouteName(): void + { + $r = self::routeCollector(); + + $this->expectException(BadRouteException::class); + + $immutable = $r->get('/', 'index-handler', ['_name' => 'index']); + $immutable->get('/users/me', 'fetch-user-handler', ['_name' => 'index']); + } + + private static function routeCollector(): ConfigureRoutes + { + return new RouteCollectorImmutable(new Std(), self::dummyDataGenerator()); + } + + private static function dummyDataGenerator(): DataGenerator + { + return new class implements DataGenerator + { + /** @var list}> */ + private array $routes = []; + + /** @inheritDoc */ + public function getData(): array + { + return [$this->routes, []]; + } + + /** @inheritDoc */ + public function addRoute(string $httpMethod, array $routeData, mixed $handler, array $extraParameters = []): void + { + TestCase::assertTrue(count($routeData) === 1 && is_string($routeData[0])); + + $this->routes[] = [$httpMethod, $routeData[0], $handler, $extraParameters]; + } + }; + } +}