Skip to content

Commit c17719b

Browse files
brendtinnocenzi
andauthored
refactor(router): refactor router logic to middleware (#1307)
Co-authored-by: Enzo Innocenzi <[email protected]>
1 parent 9b802b8 commit c17719b

File tree

16 files changed

+251
-141
lines changed

16 files changed

+251
-141
lines changed

packages/core/src/Priority.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#[Attribute(Attribute::TARGET_CLASS)]
88
final readonly class Priority
99
{
10+
public const int EXCEPTION_HANDLING = -100;
11+
1012
public const int FRAMEWORK = -1;
1113

1214
public const int HIGHEST = 0;

packages/http/src/HttpException.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ public function __construct(
1414
public readonly Status $status,
1515
?string $message = null,
1616
public readonly ?Response $cause = null,
17-
public ?Response $response = null,
1817
?\Throwable $previous = null,
1918
) {
2019
parent::__construct($message ?: '', $status->value, $previous);

packages/router/src/Exceptions/HttpErrorResponseProcessor.php

Lines changed: 0 additions & 43 deletions
This file was deleted.

packages/router/src/Exceptions/HttpExceptionHandler.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,6 @@ public function handle(Throwable $throwable): void
4545

4646
private function renderErrorResponse(Status $status, ?HttpException $exception = null): Response
4747
{
48-
if ($exception?->response) {
49-
return $exception->response;
50-
}
51-
5248
return new GenericResponse(
5349
status: $status,
5450
body: new GenericView(__DIR__ . '/HttpErrorResponse/error.view.php', [
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tempest\Router\Exceptions;
4+
5+
use Exception;
6+
7+
final class NoMatchedRoute extends Exception
8+
{
9+
public function __construct()
10+
{
11+
parent::__construct('No matched route was registered in the container. Did you remove `\Tempest\Router\MatchRouteMiddleware` from the middleware stack by any chance?');
12+
}
13+
}

packages/router/src/GenericRouter.php

Lines changed: 17 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,16 @@
88
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
99
use Tempest\Container\Container;
1010
use Tempest\Core\AppConfig;
11-
use Tempest\Http\GenericRequest;
1211
use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper;
13-
use Tempest\Http\Mappers\RequestToObjectMapper;
14-
use Tempest\Http\Mappers\RequestToPsrRequestMapper;
1512
use Tempest\Http\Request;
1613
use Tempest\Http\Response;
17-
use Tempest\Http\Responses\Invalid;
18-
use Tempest\Http\Responses\NotFound;
1914
use Tempest\Http\Responses\Ok;
20-
use Tempest\Mapper\ObjectFactory;
2115
use Tempest\Reflection\ClassReflector;
2216
use Tempest\Router\Exceptions\ControllerActionHasNoReturn;
2317
use Tempest\Router\Exceptions\InvalidRouteException;
24-
use Tempest\Router\Exceptions\NotFoundException;
18+
use Tempest\Router\Exceptions\NoMatchedRoute;
2519
use Tempest\Router\Routing\Construction\DiscoveredRoute;
2620
use Tempest\Router\Routing\Matching\RouteMatcher;
27-
use Tempest\Validation\Exceptions\ValidationException;
2821
use Tempest\View\View;
2922

3023
use function Tempest\map;
@@ -42,43 +35,28 @@ public function __construct(
4235

4336
public function dispatch(Request|PsrRequest $request): Response
4437
{
45-
return $this->processResponse(
46-
$this->processRequest($request),
47-
);
48-
}
49-
50-
private function processRequest(Request|PsrRequest $request): Response
51-
{
52-
if (! ($request instanceof PsrRequest)) {
53-
$request = map($request)->with(RequestToPsrRequestMapper::class)->do();
54-
}
55-
56-
$matchedRoute = $this->routeMatcher->match($request);
57-
58-
if ($matchedRoute === null) {
59-
return new NotFound();
38+
if (! ($request instanceof Request)) {
39+
$request = map($request)->with(PsrRequestToGenericRequestMapper::class)->do();
6040
}
6141

62-
$this->container->singleton(MatchedRoute::class, fn () => $matchedRoute);
42+
$callable = $this->getCallable();
6343

64-
try {
65-
$callable = $this->getCallable($matchedRoute);
66-
$request = $this->resolveRequest($request, $matchedRoute);
67-
$response = $callable($request);
68-
} catch (NotFoundException) {
69-
return new NotFound();
70-
} catch (ValidationException $validationException) {
71-
return new Invalid($validationException->subject, $validationException->failingRules);
72-
}
73-
74-
return $response;
44+
return $this->processResponse($callable($request));
7545
}
7646

77-
private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable
47+
private function getCallable(): HttpMiddlewareCallable
7848
{
79-
$route = $matchedRoute->route;
49+
$callControllerAction = function (Request $_) {
50+
$matchedRoute = $this->container->get(MatchedRoute::class);
51+
52+
if ($matchedRoute === null) {
53+
// At this point, the `MatchRouteMiddleware` should have run.
54+
// If that's not the case, then someone messed up by clearing all HTTP middleware
55+
throw new NoMatchedRoute();
56+
}
57+
58+
$route = $matchedRoute->route;
8059

81-
$callControllerAction = function (Request $_) use ($route, $matchedRoute) {
8260
$response = $this->container->invoke(
8361
$route->handler,
8462
...$matchedRoute->params,
@@ -93,10 +71,7 @@ private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable
9371

9472
$callable = new HttpMiddlewareCallable(fn (Request $request) => $this->createResponse($callControllerAction($request)));
9573

96-
$middlewareStack = $this->routeConfig
97-
->middleware
98-
->clone()
99-
->add(...$route->middleware);
74+
$middlewareStack = $this->routeConfig->middleware;
10075

10176
foreach ($middlewareStack->unwrap() as $middlewareClass) {
10277
$callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) {
@@ -194,38 +169,4 @@ private function processResponse(Response $response): Response
194169

195170
return $response;
196171
}
197-
198-
// TODO: could in theory be moved to a dynamic initializer
199-
private function resolveRequest(PsrRequest|ObjectFactory $psrRequest, MatchedRoute $matchedRoute): Request
200-
{
201-
// Let's find out if our input request data matches what the route's action needs
202-
$requestClass = GenericRequest::class;
203-
204-
// We'll loop over all the handler's parameters
205-
foreach ($matchedRoute->route->handler->getParameters() as $parameter) {
206-
// If the parameter's type is an instance of Request…
207-
if ($parameter->getType()->matches(Request::class)) {
208-
// We'll use that specific request class
209-
$requestClass = $parameter->getType()->getName();
210-
211-
break;
212-
}
213-
}
214-
215-
// We map the original request we got into this method to the right request class
216-
/** @var \Tempest\Http\GenericRequest $request */
217-
$request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do();
218-
219-
if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) {
220-
$request = map($request)->with(RequestToObjectMapper::class)->to($requestClass);
221-
}
222-
223-
// Next, we register this newly created request object in the container
224-
// This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class
225-
// Making it so that we don't need to set any $_SERVER variables and stuff like that
226-
$this->container->singleton(Request::class, fn () => $request);
227-
$this->container->singleton($request::class, fn () => $request);
228-
229-
return $request;
230-
}
231172
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace Tempest\Router;
4+
5+
use Tempest\Core\Priority;
6+
use Tempest\Http\HttpException;
7+
use Tempest\Http\Request;
8+
use Tempest\Http\Response;
9+
use Tempest\Http\Responses\Invalid;
10+
use Tempest\Http\Responses\NotFound;
11+
use Tempest\Router\Exceptions\NotFoundException;
12+
use Tempest\Validation\Exceptions\ValidationException;
13+
14+
#[Priority(Priority::FRAMEWORK - 10)]
15+
final readonly class HandleRouteExceptionMiddleware implements HttpMiddleware
16+
{
17+
public function __construct(
18+
private RouteConfig $routeConfig,
19+
) {}
20+
21+
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
22+
{
23+
if ($this->routeConfig->throwHttpExceptions === true) {
24+
$response = $next($request);
25+
26+
if ($response->status->isServerError() || $response->status->isClientError()) {
27+
throw new HttpException(
28+
status: $response->status,
29+
cause: $response,
30+
);
31+
}
32+
33+
return $response;
34+
}
35+
36+
try {
37+
return $next($request);
38+
} catch (NotFoundException) {
39+
return new NotFound();
40+
} catch (ValidationException $validationException) {
41+
return new Invalid($validationException->subject, $validationException->failingRules);
42+
}
43+
}
44+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
namespace Tempest\Router;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\Core\Middleware;
7+
use Tempest\Core\Priority;
8+
use Tempest\Http\Request;
9+
use Tempest\Http\Response;
10+
11+
#[Priority(Priority::LOWEST)]
12+
final readonly class HandleRouteSpecificMiddleware implements HttpMiddleware
13+
{
14+
public function __construct(
15+
private MatchedRoute $matchedRoute,
16+
private Container $container,
17+
) {}
18+
19+
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
20+
{
21+
$middlewareStack = new Middleware(...$this->matchedRoute->route->middleware);
22+
23+
$callable = new HttpMiddlewareCallable(fn (Request $request) => $next($request));
24+
25+
foreach ($middlewareStack->unwrap() as $middlewareClass) {
26+
$callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) {
27+
/** @var HttpMiddleware $middleware */
28+
$middleware = $this->container->get($middlewareClass->getName());
29+
30+
return $middleware($request, $callable);
31+
});
32+
}
33+
34+
return $callable($request);
35+
}
36+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
namespace Tempest\Router;
4+
5+
use Tempest\Container\Container;
6+
use Tempest\Core\Priority;
7+
use Tempest\Http\GenericRequest;
8+
use Tempest\Http\Mappers\RequestToObjectMapper;
9+
use Tempest\Http\Request;
10+
use Tempest\Http\Response;
11+
use Tempest\Http\Responses\NotFound;
12+
use Tempest\Router\Routing\Matching\RouteMatcher;
13+
14+
use function Tempest\map;
15+
16+
#[Priority(Priority::FRAMEWORK - 9)]
17+
final readonly class MatchRouteMiddleware implements HttpMiddleware
18+
{
19+
public function __construct(
20+
private RouteMatcher $routeMatcher,
21+
private Container $container,
22+
) {}
23+
24+
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
25+
{
26+
$matchedRoute = $this->routeMatcher->match($request);
27+
28+
if ($matchedRoute === null) {
29+
return new NotFound();
30+
}
31+
32+
// We register the matched route in the container, some internal framework components will need it
33+
$this->container->singleton(MatchedRoute::class, fn () => $matchedRoute);
34+
35+
// Convert the request to a specific request implementation, if needed
36+
$request = $this->resolveRequest($request, $matchedRoute);
37+
38+
// We register this newly created request object in the container
39+
// This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class
40+
// Making it so that we don't need to set any $_SERVER variables and stuff like that
41+
$this->container->singleton(Request::class, fn () => $request);
42+
$this->container->singleton($request::class, fn () => $request);
43+
44+
return $next($request);
45+
}
46+
47+
private function resolveRequest(Request $request, MatchedRoute $matchedRoute): Request
48+
{
49+
// Let's find out if our input request data matches what the route's action needs
50+
$requestClass = GenericRequest::class;
51+
52+
// We'll loop over all the handler's parameters
53+
foreach ($matchedRoute->route->handler->getParameters() as $parameter) {
54+
// If the parameter's type is an instance of Request…
55+
if ($parameter->getType()->matches(Request::class)) {
56+
// We'll use that specific request class
57+
$requestClass = $parameter->getType()->getName();
58+
59+
break;
60+
}
61+
}
62+
63+
if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) {
64+
$request = map($request)->with(RequestToObjectMapper::class)->to($requestClass);
65+
}
66+
67+
return $request;
68+
}
69+
}

packages/router/src/RouteConfig.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ public function __construct(
2323

2424
/** @var Middleware<\Tempest\Router\HttpMiddleware> */
2525
public Middleware $middleware = new Middleware(
26+
HandleRouteExceptionMiddleware::class,
27+
MatchRouteMiddleware::class,
2628
SetCookieMiddleware::class,
29+
HandleRouteSpecificMiddleware::class,
2730
),
2831

2932
public bool $throwHttpExceptions = true,

0 commit comments

Comments
 (0)