From e8a72a996ac08565b8ad9dda051818a5df6b99e3 Mon Sep 17 00:00:00 2001 From: Kevin Traini Date: Thu, 19 Dec 2024 20:57:14 +0100 Subject: [PATCH] feat: Add default HEAD & OPTIONS routes Closes #80 For each existing routes, when there is no handler defined for methods HEAD & OPTIONS: - HEAD -> 200 with empty body - OPTIONS -> 200 with header Allow which contains all methods allowed + HEAD and OPTIONS --- include/Route/RouteCollection.php | 9 ++++ include/Route/RouteInformation.php | 42 +++++++++++++++++ tests/unit/Route/RouteInformationTest.php | 55 +++++++++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 tests/unit/Route/RouteInformationTest.php diff --git a/include/Route/RouteCollection.php b/include/Route/RouteCollection.php index f0e2ce4..c5338be 100644 --- a/include/Route/RouteCollection.php +++ b/include/Route/RouteCollection.php @@ -83,6 +83,7 @@ public function addRoute(Method $method, string $route, RequestHandler|callable public function getMatchingRoute(string $uri, string $method): RouteInformation { $have_found_route_but_method = false; + $matching_methods = []; foreach ($this->routes as $route => $informations) { if (preg_match($route, $uri)) { $have_found_route_but_method = true; @@ -90,11 +91,19 @@ public function getMatchingRoute(string $uri, string $method): RouteInformation if ($route_information->method === Method::ALL || $route_information->method->value === uppercase($method)) { return $route_information; } + + $matching_methods[] = $route_information->method; } } } if ($have_found_route_but_method) { + if ($method === Method::HEAD->value) { + return RouteInformation::buildDefaultHEAD($uri); + } else if ($method === Method::OPTIONS->value) { + return RouteInformation::buildDefaultOPTIONS($uri, $matching_methods); + } + throw new MethodNotAllowedException($method, $uri); } else { throw new NotFoundException($uri); diff --git a/include/Route/RouteInformation.php b/include/Route/RouteInformation.php index b10d615..decf5f4 100644 --- a/include/Route/RouteInformation.php +++ b/include/Route/RouteInformation.php @@ -29,6 +29,9 @@ use Archict\Router\Method; use Archict\Router\RequestHandler; +use Archict\Router\ResponseFactory; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; /** * @internal @@ -45,4 +48,43 @@ public function __construct( public RequestHandler $handler, ) { } + + public static function buildDefaultHEAD(string $uri): self + { + return new self( + Method::HEAD, + $uri, + '//', + new class implements RequestHandler { + public function handle(ServerRequestInterface $request): string // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + { + return ''; + } + }, + ); + } + + /** + * @param Method[] $methods + */ + public static function buildDefaultOPTIONS(string $uri, array $methods): self + { + $header_allow = implode(', ', array_unique(array_map(static fn(Method $method) => $method->value, [...$methods, Method::OPTIONS, Method::HEAD]))); + + return new self( + Method::OPTIONS, + $uri, + '//', + new class($header_allow) implements RequestHandler { + public function __construct(private readonly string $header_allow) + { + } + + public function handle(ServerRequestInterface $request): ResponseInterface // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter + { + return ResponseFactory::build()->withHeader('Allow', $this->header_allow)->get(); + } + }, + ); + } } diff --git a/tests/unit/Route/RouteInformationTest.php b/tests/unit/Route/RouteInformationTest.php new file mode 100644 index 0000000..2fc8cc7 --- /dev/null +++ b/tests/unit/Route/RouteInformationTest.php @@ -0,0 +1,55 @@ +method); + self::assertSame('/test', $info->route); + self::assertSame('', $info->handler->handle(new ServerRequest('HEAD', '/test'))); + } + + public function testItBuildsADefaultHandlerForOPTIONS(): void + { + $info = RouteInformation::buildDefaultOPTIONS('/test', [Method::GET, Method::POST]); + self::assertSame(Method::OPTIONS, $info->method); + self::assertSame('/test', $info->route); + $response = $info->handler->handle(new ServerRequest('OPTIONS', '/test')); + self::assertInstanceOf(ResponseInterface::class, $response); + self::assertSame('', $response->getBody()->getContents()); + self::assertSame('GET, POST, OPTIONS, HEAD', $response->getHeaderLine('Allow')); + } +}