Skip to content

Commit

Permalink
feat: Add default HEAD & OPTIONS routes (#81)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Gashmob authored Dec 19, 2024
2 parents bd792f2 + e8a72a9 commit cffbd7d
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 0 deletions.
9 changes: 9 additions & 0 deletions include/Route/RouteCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,27 @@ 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;
foreach ($informations as $route_information) {
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);
Expand Down
42 changes: 42 additions & 0 deletions include/Route/RouteInformation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
},
);
}
}
55 changes: 55 additions & 0 deletions tests/unit/Route/RouteInformationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php
/**
* MIT License
*
* Copyright (c) 2024-Present Kevin Traini
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

declare(strict_types=1);

namespace Archict\Router\Route;

use Archict\Router\Method;
use GuzzleHttp\Psr7\ServerRequest;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ResponseInterface;

final class RouteInformationTest extends TestCase
{
public function testItBuildsADefaultHandlerForHEAD(): void
{
$info = RouteInformation::buildDefaultHEAD('/test');
self::assertSame(Method::HEAD, $info->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'));
}
}

0 comments on commit cffbd7d

Please sign in to comment.