diff --git a/README.md b/README.md index 9bbcf93..6615313 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ $routes = new RouteGroup( ], patterns: ['page' => '.*'], ); + +$router = new Router($routes, $container); + +$response = $router->handle($request); ``` #### Path @@ -65,7 +69,7 @@ new RouteGroup([ ]); ``` -If you have a parameter that is used in multiple routes, you can define it in the `RouteGroup`. It will be used in all the routes of the group: +If you have a parameter that is used in multiple routes, you can define it inside the `RouteGroup`. It will be used in all the routes of the group: ```php new RouteGroup( diff --git a/src/RouteGroup.php b/src/RouteGroup.php index f765790..61c4888 100644 --- a/src/RouteGroup.php +++ b/src/RouteGroup.php @@ -7,7 +7,7 @@ final class RouteGroup { /** - * @param Route[] $routes + * @param array $routes * @param mixed[] $middlewares * @param mixed[] $conditions * @param array $patterns diff --git a/src/Router.php b/src/Router.php index 4d772f1..d5b2c5c 100644 --- a/src/Router.php +++ b/src/Router.php @@ -13,33 +13,37 @@ }; use Psr\Http\Server\{RequestHandlerInterface, MiddlewareInterface}; -final readonly class Router implements RequestHandlerInterface +final class Router implements RequestHandlerInterface { + private int $conditionIndex = 0; + private int $middlewareIndex = 0; + private int $routeIndex = 0; + public function __construct( - private RouteGroup $routeGroup, - private ContainerInterface $container, - private mixed $fallback = null, + private readonly RouteGroup $routeGroup, + private readonly ContainerInterface $container, + private readonly mixed $fallback = null, ) { } public function handle(ServerRequestInterface $request): ResponseInterface { - if ($this->routeGroup->conditions !== []) { + if (isset($this->routeGroup->conditions[$this->conditionIndex])) { return $this->executeConditions($request); } - if ($this->routeGroup->middlewares !== []) { + if (isset($this->routeGroup->middlewares[$this->middlewareIndex])) { return $this->executeMiddlewares($request); } - return $this->executeRoutes($request); + return $this->executeRoutables($request); } private function executeConditions(ServerRequestInterface $request): ResponseInterface { $newRequest = $request; - while ($this->routeGroup->conditions !== []) { - $matchedParams = $this->callConditionHandler(array_shift($this->routeGroup->conditions), $newRequest); + while (isset($this->routeGroup->conditions[$this->conditionIndex])) { + $matchedParams = $this->executeCondition($this->routeGroup->conditions[$this->conditionIndex++], $newRequest); if ($matchedParams === false) { return $this->fallback($request); } @@ -55,12 +59,12 @@ private function executeConditions(ServerRequestInterface $request): ResponseInt /** * @return array|false */ - private function callConditionHandler(mixed $callback, ServerRequestInterface $request): array|false + private function executeCondition(mixed $callback, ServerRequestInterface $request): array|false { $handler = $this->resolveCallback($callback); if (!is_callable($handler)) { - throw new InvalidRoute('Condition handler cannot be called.'); + throw new InvalidRoute("Condition callback is not callable."); } $result = $handler($request); @@ -74,27 +78,30 @@ private function callConditionHandler(mixed $callback, ServerRequestInterface $r private function executeMiddlewares(ServerRequestInterface $request): ResponseInterface { - return $this->callMiddlewareHandler(array_shift($this->routeGroup->middlewares), $request); - } - - private function callMiddlewareHandler(mixed $callback, ServerRequestInterface $request): ResponseInterface - { - $handler = $this->resolveCallback($callback); + $middleware = $this->routeGroup->middlewares[$this->middlewareIndex++]; + $handler = $this->resolveCallback($middleware); if ($handler instanceof MiddlewareInterface) { return $handler->process($request, $this); } if (!is_callable($handler)) { - throw new InvalidRoute('Middleware handler cannot be called.'); + throw new InvalidRoute("Middleware callback is not callable."); } return $this->processResponse($handler($request, $this)); } - private function executeRoutes(ServerRequestInterface $request): ResponseInterface + private function executeRoutables(ServerRequestInterface $request): ResponseInterface { - foreach ($this->routeGroup->routes as $route) { + while (isset($this->routeGroup->routes[$this->routeIndex])) { + $route = $this->routeGroup->routes[$this->routeIndex++]; + + if ($route instanceof RouteGroup) { + $newRouter = new Router($route, $this->container, $this->handle(...)); + return $newRouter->handle($request); + } + $matchedParams = $route->match($request, $this->routeGroup->patterns); if ($matchedParams === false) { continue; @@ -124,7 +131,7 @@ private function callRouteHandler(mixed $callback, ServerRequestInterface $reque } if (!is_callable($handler)) { - throw new InvalidRoute('Route handler cannot be called.'); + throw new InvalidRoute("Route callback is not callable."); } return $this->processResponse($handler($request, $this)); diff --git a/tests/RouterConditionTest.php b/tests/RouteGroupConditionTest.php similarity index 80% rename from tests/RouterConditionTest.php rename to tests/RouteGroupConditionTest.php index 8fd1b2e..f080a9a 100644 --- a/tests/RouterConditionTest.php +++ b/tests/RouteGroupConditionTest.php @@ -6,14 +6,14 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; -use IngeniozIT\Router\{InvalidRoute, RouteGroup, Router, Route}; +use IngeniozIT\Router\{Router, RouteGroup, Route, InvalidRoute}; use IngeniozIT\Http\Message\UriFactory; use Closure; /** * @SuppressWarnings(PHPMD.StaticAccess) */ -final class RouterConditionTest extends TestCase +final class RouteGroupConditionTest extends TestCase { use PsrTrait; @@ -58,6 +58,25 @@ public static function providerConditions(): array ]; } + public function testCanHaveMultipleConditions(): void + { + $routeGroup = new RouteGroup( + routes: [ + Route::get(path: '/', callback: static fn(): ResponseInterface => self::response('TEST2')), + ], + conditions: [ + static fn(): array => [], + static fn(): bool => false, + ], + ); + $request = self::serverRequest('GET', '/'); + + $response = $this->router($routeGroup, static fn(): ResponseInterface => + self::response('TEST'))->handle($request); + + self::assertEquals('TEST', (string)$response->getBody()); + } + /** * @dataProvider providerInvalidConditions */ diff --git a/tests/RouterMiddlewareTest.php b/tests/RouteGroupMiddlewareTest.php similarity index 77% rename from tests/RouterMiddlewareTest.php rename to tests/RouteGroupMiddlewareTest.php index df0b55b..905701b 100644 --- a/tests/RouterMiddlewareTest.php +++ b/tests/RouteGroupMiddlewareTest.php @@ -4,14 +4,15 @@ namespace IngeniozIT\Router\Tests; +use Exception; use PHPUnit\Framework\TestCase; -use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use IngeniozIT\Router\{ - InvalidRoute, - RouteGroup, Router, + RouteGroup, Route, + InvalidRoute, }; +use Psr\Http\Message\{ResponseInterface, ServerRequestInterface}; use IngeniozIT\Router\Tests\Fakes\TestMiddleware; use Psr\Http\Server\RequestHandlerInterface; use IngeniozIT\Http\Message\UriFactory; @@ -20,7 +21,7 @@ /** * @SuppressWarnings(PHPMD.StaticAccess) */ -final class RouterMiddlewareTest extends TestCase +final class RouteGroupMiddlewareTest extends TestCase { use PsrTrait; @@ -68,6 +69,25 @@ public static function providerMiddlewares(): array ]; } + public function testCanHaveMultipleMiddlewares(): void + { + $routeGroup = new RouteGroup( + routes: [ + Route::get(path: '/', callback: static fn(): ResponseInterface => self::response('TEST2')), + ], + middlewares: [ + static fn(ServerRequestInterface $request, RequestHandlerInterface $handler) => $handler->handle($request), + static fn(ServerRequestInterface $request, RequestHandlerInterface $handler) => throw new Exception(''), + ], + ); + $request = self::serverRequest('GET', '/'); + + self::expectException(Exception::class); + $response = $this->router($routeGroup)->handle($request); + + self::assertEquals('TEST', (string) $response->getBody()); + } + /** * @dataProvider providerInvalidMiddlewares */ diff --git a/tests/RouteTest.php b/tests/RouteTest.php index c11bd9b..a12e19a 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -17,7 +17,7 @@ final class RouteTest extends TestCase /** * @dataProvider providerMethodsAndRoutes */ - public function testMatchesRequestBasedOnMethod(string $method, callable $routeCallable): void + public function testMatchesRequestsBasedOnMethod(string $method, callable $routeCallable): void { /** @var Route $route */ $route = $routeCallable('/', 'foo'); @@ -89,7 +89,7 @@ public function testCanMatchSomeMethods(): void self::assertSame(false, $putResult); } - public function testMatchesRequestBasedOnPath(): void + public function testMatchesRequestsBasedOnPath(): void { $route = Route::get('/foo', 'foo'); $matchingRequest = self::serverRequest('GET', '/foo'); @@ -133,9 +133,20 @@ public function testCanUseCustomPatterns(Route $route): void public static function providerRoutePatterns(): array { return [ - 'from the path' => [Route::get('/foo/{bar:\d+}/{baz:\d+}', 'foo')], - 'from the patterns parameter' => [Route::get('/foo/{bar}/{baz}', 'foo', patterns: ['bar' => '\d+', 'baz' => '\d+'])], - 'path takes precedence over the patterns parameter' => [Route::get('/foo/{bar:\d+}/{baz:\d+}', 'foo', patterns: ['bar' => '[a-z]+', 'baz' => '\d+'])], + 'patterns inside the path' => [Route::get( + path: '/foo/{bar:\d+}/{baz:\d+}', + callback: 'foo' + )], + 'patterns as a parameter' => [Route::get( + path: '/foo/{bar}/{baz}', + callback: 'foo', + patterns: ['bar' => '\d+', 'baz' => '\d+'], + )], + 'path takes precendence over parameters' => [Route::get( + path: '/foo/{bar:\d+}/{baz:\d+}', + callback: 'foo', + patterns: ['bar' => '[a-z]+', 'baz' => '\d+'], + )], ]; } diff --git a/tests/SubGroupTest.php b/tests/SubGroupTest.php new file mode 100644 index 0000000..cecc6a5 --- /dev/null +++ b/tests/SubGroupTest.php @@ -0,0 +1,59 @@ + 'TEST'), + ], + ), + ], + ); + $request = self::serverRequest('GET', '/sub'); + + $response = $this->router($routeGroup)->handle($request); + + self::assertEquals('TEST', (string)$response->getBody()); + } + + public function testCanHandleARouteAfterASubGroup(): void + { + $routeGroup = new RouteGroup( + routes: [ + new RouteGroup( + routes: [ + Route::get(path: '/sub', callback: static fn() => 'TEST'), + ], + ), + Route::get(path: '/after-sub', callback: static fn() => 'TEST2'), + ], + ); + $request = self::serverRequest('GET', '/after-sub'); + + $response = $this->router($routeGroup)->handle($request); + + self::assertEquals('TEST2', (string)$response->getBody()); + } +}