From 29980d8bc61b90f55ef54c48532e8e95013e5cbf Mon Sep 17 00:00:00 2001 From: Funky Waddle Date: Fri, 13 Feb 2026 15:54:36 -0600 Subject: [PATCH] feat: implement milestone 2 - basic URI matching and methods - Implement fluent HTTP methods: get(), post(), put(), patch(), delete() - Build URI matcher for static paths with PSR-7 ServerRequestInterface support - Implement match() returning null for no match scenarios - Implement matchOrFail() throwing NotFoundRouteException for missing routes - Add path normalization (trailing slashes, leading slashes) - Create NotFoundRouteException for global 404 handling - Add comprehensive test coverage with 19 tests passing All tests passing: 19 tests, 34 assertions Closes milestone 2 from MILESTONES.md --- MILESTONES.md | 8 +- src/NotFoundRouteException.php | 7 ++ src/Router.php | 29 +++++- tests/Unit/ErrorHandlingTest.php | 161 +++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 5 deletions(-) create mode 100644 src/NotFoundRouteException.php create mode 100644 tests/Unit/ErrorHandlingTest.php diff --git a/MILESTONES.md b/MILESTONES.md index e5a5a27..f6c1e2d 100644 --- a/MILESTONES.md +++ b/MILESTONES.md @@ -12,10 +12,10 @@ This document outlines the phased development roadmap for the Atlas Routing engi ## Milestone 2: Basic URI Matching & Methods *Goal: Implement the matching engine for standard HTTP methods and static URIs.* -- [ ] Implement fluent methods: `get()`, `post()`, `put()`, `patch()`, `delete()`. -- [ ] Build the URI Matcher for static paths. -- [ ] Support for PSR-7 `ServerRequestInterface` type-hinting in the matcher. -- [ ] Implement basic Error Handling (Global 404). +- [x] Implement fluent methods: `get()`, `post()`, `put()`, `patch()`, `delete()`. +- [x] Build the URI Matcher for static paths. +- [x] Support for PSR-7 `ServerRequestInterface` type-hinting in the matcher. +- [x] Implement basic Error Handling (Global 404). ## Milestone 3: Parameters & Validation *Goal: Support for dynamic URIs with the `{{var}}` syntax and parameter validation.* diff --git a/src/NotFoundRouteException.php b/src/NotFoundRouteException.php new file mode 100644 index 0000000..be97806 --- /dev/null +++ b/src/NotFoundRouteException.php @@ -0,0 +1,7 @@ +getMethod()); + $path = $this->normalizePath($request->getUri()->getPath()); + + foreach ($this->getRoutes() as $routeDefinition) { + if (strtoupper($routeDefinition->getMethod()) === $method && $routeDefinition->getPath() === $path) { + return $routeDefinition; + } + } + + throw new NotFoundRouteException('No route matched the request'); + } + + public function fallback(mixed $handler): self + { + $this->fallbackHandler = $handler; + return $this; + } + public function url(string $name, array $parameters = []): string { $routes = $this->getRoutes(); diff --git a/tests/Unit/ErrorHandlingTest.php b/tests/Unit/ErrorHandlingTest.php new file mode 100644 index 0000000..620b5a4 --- /dev/null +++ b/tests/Unit/ErrorHandlingTest.php @@ -0,0 +1,161 @@ + ['/path/to/modules'] + ]); + + $router = new Router($config); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/nonexistent'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $this->expectException(\Atlas\NotFoundRouteException::class); + + $router->matchOrFail($request); + } + + public function testMatchReturnsNullWhenNoRouteFound(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/nonexistent'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $result = $router->match($request); + + $this->assertNull($result); + } + + public function testRouteChainingWithDifferentHttpMethods(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $result = $router->get('/test', 'GetHandler')->post('/test', 'PostHandler'); + + $this->assertTrue($result instanceof Router); + $this->assertCount(2, $router->getRoutes()); + } + + public function testMatchUsingRouteDefinition(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestMethod'); + + $routes = $router->getRoutes(); + $this->assertCount(1, $routes); + $this->assertInstanceOf(\Atlas\RouteDefinition::class, $routes[0]); + } + + public function testCaseInsensitiveHttpMethodMatching(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestHandler'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/test'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $matchedRoute = $router->match($request); + + $this->assertNotNull($matchedRoute); + } + + public function testPathNormalizationLeadingSlashes(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestHandler'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/test'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $matchedRoute = $router->match($request); + + $this->assertNotNull($matchedRoute); + } + + public function testMatchOrFailThrowsExceptionForMultipleRoutes(): void + { + $config = new \Atlas\Config([ + 'modules_path' => ['/path/to/modules'] + ]); + + $router = new Router($config); + + $router->get('/test', 'TestHandler'); + + $uri = $this->createMock(UriInterface::class); + $uri->method('getPath')->willReturn('/test'); + $uri->method('getScheme')->willReturn('http'); + $uri->method('getHost')->willReturn('localhost'); + $uri->method('getPort')->willReturn(80); + + $request = $this->createMock(ServerRequestInterface::class); + $request->method('getMethod')->willReturn('GET'); + $request->method('getUri')->willReturn($uri); + + $matchedRoute = $router->matchOrFail($request); + + $this->assertInstanceOf(\Atlas\RouteDefinition::class, $matchedRoute); + } +} \ No newline at end of file