Skip to content

Commit

Permalink
handle nested route groups
Browse files Browse the repository at this point in the history
  • Loading branch information
IngeniozIT committed Dec 13, 2023
1 parent a9c68f4 commit 0c23758
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 34 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ $routes = new RouteGroup(
],
patterns: ['page' => '.*'],
);

$router = new Router($routes, $container);

$response = $router->handle($request);
```

#### Path
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/RouteGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
final class RouteGroup
{
/**
* @param Route[] $routes
* @param array<Route|RouteGroup> $routes
* @param mixed[] $middlewares
* @param mixed[] $conditions
* @param array<string, string> $patterns
Expand Down
49 changes: 28 additions & 21 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -55,12 +59,12 @@ private function executeConditions(ServerRequestInterface $request): ResponseInt
/**
* @return array<string, mixed>|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);
Expand All @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,7 +21,7 @@
/**
* @SuppressWarnings(PHPMD.StaticAccess)
*/
final class RouterMiddlewareTest extends TestCase
final class RouteGroupMiddlewareTest extends TestCase
{
use PsrTrait;

Expand Down Expand Up @@ -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
*/
Expand Down
21 changes: 16 additions & 5 deletions tests/RouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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+'],
)],
];
}

Expand Down
59 changes: 59 additions & 0 deletions tests/SubGroupTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace IngeniozIT\Router\Tests;

use PHPUnit\Framework\TestCase;
use IngeniozIT\Router\{Router, RouteGroup, Route};
use Closure;

/**
* @SuppressWarnings(PHPMD.StaticAccess)
*/
class SubGroupTest extends TestCase
{
use PsrTrait;

private function router(RouteGroup $routeGroup, ?Closure $fallback = null): Router
{
return new Router($routeGroup, self::container(), $fallback);
}

public function testCanHaveSubGroups(): void
{
$routeGroup = new RouteGroup(
routes: [
new RouteGroup(
routes: [
Route::get(path: '/sub', callback: static fn() => '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());
}
}

0 comments on commit 0c23758

Please sign in to comment.