Skip to content

Commit

Permalink
add conditions to RouteGroup
Browse files Browse the repository at this point in the history
  • Loading branch information
IngeniozIT committed Oct 7, 2023
1 parent e4e1603 commit df114cc
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 33 deletions.
54 changes: 32 additions & 22 deletions src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@

namespace IngeniozIT\Router;

use Psr\Http\Server\{RequestHandlerInterface, MiddlewareInterface};
use Psr\Http\Message\ServerRequestInterface;
use Closure;

final readonly class Route
{
Expand Down Expand Up @@ -39,63 +37,63 @@
/**
* @param array<string, string> $patterns
*/
public static function get(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function get(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::GET, $path, $callback, $name, $patterns);
}

/**
* @param array<string, string> $patterns
*/
public static function post(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function post(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::POST, $path, $callback, $name, $patterns);
}

/**
* @param array<string, string> $patterns
*/
public static function put(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function put(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::PUT, $path, $callback, $name, $patterns);
}

/**
* @param array<string, string> $patterns
*/
public static function patch(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function patch(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::PATCH, $path, $callback, $name, $patterns);
}

/**
* @param array<string, string> $patterns
*/
public static function delete(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function delete(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::DELETE, $path, $callback, $name, $patterns);
}

/**
* @param array<string, string> $patterns
*/
public static function head(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function head(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::HEAD, $path, $callback, $name, $patterns);
}

/**
* @param array<string, string> $patterns
*/
public static function options(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function options(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::OPTIONS, $path, $callback, $name, $patterns);
}

/**
* @param array<string, string> $patterns
*/
public static function any(string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function any(string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
return new self(self::ANY, $path, $callback, $name, $patterns);
}
Expand All @@ -104,7 +102,7 @@ public static function any(string $path, Closure|string|RequestHandlerInterface|
* @param string[] $methods
* @param array<string, string> $patterns
*/
public static function some(array $methods, string $path, Closure|string|RequestHandlerInterface|MiddlewareInterface $callback, ?string $name = null, array $patterns = []): self
public static function some(array $methods, string $path, mixed $callback, ?string $name = null, array $patterns = []): self
{
$method = 0;
foreach ($methods as $methodString) {
Expand All @@ -120,7 +118,7 @@ public static function some(array $methods, string $path, Closure|string|Request
public function __construct(
public int $method,
public string $path,
public Closure|string|RequestHandlerInterface|MiddlewareInterface $callback,
public mixed $callback,
public ?string $name = null,
public array $patterns = [],
) {
Expand All @@ -136,13 +134,13 @@ public function match(ServerRequestInterface $request): false|array
}

$path = $request->getUri()->getPath();
preg_match_all('/{(.+)}/', $this->path, $matches, PREG_SET_ORDER);
$parameters = $this->extractParametersFromPath($this->path);

if (empty($matches)) {
if ($parameters === []) {
return $path === $this->path ? [] : false;
}

$extractedParameters = $this->extractParameters($matches, $path);
$extractedParameters = $this->extractParametersValues($parameters, $path);
return $extractedParameters === [] ? false : $extractedParameters;
}

Expand All @@ -152,25 +150,37 @@ private function httpMethodMatches(string $method): bool
}

/**
* @param array<string[]> $matches
* @return string[]
*/
private function extractParametersFromPath(string $path): array
{
preg_match_all('/{(.+)}/', $path, $matches, PREG_SET_ORDER);
return array_map(
static fn(array $match): string => $match[1],
$matches
);
}

/**
* @param string[] $parameters
* @return array<string, string>
*/
private function extractParameters(array $matches, string $path): array
private function extractParametersValues(array $parameters, string $path): array
{
preg_match($this->buildRegex($matches), $path, $matches);
return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
preg_match($this->buildRegex($parameters), $path, $parameters);
return array_filter($parameters, 'is_string', ARRAY_FILTER_USE_KEY);
}

/**
* @param array<string[]> $matches
* @param string[] $matches
*/
private function buildRegex(array $matches): string
{
$quotedPath = '#' . preg_quote($this->path, '#') . '#';
foreach ($matches as $match) {
$quotedPath = str_replace(
'\{' . $match[1] . '\}',
'(?<' . $match[1] . '>' . ($this->patterns[$match[1]] ?? '[^/]+') . ')',
'\{' . $match . '\}',
'(?<' . $match . '>' . ($this->patterns[$match] ?? '[^/]+') . ')',
$quotedPath
);
}
Expand Down
7 changes: 3 additions & 4 deletions src/RouteGroup.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,18 @@

namespace IngeniozIT\Router;

use Psr\Http\Server\MiddlewareInterface;
use Closure;

final class RouteGroup
{
/**
* @param Route[] $routes
* @param array<Closure|MiddlewareInterface|string> $middlewares
* @param mixed[] $middlewares
* @param mixed[] $conditions
* @param array<string, string> $patterns
*/
public function __construct(
public array $routes,
public array $middlewares = [],
public array $conditions = [],
public array $patterns = [],
) {
}
Expand Down
51 changes: 45 additions & 6 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,55 @@
use Psr\Container\ContainerInterface;
use Psr\Http\Message\{ServerRequestInterface, ResponseInterface};
use Psr\Http\Server\{RequestHandlerInterface, MiddlewareInterface};
use Closure;

final readonly class Router implements RequestHandlerInterface
{
public function __construct(
private RouteGroup $routeGroup,
private ContainerInterface $container,
private ?Closure $fallback = null,
private mixed $fallback = null,
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface
{
if ($this->routeGroup->conditions !== []) {
return $this->executeConditions($request);
}

if ($this->routeGroup->middlewares !== []) {
return $this->executeCallback(array_shift($this->routeGroup->middlewares), $request);
return $this->executeMiddlewares($request);
}

return $this->executeRoutes($request);
}

private function executeConditions(ServerRequestInterface $request): ResponseInterface
{
$newRequest = $request;
while ($this->routeGroup->conditions !== []) {
/** @var false|array<string, string> $matchedParams */
$matchedParams = $this->executeCallback(array_shift($this->routeGroup->conditions), $newRequest);
if ($matchedParams === false) {
return $this->fallback($request);
}

foreach ($matchedParams as $key => $value) {
$newRequest = $newRequest->withAttribute($key, $value);
}
}

return $this->handle($newRequest);
}

private function executeMiddlewares(ServerRequestInterface $request): ResponseInterface
{
/** @var ResponseInterface */
return $this->executeCallback(array_shift($this->routeGroup->middlewares), $request);
}

private function executeRoutes(ServerRequestInterface $request): ResponseInterface
{
foreach ($this->routeGroup->routes as $route) {
$matchedParams = $route->match($request);
if ($matchedParams === false) {
Expand All @@ -35,17 +67,24 @@ public function handle(ServerRequestInterface $request): ResponseInterface
$newRequest = $newRequest->withAttribute($key, $value);
}

/** @var ResponseInterface */
return $this->executeCallback($route->callback, $newRequest);
}

if (!$this->fallback instanceof \Closure) {
return $this->fallback($request);
}

private function fallback(ServerRequestInterface $request): ResponseInterface
{
if ($this->fallback === null) {
throw new EmptyRouteStack('No routes left to process.');
}

return ($this->fallback)($request);
/** @var ResponseInterface */
return $this->executeCallback($this->fallback, $request);
}

private function executeCallback(string|object $callback, ServerRequestInterface $request): ResponseInterface
private function executeCallback(mixed $callback, ServerRequestInterface $request): mixed
{
if (is_string($callback)) {
$callback = $this->container->get($callback);
Expand Down
59 changes: 58 additions & 1 deletion tests/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
* @SuppressWarnings(PHPMD.StaticAccess)
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @SuppressWarnings(PHPMD.TooManyPublicMethods)
*/
final class RouterTest extends TestCase
{
Expand Down Expand Up @@ -141,7 +142,7 @@ public function testCanHaveAFallbackRoute(): void
/**
* @dataProvider providerMiddlewares
*/
public function testCanHaveAMiddleware(Closure $middleware, string $expectedResponse): void
public function testCanHaveAMiddlewares(Closure $middleware, string $expectedResponse): void
{
$routeGroup = new RouteGroup(
routes: [
Expand Down Expand Up @@ -172,4 +173,60 @@ public static function providerMiddlewares(): array
],
];
}

/**
* @dataProvider providerConditions
*/
public function testCanHaveConditions(Closure $condition, string $expectedResponse): void
{
$routeGroup = new RouteGroup(
routes: [
Route::get(path: '/', callback: static fn(): ResponseInterface => self::response('TEST2')),
],
conditions: [$condition],
);
$request = self::serverRequest('GET', '/');

$response = $this->router($routeGroup, static fn(): ResponseInterface => self::response('TEST'))->handle($request);

self::assertEquals($expectedResponse, (string) $response->getBody());
}

/**
* @return array<string, array{condition: Closure, expectedResponse: string}>
*/
public static function providerConditions(): array
{
return [
'valid condition executes routes' => [
'condition' => static fn(): array => [],
'expectedResponse' => 'TEST2',
],
'invalid condition executes fallback' => [
'condition' => static fn(): bool => false,
'expectedResponse' => 'TEST',
],
];
}

public function testAddsConditionsParametersToRequest(): void
{
$routeGroup = new RouteGroup(
routes: [
Route::get(
path: '/',
callback: static fn(ServerRequestInterface $request): ResponseInterface =>
self::response(var_export($request->getAttribute('foo'), true))
),
],
conditions: [
static fn(): array => ['foo' => 'bar'],
],
);
$request = self::serverRequest('GET', '/');

$response = $this->router($routeGroup)->handle($request);

self::assertEquals("'bar'", (string) $response->getBody());
}
}

0 comments on commit df114cc

Please sign in to comment.