Skip to content

Commit 7001e22

Browse files
committed
feat: implement optional route parameters
1 parent 22cfe96 commit 7001e22

File tree

4 files changed

+75
-12
lines changed

4 files changed

+75
-12
lines changed

packages/router/src/Routing/Construction/DiscoveredRoute.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ final class DiscoveredRoute implements Route
1414

1515
public const string ROUTE_PARAM_NAME_REGEX = '(\w*)';
1616

17+
public const string ROUTE_PARAM_OPTIONAL_REGEX = '(\??)';
18+
1719
public const string ROUTE_PARAM_CUSTOM_REGEX = '(?::([^{}]*(?:\{(?-1)\}[^{}]*)*))?';
1820

1921
/** @param \Tempest\Router\RouteDecorator[] $decorators */
@@ -23,10 +25,13 @@ public static function fromRoute(Route $route, array $decorators, MethodReflecto
2325
$route = $decorator->decorate($route);
2426
}
2527

28+
$paramInfo = self::getRouteParams($route->uri);
29+
2630
return new self(
2731
$route->uri,
2832
$route->method,
29-
self::getRouteParams($route->uri),
33+
$paramInfo['names'],
34+
$paramInfo['optional'],
3035
$route->middleware,
3136
$methodReflector,
3237
$route->without ?? [],
@@ -39,6 +44,8 @@ private function __construct(
3944
public string $uri,
4045
public Method $method,
4146
public array $parameters,
47+
/** @var array<string, bool> */
48+
public array $optionalParameters,
4249
/** @var class-string<\Tempest\Router\HttpMiddleware>[] */
4350
public array $middleware,
4451
public MethodReflector $handler,
@@ -47,14 +54,30 @@ private function __construct(
4754
$this->isDynamic = $parameters !== [];
4855
}
4956

50-
/** @return string[] */
57+
/**
58+
* @return array{
59+
* names: string[],
60+
* optional: array<string, bool>
61+
* }
62+
*/
5163
private static function getRouteParams(string $uriPart): array
5264
{
53-
$regex = '#\{' . self::ROUTE_PARAM_NAME_REGEX . self::ROUTE_PARAM_CUSTOM_REGEX . '\}#';
65+
$regex = '#\{' . self::ROUTE_PARAM_NAME_REGEX . self::ROUTE_PARAM_OPTIONAL_REGEX . self::ROUTE_PARAM_CUSTOM_REGEX . '\}#';
5466

5567
preg_match_all($regex, $uriPart, $matches);
5668

57-
return $matches[1] ?? [];
69+
$names = $matches[1] ?? [];
70+
$optionalMarkers = $matches[2] ?? [];
71+
72+
$optional = [];
73+
foreach ($names as $i => $name) {
74+
$optional[$name] = $optionalMarkers[$i] === '?';
75+
}
76+
77+
return [
78+
'names' => $names,
79+
'optional' => $optional,
80+
];
5881
}
5982

6083
/**

packages/router/src/Routing/Construction/RouteTreeNode.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,24 @@ public function findOrCreateNodeForSegment(string $routeSegment): self
5050
return $this->dynamicPaths[$regexRouteSegment] ??= self::createDynamicRouteNode($regexRouteSegment);
5151
}
5252

53-
public function setTargetRoute(MarkedRoute $markedRoute): void
53+
public function setTargetRoute(MarkedRoute $markedRoute, bool $allowDuplicate = false): void
5454
{
55-
if ($this->targetRoute !== null) {
55+
if ($this->targetRoute !== null && ! $allowDuplicate) {
5656
throw new DuplicateRouteException($markedRoute->route);
5757
}
5858

59-
$this->targetRoute = $markedRoute;
59+
if ($this->targetRoute === null) {
60+
$this->targetRoute = $markedRoute;
61+
}
6062
}
6163

6264
private static function convertDynamicSegmentToRegex(string $uriPart): string
6365
{
64-
$regex = '#\{' . DiscoveredRoute::ROUTE_PARAM_NAME_REGEX . DiscoveredRoute::ROUTE_PARAM_CUSTOM_REGEX . '\}#';
66+
$regex = '#\{' . DiscoveredRoute::ROUTE_PARAM_NAME_REGEX . DiscoveredRoute::ROUTE_PARAM_OPTIONAL_REGEX . DiscoveredRoute::ROUTE_PARAM_CUSTOM_REGEX . '\}#';
6567

6668
return preg_replace_callback(
6769
$regex,
68-
static fn ($matches) => trim($matches[2] ?? DiscoveredRoute::DEFAULT_MATCHING_GROUP),
70+
static fn ($matches) => trim($matches[3] ?? DiscoveredRoute::DEFAULT_MATCHING_GROUP),
6971
$uriPart,
7072
);
7173
}

packages/router/src/Routing/Construction/RoutingTree.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,33 @@ public function add(MarkedRoute $markedRoute): void
2727
// @mago-expect lint:no-multi-assignments
2828
$node = $this->roots[$method->value] ??= RouteTreeNode::createRootRoute();
2929

30+
$segments = $markedRoute->route->split();
31+
$hasOptionalParams = false;
32+
3033
// Traverse the tree and find the node for each route segment
31-
foreach ($markedRoute->route->split() as $routeSegment) {
34+
foreach ($segments as $index => $routeSegment) {
35+
$isOptional = $this->isOptionalSegment($routeSegment);
36+
37+
if ($isOptional) {
38+
$hasOptionalParams = true;
39+
$node->setTargetRoute($markedRoute, allowDuplicate: true);
40+
$routeSegment = $this->stripOptionalMarker($routeSegment);
41+
}
42+
3243
$node = $node->findOrCreateNodeForSegment($routeSegment);
3344
}
3445

35-
$node->setTargetRoute($markedRoute);
46+
$node->setTargetRoute($markedRoute, allowDuplicate: $hasOptionalParams);
47+
}
48+
49+
private function isOptionalSegment(string $segment): bool
50+
{
51+
return str_contains($segment, '?');
52+
}
53+
54+
private function stripOptionalMarker(string $segment): string
55+
{
56+
return str_replace('?', '', $segment);
3657
}
3758

3859
/** @return array<string, MatchingRegex> */

packages/router/src/Routing/Matching/GenericRouteMatcher.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,24 @@ private function extractParams(DiscoveredRoute $route, array $routeMatches): arr
8080
$valueMap = [];
8181

8282
foreach ($route->parameters as $i => $param) {
83-
$value = $routeMatches[$i + 1];
83+
$value = $routeMatches[$i + 1] ?? null;
84+
$isOptional = $route->optionalParameters[$param] ?? false;
85+
86+
if ($value === null || $value === '') {
87+
if (! $isOptional) {
88+
continue;
89+
}
90+
91+
$parameterReflector = $route->handler->getParameter($param);
92+
93+
if ($parameterReflector && $parameterReflector->hasDefaultValue()) {
94+
$valueMap[$param] = $parameterReflector->getDefaultValue();
95+
96+
continue;
97+
}
98+
99+
continue;
100+
}
84101

85102
$parameterReflector = $route->handler->getParameter($param);
86103

0 commit comments

Comments
 (0)