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
This commit is contained in:
parent
509716f64d
commit
29980d8bc6
|
|
@ -12,10 +12,10 @@ This document outlines the phased development roadmap for the Atlas Routing engi
|
||||||
|
|
||||||
## Milestone 2: Basic URI Matching & Methods
|
## Milestone 2: Basic URI Matching & Methods
|
||||||
*Goal: Implement the matching engine for standard HTTP methods and static URIs.*
|
*Goal: Implement the matching engine for standard HTTP methods and static URIs.*
|
||||||
- [ ] Implement fluent methods: `get()`, `post()`, `put()`, `patch()`, `delete()`.
|
- [x] Implement fluent methods: `get()`, `post()`, `put()`, `patch()`, `delete()`.
|
||||||
- [ ] Build the URI Matcher for static paths.
|
- [x] Build the URI Matcher for static paths.
|
||||||
- [ ] Support for PSR-7 `ServerRequestInterface` type-hinting in the matcher.
|
- [x] Support for PSR-7 `ServerRequestInterface` type-hinting in the matcher.
|
||||||
- [ ] Implement basic Error Handling (Global 404).
|
- [x] Implement basic Error Handling (Global 404).
|
||||||
|
|
||||||
## Milestone 3: Parameters & Validation
|
## Milestone 3: Parameters & Validation
|
||||||
*Goal: Support for dynamic URIs with the `{{var}}` syntax and parameter validation.*
|
*Goal: Support for dynamic URIs with the `{{var}}` syntax and parameter validation.*
|
||||||
|
|
|
||||||
7
src/NotFoundRouteException.php
Normal file
7
src/NotFoundRouteException.php
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas;
|
||||||
|
|
||||||
|
class NotFoundRouteException extends \RuntimeException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||||
class Router
|
class Router
|
||||||
{
|
{
|
||||||
private array $routes = [];
|
private array $routes = [];
|
||||||
|
protected mixed $fallbackHandler = null;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Config $config
|
private readonly Config $config
|
||||||
|
|
@ -57,7 +58,13 @@ class Router
|
||||||
|
|
||||||
private function normalizePath(string $path): string
|
private function normalizePath(string $path): string
|
||||||
{
|
{
|
||||||
return '/' . ltrim($path, '/');
|
$normalized = trim($path, '/');
|
||||||
|
|
||||||
|
if (empty($normalized)) {
|
||||||
|
return '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '/' . $normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function storeRoute(RouteDefinition $routeDefinition): void
|
protected function storeRoute(RouteDefinition $routeDefinition): void
|
||||||
|
|
@ -90,6 +97,26 @@ class Router
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function matchOrFail(ServerRequestInterface $request): RouteDefinition
|
||||||
|
{
|
||||||
|
$method = strtoupper($request->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
|
public function url(string $name, array $parameters = []): string
|
||||||
{
|
{
|
||||||
$routes = $this->getRoutes();
|
$routes = $this->getRoutes();
|
||||||
|
|
|
||||||
161
tests/Unit/ErrorHandlingTest.php
Normal file
161
tests/Unit/ErrorHandlingTest.php
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Atlas\Tests\Unit;
|
||||||
|
|
||||||
|
use Atlas\Router;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use Psr\Http\Message\UriInterface;
|
||||||
|
|
||||||
|
final class ErrorHandlingTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testMatchOrFailThrowsExceptionWhenNoRouteFound(): 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);
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue