diff --git a/src/Http/Router.php b/src/Http/Router.php new file mode 100644 index 0000000..2d0b54c --- /dev/null +++ b/src/Http/Router.php @@ -0,0 +1,47 @@ +): 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, '

Not found

'); + } +} diff --git a/tests/Unit/Http/RouterTest.php b/tests/Unit/Http/RouterTest.php new file mode 100644 index 0000000..68008af --- /dev/null +++ b/tests/Unit/Http/RouterTest.php @@ -0,0 +1,83 @@ +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: '', + ); + } +}