From 314e4129b0233124d6776971237ff4e069c23421 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 10:19:09 +0200 Subject: [PATCH 01/10] wip --- .../router/src/Exceptions/NoMatchedRoute.php | 13 +++ packages/router/src/GenericRouter.php | 86 ++++--------------- .../src/HandleRouteExceptionMiddleware.php | 26 ++++++ .../src/HandleRouteSpecificMiddleware.php | 36 ++++++++ packages/router/src/MatchRouteMiddleware.php | 75 ++++++++++++++++ packages/router/src/RouteConfig.php | 3 + tests/Integration/Route/NotFoundTest.php | 13 +++ 7 files changed, 183 insertions(+), 69 deletions(-) create mode 100644 packages/router/src/Exceptions/NoMatchedRoute.php create mode 100644 packages/router/src/HandleRouteExceptionMiddleware.php create mode 100644 packages/router/src/HandleRouteSpecificMiddleware.php create mode 100644 packages/router/src/MatchRouteMiddleware.php create mode 100644 tests/Integration/Route/NotFoundTest.php diff --git a/packages/router/src/Exceptions/NoMatchedRoute.php b/packages/router/src/Exceptions/NoMatchedRoute.php new file mode 100644 index 000000000..573928ecb --- /dev/null +++ b/packages/router/src/Exceptions/NoMatchedRoute.php @@ -0,0 +1,13 @@ +with(RequestToPsrRequestMapper::class)->do(); + if (! ($request instanceof Request)) { + $request = map($request)->with(PsrRequestToGenericRequestMapper::class)->do(); } - $matchedRoute = $this->routeMatcher->match($request); + $callable = $this->getCallable(); - if ($matchedRoute === null) { - return new NotFound(); - } - - $this->container->singleton(MatchedRoute::class, fn () => $matchedRoute); - - try { - $callable = $this->getCallable($matchedRoute); - $request = $this->resolveRequest($request, $matchedRoute); - $response = $callable($request); - } catch (NotFoundException) { - return new NotFound(); - } catch (ValidationException $validationException) { - return new Invalid($validationException->subject, $validationException->failingRules); - } - - return $response; + return $callable($request); } - private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable + private function getCallable(): HttpMiddlewareCallable { - $route = $matchedRoute->route; + $callControllerAction = function (Request $_) { + $matchedRoute = $this->container->get(MatchedRoute::class); + + if ($matchedRoute === null) { + // At this point, the `MatchRouteMiddleware` should have run. + // If that's not the case, then someone messed up by clearing all HTTP middleware + throw new NoMatchedRoute(); + } + + $route = $matchedRoute->route; - $callControllerAction = function (Request $_) use ($route, $matchedRoute) { $response = $this->container->invoke( $route->handler, ...$matchedRoute->params, @@ -93,10 +78,7 @@ private function getCallable(MatchedRoute $matchedRoute): HttpMiddlewareCallable $callable = new HttpMiddlewareCallable(fn (Request $request) => $this->createResponse($callControllerAction($request))); - $middlewareStack = $this->routeConfig - ->middleware - ->clone() - ->add(...$route->middleware); + $middlewareStack = $this->routeConfig->middleware; foreach ($middlewareStack->unwrap() as $middlewareClass) { $callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) { @@ -194,38 +176,4 @@ private function processResponse(Response $response): Response return $response; } - - // TODO: could in theory be moved to a dynamic initializer - private function resolveRequest(PsrRequest|ObjectFactory $psrRequest, MatchedRoute $matchedRoute): Request - { - // Let's find out if our input request data matches what the route's action needs - $requestClass = GenericRequest::class; - - // We'll loop over all the handler's parameters - foreach ($matchedRoute->route->handler->getParameters() as $parameter) { - // If the parameter's type is an instance of Request… - if ($parameter->getType()->matches(Request::class)) { - // We'll use that specific request class - $requestClass = $parameter->getType()->getName(); - - break; - } - } - - // We map the original request we got into this method to the right request class - /** @var \Tempest\Http\GenericRequest $request */ - $request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do(); - - if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) { - $request = map($request)->with(RequestToObjectMapper::class)->to($requestClass); - } - - // Next, we register this newly created request object in the container - // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class - // Making it so that we don't need to set any $_SERVER variables and stuff like that - $this->container->singleton(Request::class, fn () => $request); - $this->container->singleton($request::class, fn () => $request); - - return $request; - } } diff --git a/packages/router/src/HandleRouteExceptionMiddleware.php b/packages/router/src/HandleRouteExceptionMiddleware.php new file mode 100644 index 000000000..029a4f743 --- /dev/null +++ b/packages/router/src/HandleRouteExceptionMiddleware.php @@ -0,0 +1,26 @@ +subject, $validationException->failingRules); + } + } +} \ No newline at end of file diff --git a/packages/router/src/HandleRouteSpecificMiddleware.php b/packages/router/src/HandleRouteSpecificMiddleware.php new file mode 100644 index 000000000..8d04c7cc8 --- /dev/null +++ b/packages/router/src/HandleRouteSpecificMiddleware.php @@ -0,0 +1,36 @@ +matchedRoute->route->middleware); + + $callable = new HttpMiddlewareCallable(fn (Request $request) => $next($request)); + + foreach ($middlewareStack->unwrap() as $middlewareClass) { + $callable = new HttpMiddlewareCallable(function (Request $request) use ($middlewareClass, $callable) { + /** @var HttpMiddleware $middleware */ + $middleware = $this->container->get($middlewareClass->getName()); + + return $middleware($request, $callable); + }); + } + + return $callable($request); + } +} \ No newline at end of file diff --git a/packages/router/src/MatchRouteMiddleware.php b/packages/router/src/MatchRouteMiddleware.php new file mode 100644 index 000000000..19951a321 --- /dev/null +++ b/packages/router/src/MatchRouteMiddleware.php @@ -0,0 +1,75 @@ +with(RequestToPsrRequestMapper::class)->do(); + + $matchedRoute = $this->routeMatcher->match($psrRequest); + + if ($matchedRoute === null) { + return new NotFound(); + } + + $this->container->singleton(MatchedRoute::class, fn () => $matchedRoute); + + $request = $this->resolveRequest($psrRequest, $matchedRoute); + + return $next($request); + } + + private function resolveRequest(PsrRequest $psrRequest, MatchedRoute $matchedRoute): Request + { + // Let's find out if our input request data matches what the route's action needs + $requestClass = GenericRequest::class; + + // We'll loop over all the handler's parameters + foreach ($matchedRoute->route->handler->getParameters() as $parameter) { + // If the parameter's type is an instance of Request… + if ($parameter->getType()->matches(Request::class)) { + // We'll use that specific request class + $requestClass = $parameter->getType()->getName(); + + break; + } + } + + // We map the original request we got into this method to the right request class + /** @var \Tempest\Http\GenericRequest $request */ + $request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do(); + + if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) { + $request = map($request)->with(RequestToObjectMapper::class)->to($requestClass); + } + + // Next, we register this newly created request object in the container + // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class + // Making it so that we don't need to set any $_SERVER variables and stuff like that + $this->container->singleton(Request::class, fn () => $request); + $this->container->singleton($request::class, fn () => $request); + + return $request; + } +} \ No newline at end of file diff --git a/packages/router/src/RouteConfig.php b/packages/router/src/RouteConfig.php index 349ab8f74..9be5bad4f 100644 --- a/packages/router/src/RouteConfig.php +++ b/packages/router/src/RouteConfig.php @@ -23,7 +23,10 @@ public function __construct( /** @var Middleware<\Tempest\Router\HttpMiddleware> */ public Middleware $middleware = new Middleware( + MatchRouteMiddleware::class, + HandleRouteExceptionMiddleware::class, SetCookieMiddleware::class, + HandleRouteSpecificMiddleware::class, ), public bool $throwHttpExceptions = true, diff --git a/tests/Integration/Route/NotFoundTest.php b/tests/Integration/Route/NotFoundTest.php new file mode 100644 index 000000000..8c2c24a68 --- /dev/null +++ b/tests/Integration/Route/NotFoundTest.php @@ -0,0 +1,13 @@ +http->get('unknown-route')->assertNotFound(); + } +} \ No newline at end of file From 8b26c1cd74c898e463eb2838fba0572712c6f4d7 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 10:28:36 +0200 Subject: [PATCH 02/10] wip --- .../src/HandleRouteExceptionMiddleware.php | 3 ++ packages/router/src/MatchRouteMiddleware.php | 28 ++++++++----------- .../Routing/Matching/GenericRouteMatcher.php | 17 ++++++----- .../src/Routing/Matching/RouteMatcher.php | 4 +-- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/packages/router/src/HandleRouteExceptionMiddleware.php b/packages/router/src/HandleRouteExceptionMiddleware.php index 029a4f743..39224e98b 100644 --- a/packages/router/src/HandleRouteExceptionMiddleware.php +++ b/packages/router/src/HandleRouteExceptionMiddleware.php @@ -3,6 +3,7 @@ namespace Tempest\Router; use Tempest\Core\Priority; +use Tempest\Http\HttpException; use Tempest\Http\Request; use Tempest\Http\Response; use Tempest\Http\Responses\Invalid; @@ -21,6 +22,8 @@ public function __invoke(Request $request, HttpMiddlewareCallable $next): Respon return new NotFound(); } catch (ValidationException $validationException) { return new Invalid($validationException->subject, $validationException->failingRules); +// } catch(HttpException $httpException) { + // TODO? } } } \ No newline at end of file diff --git a/packages/router/src/MatchRouteMiddleware.php b/packages/router/src/MatchRouteMiddleware.php index 19951a321..cc0eee181 100644 --- a/packages/router/src/MatchRouteMiddleware.php +++ b/packages/router/src/MatchRouteMiddleware.php @@ -16,7 +16,7 @@ use function Tempest\map; #[Priority(Priority::FRAMEWORK - 10)] -final class MatchRouteMiddleware implements HttpMiddleware +final readonly class MatchRouteMiddleware implements HttpMiddleware { public function __construct( private RouteMatcher $routeMatcher, @@ -25,22 +25,28 @@ public function __construct( public function __invoke(Request $request, HttpMiddlewareCallable $next): Response { - $psrRequest = map($request)->with(RequestToPsrRequestMapper::class)->do(); - - $matchedRoute = $this->routeMatcher->match($psrRequest); + $matchedRoute = $this->routeMatcher->match($request); if ($matchedRoute === null) { return new NotFound(); } + // We register the matched route in the container, some internal framework components will need it $this->container->singleton(MatchedRoute::class, fn () => $matchedRoute); - $request = $this->resolveRequest($psrRequest, $matchedRoute); + // Convert the request to a specific request implementation, if needed + $request = $this->resolveRequest($request, $matchedRoute); + + // We register this newly created request object in the container + // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class + // Making it so that we don't need to set any $_SERVER variables and stuff like that + $this->container->singleton(Request::class, fn () => $request); + $this->container->singleton($request::class, fn () => $request); return $next($request); } - private function resolveRequest(PsrRequest $psrRequest, MatchedRoute $matchedRoute): Request + private function resolveRequest(Request $request, MatchedRoute $matchedRoute): Request { // Let's find out if our input request data matches what the route's action needs $requestClass = GenericRequest::class; @@ -56,20 +62,10 @@ private function resolveRequest(PsrRequest $psrRequest, MatchedRoute $matchedRou } } - // We map the original request we got into this method to the right request class - /** @var \Tempest\Http\GenericRequest $request */ - $request = map($psrRequest)->with(PsrRequestToGenericRequestMapper::class)->do(); - if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) { $request = map($request)->with(RequestToObjectMapper::class)->to($requestClass); } - // Next, we register this newly created request object in the container - // This makes it so that RequestInitializer is bypassed entirely when the controller action needs the request class - // Making it so that we don't need to set any $_SERVER variables and stuff like that - $this->container->singleton(Request::class, fn () => $request); - $this->container->singleton($request::class, fn () => $request); - return $request; } } \ No newline at end of file diff --git a/packages/router/src/Routing/Matching/GenericRouteMatcher.php b/packages/router/src/Routing/Matching/GenericRouteMatcher.php index 4b73138f2..8774e16e3 100644 --- a/packages/router/src/Routing/Matching/GenericRouteMatcher.php +++ b/packages/router/src/Routing/Matching/GenericRouteMatcher.php @@ -4,9 +4,8 @@ namespace Tempest\Router\Routing\Matching; -use Psr\Http\Message\ServerRequestInterface as PsrRequest; +use Tempest\Http\Request; use Tempest\Router\Exceptions\InvalidEnumParameterException; -use Tempest\Router\Exceptions\NotFoundException; use Tempest\Router\MatchedRoute; use Tempest\Router\RouteConfig; use Tempest\Router\Routing\Construction\DiscoveredRoute; @@ -17,7 +16,7 @@ public function __construct( private RouteConfig $routeConfig, ) {} - public function match(PsrRequest $request): ?MatchedRoute + public function match(Request $request): ?MatchedRoute { // Try to match routes without any parameters if (($staticRoute = $this->matchStaticRoute($request)) !== null) { @@ -28,9 +27,9 @@ public function match(PsrRequest $request): ?MatchedRoute return $this->matchDynamicRoute($request); } - private function matchStaticRoute(PsrRequest $request): ?MatchedRoute + private function matchStaticRoute(Request $request): ?MatchedRoute { - $staticRoute = $this->routeConfig->staticRoutes[$request->getMethod()][$request->getUri()->getPath()] ?? null; + $staticRoute = $this->routeConfig->staticRoutes[$request->method->value][$request->path] ?? null; if ($staticRoute === null) { return null; @@ -39,19 +38,19 @@ private function matchStaticRoute(PsrRequest $request): ?MatchedRoute return new MatchedRoute($staticRoute, []); } - private function matchDynamicRoute(PsrRequest $request): ?MatchedRoute + private function matchDynamicRoute(Request $request): ?MatchedRoute { // If there are no routes for the given request method, we immediately stop - $routesForMethod = $this->routeConfig->dynamicRoutes[$request->getMethod()] ?? null; + $routesForMethod = $this->routeConfig->dynamicRoutes[$request->method->value] ?? null; if ($routesForMethod === null) { return null; } // Get matching regex for route - $matchingRegexForMethod = $this->routeConfig->matchingRegexes[$request->getMethod()]; + $matchingRegexForMethod = $this->routeConfig->matchingRegexes[$request->method->value]; // Then we'll use this regex to see whether we have a match or not - $matchResult = $matchingRegexForMethod->match($request->getUri()->getPath()); + $matchResult = $matchingRegexForMethod->match($request->path); if ($matchResult === null) { return null; diff --git a/packages/router/src/Routing/Matching/RouteMatcher.php b/packages/router/src/Routing/Matching/RouteMatcher.php index 89cce0537..ddaf83548 100644 --- a/packages/router/src/Routing/Matching/RouteMatcher.php +++ b/packages/router/src/Routing/Matching/RouteMatcher.php @@ -4,10 +4,10 @@ namespace Tempest\Router\Routing\Matching; -use Psr\Http\Message\ServerRequestInterface as PsrRequest; +use Tempest\Http\Request; use Tempest\Router\MatchedRoute; interface RouteMatcher { - public function match(PsrRequest $request): ?MatchedRoute; + public function match(Request $request): ?MatchedRoute; } From 0edb80cf472a85144a4e6709a813ce221158905d Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 10:38:15 +0200 Subject: [PATCH 03/10] wip --- .../router/src/Exceptions/NoMatchedRoute.php | 2 +- .../src/HandleRouteExceptionMiddleware.php | 17 +++++++++++------ .../src/HandleRouteSpecificMiddleware.php | 2 +- packages/router/src/MatchRouteMiddleware.php | 9 ++++----- packages/router/src/RouteConfig.php | 2 +- tests/Integration/Route/NotFoundTest.php | 2 +- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/router/src/Exceptions/NoMatchedRoute.php b/packages/router/src/Exceptions/NoMatchedRoute.php index 573928ecb..cdb05ec33 100644 --- a/packages/router/src/Exceptions/NoMatchedRoute.php +++ b/packages/router/src/Exceptions/NoMatchedRoute.php @@ -10,4 +10,4 @@ public function __construct() { parent::__construct('No matched route was registered in the container. Did you remove `\Tempest\Router\MatchRouteMiddleware` from the middleware stack by any chance?'); } -} \ No newline at end of file +} diff --git a/packages/router/src/HandleRouteExceptionMiddleware.php b/packages/router/src/HandleRouteExceptionMiddleware.php index 39224e98b..46423f1f7 100644 --- a/packages/router/src/HandleRouteExceptionMiddleware.php +++ b/packages/router/src/HandleRouteExceptionMiddleware.php @@ -3,7 +3,6 @@ namespace Tempest\Router; use Tempest\Core\Priority; -use Tempest\Http\HttpException; use Tempest\Http\Request; use Tempest\Http\Response; use Tempest\Http\Responses\Invalid; @@ -11,19 +10,25 @@ use Tempest\Router\Exceptions\NotFoundException; use Tempest\Validation\Exceptions\ValidationException; -#[Priority(Priority::FRAMEWORK - 9)] -final class HandleRouteExceptionMiddleware implements HttpMiddleware +#[Priority(Priority::FRAMEWORK - 10)] +final readonly class HandleRouteExceptionMiddleware implements HttpMiddleware { + public function __construct( + private RouteConfig $routeConfig, + ) {} + public function __invoke(Request $request, HttpMiddlewareCallable $next): Response { + if ($this->routeConfig->throwHttpExceptions === true) { + return $next($request); + } + try { return $next($request); } catch (NotFoundException) { return new NotFound(); } catch (ValidationException $validationException) { return new Invalid($validationException->subject, $validationException->failingRules); -// } catch(HttpException $httpException) { - // TODO? } } -} \ No newline at end of file +} diff --git a/packages/router/src/HandleRouteSpecificMiddleware.php b/packages/router/src/HandleRouteSpecificMiddleware.php index 8d04c7cc8..9e27c96da 100644 --- a/packages/router/src/HandleRouteSpecificMiddleware.php +++ b/packages/router/src/HandleRouteSpecificMiddleware.php @@ -33,4 +33,4 @@ public function __invoke(Request $request, HttpMiddlewareCallable $next): Respon return $callable($request); } -} \ No newline at end of file +} diff --git a/packages/router/src/MatchRouteMiddleware.php b/packages/router/src/MatchRouteMiddleware.php index cc0eee181..7b996a59a 100644 --- a/packages/router/src/MatchRouteMiddleware.php +++ b/packages/router/src/MatchRouteMiddleware.php @@ -6,17 +6,16 @@ use Tempest\Container\Container; use Tempest\Core\Priority; use Tempest\Http\GenericRequest; -use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper; use Tempest\Http\Mappers\RequestToObjectMapper; -use Tempest\Http\Mappers\RequestToPsrRequestMapper; use Tempest\Http\Request; use Tempest\Http\Response; use Tempest\Http\Responses\NotFound; use Tempest\Router\Routing\Matching\RouteMatcher; + use function Tempest\map; -#[Priority(Priority::FRAMEWORK - 10)] -final readonly class MatchRouteMiddleware implements HttpMiddleware +#[Priority(Priority::FRAMEWORK - 9)] +final readonly class MatchRouteMiddleware implements HttpMiddleware { public function __construct( private RouteMatcher $routeMatcher, @@ -68,4 +67,4 @@ private function resolveRequest(Request $request, MatchedRoute $matchedRoute): R return $request; } -} \ No newline at end of file +} diff --git a/packages/router/src/RouteConfig.php b/packages/router/src/RouteConfig.php index 9be5bad4f..d149ef266 100644 --- a/packages/router/src/RouteConfig.php +++ b/packages/router/src/RouteConfig.php @@ -23,8 +23,8 @@ public function __construct( /** @var Middleware<\Tempest\Router\HttpMiddleware> */ public Middleware $middleware = new Middleware( - MatchRouteMiddleware::class, HandleRouteExceptionMiddleware::class, + MatchRouteMiddleware::class, SetCookieMiddleware::class, HandleRouteSpecificMiddleware::class, ), diff --git a/tests/Integration/Route/NotFoundTest.php b/tests/Integration/Route/NotFoundTest.php index 8c2c24a68..df034827a 100644 --- a/tests/Integration/Route/NotFoundTest.php +++ b/tests/Integration/Route/NotFoundTest.php @@ -10,4 +10,4 @@ public function test_unmatched_route_returns_not_found(): void { $this->http->get('unknown-route')->assertNotFound(); } -} \ No newline at end of file +} From e36d4dbb58f9b7a00580befc44f0bcc002219795 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 10:40:21 +0200 Subject: [PATCH 04/10] wip --- .../Routing/Matching/GenericRouteMatcherTest.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/router/tests/Routing/Matching/GenericRouteMatcherTest.php b/packages/router/tests/Routing/Matching/GenericRouteMatcherTest.php index 124c40f9a..e2c626f9d 100644 --- a/packages/router/tests/Routing/Matching/GenericRouteMatcherTest.php +++ b/packages/router/tests/Routing/Matching/GenericRouteMatcherTest.php @@ -6,6 +6,7 @@ use Laminas\Diactoros\ServerRequest; use PHPUnit\Framework\TestCase; +use Tempest\Http\GenericRequest; use Tempest\Http\Method; use Tempest\Router\RouteConfig; use Tempest\Router\Routing\Matching\GenericRouteMatcher; @@ -57,7 +58,7 @@ protected function setUp(): void public function test_match_on_static_route(): void { - $request = new ServerRequest(uri: '/static', method: 'GET'); + $request = new GenericRequest(method: Method::GET, uri: '/static'); $matchedRoute = $this->subject->match($request); @@ -68,7 +69,7 @@ public function test_match_on_static_route(): void public function test_match_returns_null_on_unknown_route(): void { - $request = new ServerRequest(uri: '/non-existing', method: 'GET'); + $request = new GenericRequest(Method::GET, '/non-existing'); $matchedRoute = $this->subject->match($request); @@ -77,7 +78,7 @@ public function test_match_returns_null_on_unknown_route(): void public function test_match_returns_null_on_unconfigured_method(): void { - $request = new ServerRequest(uri: '/static', method: 'POST'); + $request = new GenericRequest(method: Method::POST, uri: '/static'); $matchedRoute = $this->subject->match($request); @@ -86,7 +87,7 @@ public function test_match_returns_null_on_unconfigured_method(): void public function test_match_on_dynamic_route(): void { - $request = new ServerRequest(uri: '/dynamic/5', method: 'GET'); + $request = new GenericRequest(method: Method::GET, uri: '/dynamic/5'); $matchedRoute = $this->subject->match($request); @@ -97,7 +98,7 @@ public function test_match_on_dynamic_route(): void public function test_match_on_dynamic_route_with_many_parameters(): void { - $request = new ServerRequest(uri: '/dynamic/5/brendt/brent/6', method: 'GET'); + $request = new GenericRequest(method: Method::GET, uri: '/dynamic/5/brendt/brent/6'); $matchedRoute = $this->subject->match($request); From cc8ffd799028509060ea05bec60d390d606315b1 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 10:54:42 +0200 Subject: [PATCH 05/10] wip --- .../Exceptions/HttpErrorResponseProcessor.php | 43 ------------------- packages/router/src/GenericRouter.php | 9 +--- .../src/HandleRouteExceptionMiddleware.php | 12 +++++- 3 files changed, 12 insertions(+), 52 deletions(-) delete mode 100644 packages/router/src/Exceptions/HttpErrorResponseProcessor.php diff --git a/packages/router/src/Exceptions/HttpErrorResponseProcessor.php b/packages/router/src/Exceptions/HttpErrorResponseProcessor.php deleted file mode 100644 index 9562431ce..000000000 --- a/packages/router/src/Exceptions/HttpErrorResponseProcessor.php +++ /dev/null @@ -1,43 +0,0 @@ -status->isServerError() && ! $response->status->isClientError()) { - return $response; - } - - // If the response already has a body, it means it is most likely - // meant to be returned as-is, so we don't have to throw an exception. - if ($response->body) { - return $response; - } - - // During tests, the router is generally configured to not throw HTTP exceptions in order - // to perform assertions on the responses. In this case, we return the response as is. - if (! $this->routeConfig->throwHttpExceptions) { - return $response; - } - - throw new HttpException( - status: $response->status, - cause: $response, - ); - } -} diff --git a/packages/router/src/GenericRouter.php b/packages/router/src/GenericRouter.php index 1917eee7b..c09472a4b 100644 --- a/packages/router/src/GenericRouter.php +++ b/packages/router/src/GenericRouter.php @@ -34,13 +34,6 @@ public function __construct( ) {} public function dispatch(Request|PsrRequest $request): Response - { - return $this->processResponse( - $this->processRequest($request), - ); - } - - private function processRequest(Request|PsrRequest $request): Response { if (! ($request instanceof Request)) { $request = map($request)->with(PsrRequestToGenericRequestMapper::class)->do(); @@ -48,7 +41,7 @@ private function processRequest(Request|PsrRequest $request): Response $callable = $this->getCallable(); - return $callable($request); + return $this->processResponse($callable($request)); } private function getCallable(): HttpMiddlewareCallable diff --git a/packages/router/src/HandleRouteExceptionMiddleware.php b/packages/router/src/HandleRouteExceptionMiddleware.php index 46423f1f7..628f3933d 100644 --- a/packages/router/src/HandleRouteExceptionMiddleware.php +++ b/packages/router/src/HandleRouteExceptionMiddleware.php @@ -3,6 +3,7 @@ namespace Tempest\Router; use Tempest\Core\Priority; +use Tempest\Http\HttpException; use Tempest\Http\Request; use Tempest\Http\Response; use Tempest\Http\Responses\Invalid; @@ -20,7 +21,16 @@ public function __construct( public function __invoke(Request $request, HttpMiddlewareCallable $next): Response { if ($this->routeConfig->throwHttpExceptions === true) { - return $next($request); + $response = $next($request); + + if ($response->status->isServerError() || $response->status->isClientError()) { + throw new HttpException( + status: $response->status, + cause: $response, + ); + } + + return $response; } try { From b3e57c85502bc768551370d06eee678b455379a5 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 11:02:54 +0200 Subject: [PATCH 06/10] wip --- .../Http/Routing/Matching/GenericRouteMatcherBench.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php b/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php index 34b8eb56e..c5a10adce 100644 --- a/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php +++ b/tests/Benchmark/Http/Routing/Matching/GenericRouteMatcherBench.php @@ -9,6 +9,8 @@ use PhpBench\Attributes\ParamProviders; use PhpBench\Attributes\Revs; use PhpBench\Attributes\Warmup; +use Tempest\Http\GenericRequest; +use Tempest\Http\Method; use Tempest\Router\RouteConfig; use Tempest\Router\Routing\Construction\RouteConfigurator; use Tempest\Router\Routing\Matching\GenericRouteMatcher; @@ -31,7 +33,7 @@ public function __construct() public function benchMatch(array $params): void { $this->matcher->match( - new ServerRequest(uri: $params['uri'], method: 'GET'), + new GenericRequest(Method::GET, $params['uri']), ); } From e012b0c5bf93da945fcd417f37fc3647fab0b820 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 11:35:40 +0200 Subject: [PATCH 07/10] wip --- packages/core/src/Priority.php | 2 ++ packages/router/src/MatchRouteMiddleware.php | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/Priority.php b/packages/core/src/Priority.php index 0bf263053..87d13a60b 100644 --- a/packages/core/src/Priority.php +++ b/packages/core/src/Priority.php @@ -7,6 +7,8 @@ #[Attribute(Attribute::TARGET_CLASS)] final readonly class Priority { + public const int EXCEPTION_HANDLING = -100; + public const int FRAMEWORK = -1; public const int HIGHEST = 0; diff --git a/packages/router/src/MatchRouteMiddleware.php b/packages/router/src/MatchRouteMiddleware.php index 7b996a59a..d596c6117 100644 --- a/packages/router/src/MatchRouteMiddleware.php +++ b/packages/router/src/MatchRouteMiddleware.php @@ -2,7 +2,6 @@ namespace Tempest\Router; -use Psr\Http\Message\ServerRequestInterface as PsrRequest; use Tempest\Container\Container; use Tempest\Core\Priority; use Tempest\Http\GenericRequest; From 1cd911b14ee3a0f1febf6d1edf6b0be7fc2f858d Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 11:45:18 +0200 Subject: [PATCH 08/10] wip --- .../Fixtures/CustomNotFoundMiddleware.php | 22 +++++++++++++++++++ tests/Integration/Route/NotFoundTest.php | 13 +++++++++++ 2 files changed, 35 insertions(+) create mode 100644 tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php diff --git a/tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php b/tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php new file mode 100644 index 000000000..c52a67511 --- /dev/null +++ b/tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php @@ -0,0 +1,22 @@ +addHeader('x-not-found', 'indeed'); + + return $response; + } +} \ No newline at end of file diff --git a/tests/Integration/Route/NotFoundTest.php b/tests/Integration/Route/NotFoundTest.php index df034827a..45a1c1ec0 100644 --- a/tests/Integration/Route/NotFoundTest.php +++ b/tests/Integration/Route/NotFoundTest.php @@ -2,12 +2,25 @@ namespace Tests\Tempest\Integration\Route; +use Tempest\Router\RouteConfig; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; +use Tests\Tempest\Integration\Route\Fixtures\CustomNotFoundMiddleware; +/** + * @property \Tempest\Framework\Testing\Http\HttpRouterTester $http + */ final class NotFoundTest extends FrameworkIntegrationTestCase { public function test_unmatched_route_returns_not_found(): void { $this->http->get('unknown-route')->assertNotFound(); } + + public function test_custom_not_found_middleware(): void + { + $routeConfig = $this->container->get(RouteConfig::class); + $routeConfig->middleware->add(CustomNotFoundMiddleware::class); + + $this->http->get('unknown-route')->assertHasHeader('x-not-found'); + } } From b6a67bed75616932100d6ae7ab84cdc6e87adcc5 Mon Sep 17 00:00:00 2001 From: brendt Date: Thu, 26 Jun 2025 11:46:16 +0200 Subject: [PATCH 09/10] wip --- tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php b/tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php index c52a67511..f33e6750c 100644 --- a/tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php +++ b/tests/Integration/Route/Fixtures/CustomNotFoundMiddleware.php @@ -19,4 +19,4 @@ public function __invoke(Request $request, HttpMiddlewareCallable $next): Respon return $response; } -} \ No newline at end of file +} From 48a861c9a428b28cdedf37a0d82f441b7ba0b37a Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Thu, 26 Jun 2025 11:48:18 +0200 Subject: [PATCH 10/10] refactor: remove `HttpException` custom responses --- packages/http/src/HttpException.php | 1 - packages/router/src/Exceptions/HttpExceptionHandler.php | 4 ---- 2 files changed, 5 deletions(-) diff --git a/packages/http/src/HttpException.php b/packages/http/src/HttpException.php index 7d29aaf27..bd15deb69 100644 --- a/packages/http/src/HttpException.php +++ b/packages/http/src/HttpException.php @@ -14,7 +14,6 @@ public function __construct( public readonly Status $status, ?string $message = null, public readonly ?Response $cause = null, - public ?Response $response = null, ?\Throwable $previous = null, ) { parent::__construct($message ?: '', $status->value, $previous); diff --git a/packages/router/src/Exceptions/HttpExceptionHandler.php b/packages/router/src/Exceptions/HttpExceptionHandler.php index f1d6191e2..37fbe9fa3 100644 --- a/packages/router/src/Exceptions/HttpExceptionHandler.php +++ b/packages/router/src/Exceptions/HttpExceptionHandler.php @@ -45,10 +45,6 @@ public function handle(Throwable $throwable): void private function renderErrorResponse(Status $status, ?HttpException $exception = null): Response { - if ($exception?->response) { - return $exception->response; - } - return new GenericResponse( status: $status, body: new GenericView(__DIR__ . '/HttpErrorResponse/error.view.php', [