Skip to content

Commit 314e412

Browse files
committed
wip
1 parent 9b802b8 commit 314e412

File tree

7 files changed

+183
-69
lines changed

7 files changed

+183
-69
lines changed
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 & 69 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;
@@ -49,36 +42,28 @@ public function dispatch(Request|PsrRequest $request): Response
4942

5043
private function processRequest(Request|PsrRequest $request): Response
5144
{
52-
if (! ($request instanceof PsrRequest)) {
53-
$request = map($request)->with(RequestToPsrRequestMapper::class)->do();
45+
if (! ($request instanceof Request)) {
46+
$request = map($request)->with(PsrRequestToGenericRequestMapper::class)->do();
5447
}
5548

56-
$matchedRoute = $this->routeMatcher->match($request);
49+
$callable = $this->getCallable();
5750

58-
if ($matchedRoute === null) {
59-
return new NotFound();
60-
}
61-
62-
$this->container->singleton(MatchedRoute::class, fn () => $matchedRoute);
63-
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;
51+
return $callable($request);
7552
}
7653

77-
private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable
54+
private function getCallable(): HttpMiddlewareCallable
7855
{
79-
$route = $matchedRoute->route;
56+
$callControllerAction = function (Request $_) {
57+
$matchedRoute = $this->container->get(MatchedRoute::class);
58+
59+
if ($matchedRoute === null) {
60+
// At this point, the `MatchRouteMiddleware` should have run.
61+
// If that's not the case, then someone messed up by clearing all HTTP middleware
62+
throw new NoMatchedRoute();
63+
}
64+
65+
$route = $matchedRoute->route;
8066

81-
$callControllerAction = function (Request $_) use ($route, $matchedRoute) {
8267
$response = $this->container->invoke(
8368
$route->handler,
8469
...$matchedRoute->params,
@@ -93,10 +78,7 @@ private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable
9378

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

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

10183
foreach ($middlewareStack->unwrap() as $middlewareClass) {
10284
$callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) {
@@ -194,38 +176,4 @@ private function processResponse(Response $response): Response
194176

195177
return $response;
196178
}
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-
}
231179
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Tempest\Router;
4+
5+
use Tempest\Core\Priority;
6+
use Tempest\Http\Request;
7+
use Tempest\Http\Response;
8+
use Tempest\Http\Responses\Invalid;
9+
use Tempest\Http\Responses\NotFound;
10+
use Tempest\Router\Exceptions\NotFoundException;
11+
use Tempest\Validation\Exceptions\ValidationException;
12+
13+
#[Priority(Priority::FRAMEWORK - 9)]
14+
final class HandleRouteExceptionMiddleware implements HttpMiddleware
15+
{
16+
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
17+
{
18+
try {
19+
return $next($request);
20+
} catch (NotFoundException) {
21+
return new NotFound();
22+
} catch (ValidationException $validationException) {
23+
return new Invalid($validationException->subject, $validationException->failingRules);
24+
}
25+
}
26+
}
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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Tempest\Router;
4+
5+
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
6+
use Tempest\Container\Container;
7+
use Tempest\Core\Priority;
8+
use Tempest\Http\GenericRequest;
9+
use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper;
10+
use Tempest\Http\Mappers\RequestToObjectMapper;
11+
use Tempest\Http\Mappers\RequestToPsrRequestMapper;
12+
use Tempest\Http\Request;
13+
use Tempest\Http\Response;
14+
use Tempest\Http\Responses\NotFound;
15+
use Tempest\Router\Routing\Matching\RouteMatcher;
16+
use function Tempest\map;
17+
18+
#[Priority(Priority::FRAMEWORK - 10)]
19+
final class MatchRouteMiddleware implements HttpMiddleware
20+
{
21+
public function __construct(
22+
private RouteMatcher $routeMatcher,
23+
private Container $container,
24+
) {}
25+
26+
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
27+
{
28+
$psrRequest = map($request)->with(RequestToPsrRequestMapper::class)->do();
29+
30+
$matchedRoute = $this->routeMatcher->match($psrRequest);
31+
32+
if ($matchedRoute === null) {
33+
return new NotFound();
34+
}
35+
36+
$this->container->singleton(MatchedRoute::class, fn () => $matchedRoute);
37+
38+
$request = $this->resolveRequest($psrRequest, $matchedRoute);
39+
40+
return $next($request);
41+
}
42+
43+
private function resolveRequest(PsrRequest $psrRequest, MatchedRoute $matchedRoute): Request
44+
{
45+
// Let's find out if our input request data matches what the route's action needs
46+
$requestClass = GenericRequest::class;
47+
48+
// We'll loop over all the handler's parameters
49+
foreach ($matchedRoute->route->handler->getParameters() as $parameter) {
50+
// If the parameter's type is an instance of Request…
51+
if ($parameter->getType()->matches(Request::class)) {
52+
// We'll use that specific request class
53+
$requestClass = $parameter->getType()->getName();
54+
55+
break;
56+
}
57+
}
58+
59+
// We map the original request we got into this method to the right request class
60+
/** @var \Tempest\Http\GenericRequest $request */
61+
$request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do();
62+
63+
if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) {
64+
$request = map($request)->with(RequestToObjectMapper::class)->to($requestClass);
65+
}
66+
67+
// Next, we register this newly created request object in the container
68+
// This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class
69+
// Making it so that we don't need to set any $_SERVER variables and stuff like that
70+
$this->container->singleton(Request::class, fn () => $request);
71+
$this->container->singleton($request::class, fn () => $request);
72+
73+
return $request;
74+
}
75+
}

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+
MatchRouteMiddleware::class,
27+
HandleRouteExceptionMiddleware::class,
2628
SetCookieMiddleware::class,
29+
HandleRouteSpecificMiddleware::class,
2730
),
2831

2932
public bool $throwHttpExceptions = true,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Route;
4+
5+
use Tests\Tempest\Integration\FrameworkIntegrationTestCase;
6+
7+
final class NotFoundTest extends FrameworkIntegrationTestCase
8+
{
9+
public function test_unmatched_route_returns_not_found(): void
10+
{
11+
$this->http->get('unknown-route')->assertNotFound();
12+
}
13+
}

0 commit comments

Comments
 (0)