Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/router/src/HandleRouteExceptionMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 1 addition & 36 deletions packages/router/src/MatchRouteMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,14 @@

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;
use Tempest\Http\Responses\NotFound;
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(
Expand All @@ -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;
}
}
62 changes: 62 additions & 0 deletions packages/router/src/ResolveRouteRequestMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

declare(strict_types=1);

namespace Tempest\Router;

use Tempest\Container\Container;
use Tempest\Http\GenericRequest;
use Tempest\Http\Mappers\RequestToObjectMapper;
use Tempest\Http\Request;
use Tempest\Http\Response;
use Tempest\Support\Priority;

use function Tempest\Mapper\map;

#[Priority(Priority::FRAMEWORK - 9)]
final readonly class ResolveRouteRequestMiddleware implements HttpMiddleware
{
public function __construct(
private MatchedRoute $matchedRoute,
private Container $container,
) {}

public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
{
$request = $this->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;
}
}
1 change: 1 addition & 0 deletions packages/router/src/RouteConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function __construct(
public Middleware $middleware = new Middleware(
HandleRouteExceptionMiddleware::class,
MatchRouteMiddleware::class,
ResolveRouteRequestMiddleware::class,
SetCookieHeadersMiddleware::class,
HandleRouteSpecificMiddleware::class,
),
Expand Down
2 changes: 2 additions & 0 deletions packages/router/src/Stateless.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -17,6 +18,7 @@ public function decorate(Route $route): Route
...$route->without,
PreventCrossSiteRequestsMiddleware::class,
ManageSessionMiddleware::class,
TrackPreviousUrlMiddleware::class,
SetCookieHeadersMiddleware::class,
];

Expand Down
56 changes: 56 additions & 0 deletions tests/Integration/Route/RouterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {}
}
Loading