Skip to content

Commit

Permalink
Router::pathTo handles custom parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
IngeniozIT committed Feb 27, 2024
1 parent 44476e6 commit adeb45b
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 87 deletions.
64 changes: 0 additions & 64 deletions index.php

This file was deleted.

23 changes: 23 additions & 0 deletions src/Exception/InvalidRouteParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace IngeniozIT\Router\Exception;

use InvalidArgumentException;
use Throwable;

final class InvalidRouteParameter extends InvalidArgumentException
{
public function __construct(
string $routeName,
string $parameterName,
string $pattern,
?Throwable $previous = null
) {
parent::__construct(
"Parameter '$parameterName' for route with name '$routeName' does not match the pattern '$pattern'.",
previous: $previous,
);
}
}
22 changes: 22 additions & 0 deletions src/Exception/MissingRouteParameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace IngeniozIT\Router\Exception;

use InvalidArgumentException;
use Throwable;

final class MissingRouteParameter extends InvalidArgumentException
{
public function __construct(
string $routeName,
string $parameterName,
?Throwable $previous = null
) {
parent::__construct(
"Missing parameter '$parameterName' for route with name '$routeName'.",
previous: $previous,
);
}
}
40 changes: 30 additions & 10 deletions src/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
'OPTIONS' => self::OPTIONS,
];

/** @var array<string, string> */
public array $where;
public string $path;
public bool $hasParameters;

