Skip to content

Commit f055fc1

Browse files
authored
feat: route enum binding support (#668)
1 parent 166912d commit f055fc1

File tree

8 files changed

+157
-29
lines changed

8 files changed

+157
-29
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Exceptions;
6+
7+
use Exception;
8+
9+
final class NotFoundException extends Exception
10+
{
11+
}

src/Tempest/Http/src/GenericRouter.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tempest\Http;
66

7+
use BackedEnum;
78
use Closure;
89
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
910
use ReflectionException;
@@ -12,6 +13,7 @@
1213
use Tempest\Http\Exceptions\ControllerActionHasNoReturn;
1314
use Tempest\Http\Exceptions\InvalidRouteException;
1415
use Tempest\Http\Exceptions\MissingControllerOutputException;
16+
use Tempest\Http\Exceptions\NotFoundException;
1517
use Tempest\Http\Mappers\RequestToPsrRequestMapper;
1618
use Tempest\Http\Responses\Invalid;
1719
use Tempest\Http\Responses\NotFound;
@@ -59,8 +61,9 @@ public function dispatch(Request|PsrRequest $request): Response
5961
try {
6062
$request = $this->resolveRequest($request, $matchedRoute);
6163
$response = $callable($request);
64+
} catch (NotFoundException) {
65+
return new NotFound();
6266
} catch (ValidationException $validationException) {
63-
// TODO: refactor to middleware
6467
return new Invalid($request, $validationException->failingRules);
6568
}
6669

@@ -141,6 +144,10 @@ public function toUri(array|string $action, ...$params): string
141144
continue;
142145
}
143146

147+
if ($value instanceof BackedEnum) {
148+
$value = $value->value;
149+
}
150+
144151
$pattern = '#\{' . $key . Route::ROUTE_PARAM_CUSTOM_REGEX . '\}#';
145152
$uri = preg_replace($pattern, (string)$value, $uri);
146153
}

src/Tempest/Http/src/RouteBindingInitializer.php

Lines changed: 0 additions & 28 deletions
This file was deleted.
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http;
6+
7+
use BackedEnum;
8+
use Tempest\Container\Container;
9+
use Tempest\Container\DynamicInitializer;
10+
use Tempest\Http\Exceptions\NotFoundException;
11+
use Tempest\Reflection\ClassReflector;
12+
13+
final class RouteEnumBindingInitializer implements DynamicInitializer
14+
{
15+
public function canInitialize(ClassReflector $class): bool
16+
{
17+
return $class->getType()->matches(BackedEnum::class);
18+
}
19+
20+
public function initialize(ClassReflector $class, Container $container): object
21+
{
22+
$matchedRoute = $container->get(MatchedRoute::class);
23+
24+
$parameter = null;
25+
26+
foreach ($matchedRoute->route->handler->getParameters() as $searchParameter) {
27+
if ($searchParameter->getType()->equals($class->getType())) {
28+
$parameter = $searchParameter;
29+
30+
break;
31+
}
32+
}
33+
34+
$enum = $class->callStatic('tryFrom', $matchedRoute->params[$parameter->getName()]);
35+
36+
if ($enum === null) {
37+
throw new NotFoundException();
38+
}
39+
40+
return $enum;
41+
}
42+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http;
6+
7+
use Tempest\Container\Container;
8+
use Tempest\Container\DynamicInitializer;
9+
use Tempest\Database\DatabaseModel;
10+
use Tempest\Database\Id;
11+
use Tempest\Http\Exceptions\NotFoundException;
12+
use Tempest\Reflection\ClassReflector;
13+
14+
final class RouteModelBindingInitializer implements DynamicInitializer
15+
{
16+
public function canInitialize(ClassReflector $class): bool
17+
{
18+
return $class->getType()->matches(DatabaseModel::class);
19+
}
20+
21+
public function initialize(ClassReflector $class, Container $container): object
22+
{
23+
$matchedRoute = $container->get(MatchedRoute::class);
24+
25+
$parameter = null;
26+
27+
foreach ($matchedRoute->route->handler->getParameters() as $searchParameter) {
28+
if ($searchParameter->getType()->equals($class->getType())) {
29+
$parameter = $searchParameter;
30+
31+
break;
32+
}
33+
}
34+
35+
$model = $class->callStatic('find', new Id($matchedRoute->params[$parameter->getName()]));
36+
37+
if ($model === null) {
38+
throw new NotFoundException();
39+
}
40+
41+
return $model;
42+
}
43+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Fixtures\Controllers;
6+
7+
use Tempest\Http\Get;
8+
use Tempest\Http\Response;
9+
use Tempest\Http\Responses\Ok;
10+
11+
final readonly class ControllerWithEnumBinding
12+
{
13+
#[Get('/with-enum/{input}')]
14+
public function __invoke(EnumForController $input): Response
15+
{
16+
return new Ok();
17+
}
18+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Tempest\Fixtures\Controllers;
6+
7+
enum EnumForController: string
8+
{
9+
case FOO = 'foo';
10+
case BAR = 'bar';
11+
case BAZ = 'baz';
12+
}

tests/Integration/Route/RouterTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
use Tempest\Http\Responses\Ok;
1111
use Tempest\Http\Router;
1212
use Tempest\Http\Status;
13+
use function Tempest\uri;
14+
use Tests\Tempest\Fixtures\Controllers\ControllerWithEnumBinding;
15+
use Tests\Tempest\Fixtures\Controllers\EnumForController;
1316
use Tests\Tempest\Fixtures\Controllers\TestController;
1417
use Tests\Tempest\Fixtures\Controllers\TestGlobalMiddleware;
1518
use Tests\Tempest\Fixtures\Migrations\CreateAuthorTable;
@@ -147,4 +150,24 @@ public function test_repeated_routes(): void
147150
$this->http->post('/repeated/e')->assertOk();
148151
$this->http->post('/repeated/f')->assertOk();
149152
}
153+
154+
public function test_enum_route_binding(): void
155+
{
156+
$this->http->get('/with-enum/foo')->assertOk();
157+
$this->http->get('/with-enum/bar')->assertOk();
158+
$this->http->get('/with-enum/unknown')->assertNotFound();
159+
}
160+
161+
public function test_generate_uri_with_enum(): void
162+
{
163+
$this->assertSame(
164+
'/with-enum/foo',
165+
uri(ControllerWithEnumBinding::class, input: EnumForController::FOO),
166+
);
167+
168+
$this->assertSame(
169+
'/with-enum/bar',
170+
uri(ControllerWithEnumBinding::class, input: EnumForController::BAR),
171+
);
172+
}
150173
}

0 commit comments

Comments
 (0)