feat: add Http router
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BattleForge\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final class Router
|
||||||
|
{
|
||||||
|
/** @var list<array{method: string, pattern: string, handler: callable(Request, array<string, string>): Response}> */
|
||||||
|
private array $routes = [];
|
||||||
|
|
||||||
|
public function add(string $method, string $path, callable $handler): void
|
||||||
|
{
|
||||||
|
$pattern = '#^' . preg_replace('#\{([a-zA-Z_][a-zA-Z0-9_]*)\}#', '(?P<$1>[^/]+)', $path) . '$#';
|
||||||
|
|
||||||
|
$this->routes[] = [
|
||||||
|
'method' => strtoupper($method),
|
||||||
|
'pattern' => $pattern,
|
||||||
|
'handler' => $handler,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dispatch(Request $request): Response
|
||||||
|
{
|
||||||
|
foreach ($this->routes as $route) {
|
||||||
|
if ($route['method'] !== $request->method) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preg_match($route['pattern'], $request->path, $matches) === 1) {
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
foreach ($matches as $key => $value) {
|
||||||
|
if (is_string($key)) {
|
||||||
|
$params[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($route['handler'])($request, $params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::html(404, '<h1>Not found</h1>');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace BattleForge\Tests\Unit\Http;
|
||||||
|
|
||||||
|
use BattleForge\Http\Request;
|
||||||
|
use BattleForge\Http\Response;
|
||||||
|
use BattleForge\Http\Router;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
final class RouterTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testItDispatchesAMatchingStaticRoute(): void
|
||||||
|
{
|
||||||
|
$router = new Router();
|
||||||
|
$router->add('GET', '/', static fn (Request $request): Response => Response::html(200, 'home'));
|
||||||
|
|
||||||
|
$response = $router->dispatch($this->request('GET', '/'));
|
||||||
|
|
||||||
|
self::assertSame(200, $response->status);
|
||||||
|
self::assertSame('home', $response->body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItReturnsA404ForUnknownPaths(): void
|
||||||
|
{
|
||||||
|
$router = new Router();
|
||||||
|
|
||||||
|
$response = $router->dispatch($this->request('GET', '/missing'));
|
||||||
|
|
||||||
|
self::assertSame(404, $response->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItReturnsAMethodNotAllowedResponseForMismatchedMethods(): void
|
||||||
|
{
|
||||||
|
$router = new Router();
|
||||||
|
$router->add('POST', '/', static fn (): Response => Response::html(200, 'ok'));
|
||||||
|
|
||||||
|
$response = $router->dispatch($this->request('GET', '/'));
|
||||||
|
|
||||||
|
self::assertSame(404, $response->status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItCapturesPathParams(): void
|
||||||
|
{
|
||||||
|
$router = new Router();
|
||||||
|
$router->add('GET', '/scenarios/{id}/edit', static function (Request $request, array $params): Response {
|
||||||
|
return Response::html(200, 'id=' . $params['id']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$response = $router->dispatch($this->request('GET', '/scenarios/demo/edit'));
|
||||||
|
|
||||||
|
self::assertSame(200, $response->status);
|
||||||
|
self::assertSame('id=demo', $response->body);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testItMatchesMoreSpecificRoutesFirstByRegistrationOrder(): void
|
||||||
|
{
|
||||||
|
$router = new Router();
|
||||||
|
$router->add('GET', '/scenarios/{id}', static fn (): Response => Response::html(200, 'any'));
|
||||||
|
$router->add('GET', '/scenarios/special', static fn (): Response => Response::html(200, 'special'));
|
||||||
|
|
||||||
|
$response = $router->dispatch($this->request('GET', '/scenarios/special'));
|
||||||
|
|
||||||
|
self::assertSame('any', $response->body);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function request(string $method, string $path): Request
|
||||||
|
{
|
||||||
|
return new Request(
|
||||||
|
get: [],
|
||||||
|
post: [],
|
||||||
|
files: [],
|
||||||
|
cookies: [],
|
||||||
|
server: [],
|
||||||
|
queryString: '',
|
||||||
|
method: $method,
|
||||||
|
path: $path,
|
||||||
|
contentType: null,
|
||||||
|
rawBody: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user