/**
* @param array<string, string> $where
* @param array<string, string> $with
Expand Down Expand Up @@ -113,10 +118,7 @@ public static function any(string $path, mixed $callback, array $where = [], arr
*/
public static function some(array $methods, string $path, mixed $callback, array $where = [], array $with = [], ?string $name = null): self
{
$method = 0;
foreach ($methods as $methodString) {
$method |= self::METHODS[strtoupper($methodString)];
}
$method = array_reduce($methods, fn($carry, $methodString) => $carry | self::METHODS[strtoupper($methodString)], 0);

return new self($method, $path, $callback, $where, $with, $name);
}
Expand All @@ -127,12 +129,25 @@ public static function some(array $methods, string $path, mixed $callback, array
*/
public function __construct(
public int $method,
public string $path,
string $path,
public mixed $callback,
public array $where = [],
array $where = [],
public array $with = [],
public ?string $name = null,
) {
$this->hasParameters = str_contains($path, '{');
[$this->where, $this->path] = $this->extractPatterns($where, $path);
}

private function extractPatterns(array $where, string $path): array

Check failure on line 142 in src/Route.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Route::extractPatterns() has parameter $where with no value type specified in iterable type array.

Check failure on line 142 in src/Route.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Route::extractPatterns() return type has no value type specified in iterable type array.

Check failure on line 142 in src/Route.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Route::extractPatterns() has parameter $where with no value type specified in iterable type array.

Check failure on line 142 in src/Route.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Route::extractPatterns() return type has no value type specified in iterable type array.
{
if ($this->hasParameters && str_contains($path, ':') && preg_match_all('#{(\w+):([^}]+)}#', $path, $matches, PREG_SET_ORDER)) {

Check warning on line 144 in src/Route.php

View workflow job for this annotation

GitHub Actions / build

Escaped Mutant for Mutator "LogicalAnd": --- Original +++ New @@ @@ } private function extractPatterns(array $where, string $path) : array { - if ($this->hasParameters && str_contains($path, ':') && preg_match_all('#{(\\w+):([^}]+)}#', $path, $matches, PREG_SET_ORDER)) { + if (($this->hasParameters || str_contains($path, ':')) && preg_match_all('#{(\\w+):([^}]+)}#', $path, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $path = str_replace($match[0], '{' . $match[1] . '}', $path); $where[$match[1]] = $match[2];

Check warning on line 144 in src/Route.php

View workflow job for this annotation

GitHub Actions / build

Escaped Mutant for Mutator "LogicalAnd": --- Original +++ New @@ @@ } private function extractPatterns(array $where, string $path) : array { - if ($this->hasParameters && str_contains($path, ':') && preg_match_all('#{(\\w+):([^}]+)}#', $path, $matches, PREG_SET_ORDER)) { + if (($this->hasParameters || str_contains($path, ':')) && preg_match_all('#{(\\w+):([^}]+)}#', $path, $matches, PREG_SET_ORDER)) { foreach ($matches as $match) { $path = str_replace($match[0], '{' . $match[1] . '}', $path); $where[$match[1]] = $match[2];
foreach ($matches as $match) {
$path = str_replace($match[0], '{' . $match[1] . '}', $path);
$where[$match[1]] = $match[2];
}
}
return [$where, $path];
}

/**
Expand All @@ -145,12 +160,12 @@ public function match(ServerRequestInterface $request): false|array
}

$path = $request->getUri()->getPath();
$parameters = $this->extractParametersFromPath($this->path);

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

$parameters = $this->extractParametersFromPath($this->path);
$extractedParameters = $this->extractParametersValue($parameters, $path);
return $extractedParameters === [] ? false : $extractedParameters;
}
Expand All @@ -165,7 +180,7 @@ private function httpMethodMatches(string $method): bool
*/
private function extractParametersFromPath(string $path): array
{
preg_match_all('/{([^:]+)(?::(.+))?}/U', $path, $matches, PREG_SET_ORDER);
preg_match_all('/{([^:]+)}/U', $path, $matches, PREG_SET_ORDER);
return $matches;
}

Expand All @@ -188,11 +203,16 @@ private function buildRegex(array $parameters): string
foreach ($parameters as $parameter) {
$regex = str_replace(
preg_quote($parameter[0], '#'),
'(?<' . $parameter[1] . '>' . ($parameter[2] ?? $this->where[$parameter[1]] ?? '[^/]+') . ')',
'(?<' . $parameter[1] . '>' . $this->parameterPattern($parameter[1]) . ')',
$regex
);
}

return $regex;
}

public function parameterPattern(string $parameterName): string
{
return $this->where[$parameterName] ?? '[^/]+';
}
}
42 changes: 32 additions & 10 deletions src/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace IngeniozIT\Router;

use IngeniozIT\Router\Exception\EmptyRouteStack;
use IngeniozIT\Router\Exception\InvalidRouteParameter;
use IngeniozIT\Router\Exception\MissingRouteParameter;
use IngeniozIT\Router\Exception\RouteNotFound;
use IngeniozIT\Router\Handler\ConditionHandler;
use IngeniozIT\Router\Handler\MiddlewaresHandler;
Expand Down Expand Up @@ -107,32 +109,52 @@ private function fallback(ServerRequestInterface $request): ResponseInterface
return $routeHandler->handle($request, $this);
}

public function pathTo(string $routeName): string
public function pathTo(string $routeName, array $parameters = []): string

Check failure on line 112 in src/Router.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Router::pathTo() has parameter $parameters with no value type specified in iterable type array.

Check failure on line 112 in src/Router.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Router::pathTo() has parameter $parameters with no value type specified in iterable type array.
{
$route = $this->findNamedRoute($routeName, $this->routeGroup);
$route = $this->findNamedRoute($routeName, $parameters, $this->routeGroup);

if (!$route instanceof Route) {
if (!$route) {
throw new RouteNotFound("Route with name '$routeName' not found.");
}

return $route->path;
return $route;
}

private function findNamedRoute(string $routeName, RouteGroup $routeGroup): ?Route
private function findNamedRoute(string $routeName, array $parameters, RouteGroup $routeGroup): ?string

Check failure on line 123 in src/Router.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Router::findNamedRoute() has parameter $parameters with no value type specified in iterable type array.

Check failure on line 123 in src/Router.php

View workflow job for this annotation

GitHub Actions / build

Method IngeniozIT\Router\Router::findNamedRoute() has parameter $parameters with no value type specified in iterable type array.
{
foreach ($routeGroup->routes as $route) {
if ($route instanceof RouteGroup) {
$foundRoute = $this->findNamedRoute($routeName, $route);
if ($foundRoute instanceof Route) {
return $foundRoute;
$foundRoute = $this->findNamedRoute($routeName, $parameters, $route);
if ($foundRoute === null) {
continue;
}
return $foundRoute;
}

if ($route->name !== $routeName) {
continue;
}

if ($route->name === $routeName) {
return $route;
if (!$route->hasParameters) {
return $route->path;
}

$matchedParams = [];
preg_match_all('#{(\w+)}#', $route->path, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
if (!isset($parameters[$match[1]])) {
throw new MissingRouteParameter($routeName, $match[1]);
}
if (!preg_match('#^' . $route->parameterPattern($match[1]) . '$#', $parameters[$match[1]])) {
throw new InvalidRouteParameter($routeName, $match[1], $route->parameterPattern($match[1]));
}
$matchedParams[$match[1]] = $parameters[$match[1]];
}
$path = $route->path;
foreach ($matchedParams as $key => $value) {
$path = str_replace('{' . $key . '}', $value, $path);
}
return $path;
}

return null;
Expand Down
6 changes: 3 additions & 3 deletions tests/Features/HttpMethodTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public function testCanMatchSomeMethods(): void
{
$routeGroup = new RouteGroup(
routes: [
Route::some(['GET', 'POST'], '/', static fn(): ResponseInterface => self::response('OK')),
Route::some(['POST', 'PUT'], '/', static fn(): ResponseInterface => self::response('OK')),
Route::any('/', static fn(): ResponseInterface => self::response('KO')),
],
);
Expand All @@ -84,9 +84,9 @@ public function testCanMatchSomeMethods(): void
$postResult = $this->router($routeGroup)->handle($postRequest);
$putResult = $this->router($routeGroup)->handle($putRequest);

self::assertSame('OK', (string) $getResult->getBody());
self::assertSame('KO', (string) $getResult->getBody());
self::assertSame('OK', (string) $postResult->getBody());
self::assertSame('KO', (string) $putResult->getBody());
self::assertSame('OK', (string) $putResult->getBody());
}

public function testMethodNameCanBeLowercase(): void
Expand Down
35 changes: 35 additions & 0 deletions tests/Features/NameTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace IngeniozIT\Router\Tests\Features;

use IngeniozIT\Router\Exception\InvalidRouteParameter;
use IngeniozIT\Router\Exception\MissingRouteParameter;
use IngeniozIT\Router\Exception\RouteNotFound;
use IngeniozIT\Router\Route;
use IngeniozIT\Router\RouteGroup;
Expand All @@ -12,6 +14,7 @@ final class NameTest extends RouterCase
public function testRouterCanFindARoutePathByName(): void
{
$routeGroup = new RouteGroup([
Route::get('/bar', 'foo', name: 'not_this_route'),
Route::get('/foo', 'foo', name: 'route_name'),
]);
$router = $this->router(new RouteGroup([
Expand All @@ -24,6 +27,18 @@ public function testRouterCanFindARoutePathByName(): void
self::assertSame('/foo', $result);
}

public function testRouterCanFindARoutePathByNameWithParameters(): void
{
$routeGroup = new RouteGroup([
Route::get('/{foo:\d+}', 'foo', name: 'route_name'),
]);
$router = $this->router($routeGroup);

$result = $router->pathTo('route_name', ['foo' => '42']);

self::assertSame('/42', $result);
}

public function testRouteGroupsPassTheirNameToTheirSubRoutes(): void
{
$routeGroup = new RouteGroup(
Expand All @@ -47,4 +62,24 @@ public function testRouterCannotFindAnInexistingRoutePathByName(): void
self::expectException(RouteNotFound::class);
$router->pathTo('inexisting_route_name');
}

public function testRouterCannotFindARoutePathWithMissingParameters(): void
{
$route = Route::get('/{foo}', 'foo', name: 'route_name');
$router = $this->router(new RouteGroup([$route]));

self::expectException(MissingRouteParameter::class);
self::expectExceptionMessage("Missing parameter 'foo' for route with name 'route_name'.");
$router->pathTo('route_name');
}

public function testRouterCannotFindARoutePathWithInvalidParameters(): void
{
$route = Route::get('/{foo:\d+}', 'foo', name: 'route_name');
$router = $this->router(new RouteGroup([$route]));

self::expectException(InvalidRouteParameter::class);
self::expectExceptionMessage("Parameter 'foo' for route with name 'route_name' does not match the pattern '\d+'.");
$router->pathTo('route_name', ['foo' => 'bar']);
}
}

0 comments on commit adeb45b

Please sign in to comment.