diff --git a/packages/router/src/HandleRouteExceptionMiddleware.php b/packages/router/src/HandleRouteExceptionMiddleware.php index 634682f64..16c310d8a 100644 --- a/packages/router/src/HandleRouteExceptionMiddleware.php +++ b/packages/router/src/HandleRouteExceptionMiddleware.php @@ -9,7 +9,7 @@ use Tempest\Router\Exceptions\RouteBindingFailed; use Tempest\Support\Priority; -#[Priority(Priority::FRAMEWORK - 10)] +#[Priority(Priority::FRAMEWORK - 30)] final readonly class HandleRouteExceptionMiddleware implements HttpMiddleware { public function __invoke(Request $request, HttpMiddlewareCallable $next): Response diff --git a/packages/router/src/MatchRouteMiddleware.php b/packages/router/src/MatchRouteMiddleware.php index acbfc4586..87050102c 100644 --- a/packages/router/src/MatchRouteMiddleware.php +++ b/packages/router/src/MatchRouteMiddleware.php @@ -4,7 +4,6 @@ use Tempest\Container\Container; use Tempest\Http\GenericRequest; -use Tempest\Http\Mappers\RequestToObjectMapper; use Tempest\Http\Method; use Tempest\Http\Request; use Tempest\Http\Response; @@ -12,9 +11,7 @@ use Tempest\Router\Routing\Matching\RouteMatcher; use Tempest\Support\Priority; -use function Tempest\Mapper\map; - -#[Priority(Priority::FRAMEWORK - 9)] +#[Priority(Priority::FRAMEWORK - 29)] final readonly class MatchRouteMiddleware implements HttpMiddleware { public function __construct( @@ -37,38 +34,6 @@ public function __invoke(Request $request, HttpMiddlewareCallable $next): Respon // We register the matched route in the container, some internal framework components will need it $this->container->singleton(MatchedRoute::class, fn () => $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); - return $next($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; - - // 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)) { - continue; - } - - $requestClass = $parameter->getType()->getName(); - - break; - } - - if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) { - return map($request)->with(RequestToObjectMapper::class)->to($requestClass); - } - - return $request; - } } diff --git a/packages/router/src/ResolveRouteRequestMiddleware.php b/packages/router/src/ResolveRouteRequestMiddleware.php new file mode 100644 index 000000000..ef66a647c --- /dev/null +++ b/packages/router/src/ResolveRouteRequestMiddleware.php @@ -0,0 +1,62 @@ +resolveRequest($request); + + // 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); + + return $next($request); + } + + private function resolveRequest(Request $request): 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 ($this->matchedRoute + ->route + ->handler + ->getParameters() as $parameter) { + // If the parameter's type is an instance of Request… + if (! $parameter->getType()->matches(Request::class)) { + continue; + } + + $requestClass = $parameter->getType()->getName(); + + break; + } + + if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) { + return map($request)->with(RequestToObjectMapper::class)->to($requestClass); + } + + return $request; + } +} diff --git a/packages/router/src/RouteConfig.php b/packages/router/src/RouteConfig.php index d2bf3f291..ed70c9b6b 100644 --- a/packages/router/src/RouteConfig.php +++ b/packages/router/src/RouteConfig.php @@ -31,6 +31,7 @@ public function __construct( public Middleware $middleware = new Middleware( HandleRouteExceptionMiddleware::class, MatchRouteMiddleware::class, + ResolveRouteRequestMiddleware::class, SetCookieHeadersMiddleware::class, HandleRouteSpecificMiddleware::class, ), diff --git a/packages/router/src/Stateless.php b/packages/router/src/Stateless.php index 9f1c95d22..12762e8cf 100644 --- a/packages/router/src/Stateless.php +++ b/packages/router/src/Stateless.php @@ -4,6 +4,7 @@ use Attribute; use Tempest\Http\Session\ManageSessionMiddleware; +use Tempest\Http\Session\TrackPreviousUrlMiddleware; /** * Mark a route handler as stateless, causing all cookie and session-related middleware to be skipped. @@ -17,6 +18,7 @@ public function decorate(Route $route): Route ...$route->without, PreventCrossSiteRequestsMiddleware::class, ManageSessionMiddleware::class, + TrackPreviousUrlMiddleware::class, SetCookieHeadersMiddleware::class, ]; diff --git a/tests/Integration/Route/RouterTest.php b/tests/Integration/Route/RouterTest.php index 507b6ab4b..29bf277a2 100644 --- a/tests/Integration/Route/RouterTest.php +++ b/tests/Integration/Route/RouterTest.php @@ -7,9 +7,13 @@ use Laminas\Diactoros\ServerRequest; use Laminas\Diactoros\Stream; use Laminas\Diactoros\Uri; +use Tempest\Clock\Clock; use Tempest\Database\Migrations\CreateMigrationsTable; use Tempest\Http\ContentType; use Tempest\Http\Responses\Ok; +use Tempest\Http\Session\Session; +use Tempest\Http\Session\SessionId; +use Tempest\Http\Session\SessionManager; use Tempest\Http\Status; use Tempest\Router\GenericRouter; use Tempest\Router\RouteConfig; @@ -260,6 +264,20 @@ public function test_stateless_decorator(): void ->assertDoesNotHaveCookie('tempest_session_id'); } + public function test_stateless_decorator_does_not_manage_session(): void + { + $sessionManager = new RouteTestingSessionManager($this->container->get(Clock::class)); + + $this->container->singleton(SessionManager::class, fn () => $sessionManager); + + $this->http + ->get('/stateless') + ->assertOk(); + + $this->assertSame(0, $sessionManager->createdSessions); + $this->assertSame(0, $sessionManager->savedSessions); + } + public function test_prefix_decorator(): void { $this->http @@ -327,3 +345,41 @@ public function test_multiple_optional_parameters(): void ->assertSee('Post 789 in category tech'); } } + +final class RouteTestingSessionManager implements SessionManager +{ + public int $createdSessions = 0; + + public int $savedSessions = 0; + + public function __construct( + private readonly Clock $clock, + ) {} + + public function getOrCreate(SessionId $id): Session + { + $this->createdSessions++; + + $now = $this->clock->now(); + + return new Session( + id: $id, + createdAt: $now, + lastActiveAt: $now, + ); + } + + public function save(Session $session): void + { + $this->savedSessions++; + } + + public function delete(Session $session): void {} + + public function isValid(Session $session): bool + { + return true; + } + + public function deleteExpiredSessions(): void {} +}