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: 2 additions & 0 deletions packages/core/src/Priority.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 0 additions & 1 deletion packages/http/src/HttpException.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
43 changes: 0 additions & 43 deletions packages/router/src/Exceptions/HttpErrorResponseProcessor.php

This file was deleted.

4 changes: 0 additions & 4 deletions packages/router/src/Exceptions/HttpExceptionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down
13 changes: 13 additions & 0 deletions packages/router/src/Exceptions/NoMatchedRoute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Tempest\Router\Exceptions;

use Exception;

final class NoMatchedRoute extends Exception
{
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?');
}
}
93 changes: 17 additions & 76 deletions packages/router/src/GenericRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,16 @@
use Psr\Http\Message\ServerRequestInterface as PsrRequest;
use Tempest\Container\Container;
use Tempest\Core\AppConfig;
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\Invalid;
use Tempest\Http\Responses\NotFound;
use Tempest\Http\Responses\Ok;
use Tempest\Mapper\ObjectFactory;
use Tempest\Reflection\ClassReflector;
use Tempest\Router\Exceptions\ControllerActionHasNoReturn;
use Tempest\Router\Exceptions\InvalidRouteException;
use Tempest\Router\Exceptions\NotFoundException;
use Tempest\Router\Exceptions\NoMatchedRoute;
use Tempest\Router\Routing\Construction\DiscoveredRoute;
use Tempest\Router\Routing\Matching\RouteMatcher;
use Tempest\Validation\Exceptions\ValidationException;
use Tempest\View\View;

use function Tempest\map;
Expand All @@ -42,43 +35,28 @@ 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 PsrRequest)) {
$request = map($request)->with(RequestToPsrRequestMapper::class)->do();
}

$matchedRoute = $this->routeMatcher->match($request);

if ($matchedRoute === null) {
return new NotFound();
if (! ($request instanceof Request)) {
$request = map($request)->with(PsrRequestToGenericRequestMapper::class)->do();
}

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

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 $this->processResponse($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,
Expand All @@ -93,10 +71,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) {
Expand Down Expand Up @@ -194,38 +169,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;
}
}
44 changes: 44 additions & 0 deletions packages/router/src/HandleRouteExceptionMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Tempest\Router;

use Tempest\Core\Priority;
use Tempest\Http\HttpException;
use Tempest\Http\Request;
use Tempest\Http\Response;
use Tempest\Http\Responses\Invalid;
use Tempest\Http\Responses\NotFound;
use Tempest\Router\Exceptions\NotFoundException;
use Tempest\Validation\Exceptions\ValidationException;

#[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) {
$response = $next($request);

if ($response->status->isServerError() || $response->status->isClientError()) {
throw new HttpException(
status: $response->status,
cause: $response,
);
}

return $response;
}

try {
return $next($request);
} catch (NotFoundException) {
return new NotFound();
} catch (ValidationException $validationException) {
return new Invalid($validationException->subject, $validationException->failingRules);
}
}
}
36 changes: 36 additions & 0 deletions packages/router/src/HandleRouteSpecificMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Tempest\Router;

use Tempest\Container\Container;
use Tempest\Core\Middleware;
use Tempest\Core\Priority;
use Tempest\Http\Request;
use Tempest\Http\Response;

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

public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
{
$middlewareStack = new Middleware(...$this->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);
}
}
69 changes: 69 additions & 0 deletions packages/router/src/MatchRouteMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Tempest\Router;

use Tempest\Container\Container;
use Tempest\Core\Priority;
use Tempest\Http\GenericRequest;
use Tempest\Http\Mappers\RequestToObjectMapper;
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 - 9)]
final readonly class MatchRouteMiddleware implements HttpMiddleware
{
public function __construct(
private RouteMatcher $routeMatcher,
private Container $container,
) {}

public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
{
$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);

// 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(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)) {
// We'll use that specific request class
$requestClass = $parameter->getType()->getName();

break;
}
}

if ($requestClass !== Request::class && $requestClass !== GenericRequest::class) {
$request = map($request)->with(RequestToObjectMapper::class)->to($requestClass);
}

return $request;
}
}
3 changes: 3 additions & 0 deletions packages/router/src/RouteConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ public function __construct(

/** @var Middleware<\Tempest\Router\HttpMiddleware> */
public Middleware $middleware = new Middleware(
HandleRouteExceptionMiddleware::class,
MatchRouteMiddleware::class,
SetCookieMiddleware::class,
HandleRouteSpecificMiddleware::class,
),

public bool $throwHttpExceptions = true,
Expand Down
Loading