Skip to content

Commit 3b9afa4

Browse files
wip
1 parent 2eb3c9f commit 3b9afa4

File tree

7 files changed

+90
-66
lines changed

7 files changed

+90
-66
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"psr/http-factory": "^1.0",
3434
"psr/http-message": "^1.0|^2.0",
3535
"psr/log": "^3.0.0",
36-
"rector/rector": "^2.1",
36+
"rector/rector": "2.2.1",
3737
"symfony/cache": "^7.3",
3838
"symfony/mailer": "^7.2.6",
3939
"symfony/process": "^7.3",

packages/core/src/DevelopmentExceptionHandler.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
namespace Tempest\Core;
44

55
use Tempest\Container\Container;
6+
use Tempest\Http\ContentType;
67
use Tempest\Http\Request;
7-
use Tempest\Router\Exceptions\JsonHttpExceptionHandler;
8+
use Tempest\Router\Exceptions\JsonHttpExceptionRenderer;
89
use Tempest\Router\MatchedRoute;
910
use Throwable;
1011
use Whoops\Handler\HandlerInterface;
@@ -18,21 +19,23 @@
1819
public function __construct(
1920
private Container $container,
2021
private ExceptionReporter $exceptionReporter,
21-
private JsonHttpExceptionHandler $jsonHandler,
22+
private JsonHttpExceptionRenderer $jsonHandler,
2223
) {
2324
$this->whoops = new Run();
2425
$this->whoops->pushHandler($this->createHandler());
2526
}
2627

2728
public function handle(Throwable $throwable): void
2829
{
29-
if ($this->container->get(Request::class)->headers->get('accept') === 'application/json') {
30-
$this->jsonHandler->handle($throwable);
31-
return;
32-
}
33-
3430
$this->exceptionReporter->report($throwable);
35-
$this->whoops->handleException($throwable);
31+
32+
$request = $this->container->get(Request::class);
33+
34+
match (true) {
35+
$request->accepts(ContentType::HTML, ContentType::XHTML) => $this->whoops->handleException($throwable),
36+
$request->accepts(ContentType::JSON) => $this->jsonHandler->render($throwable),
37+
default => $this->whoops->handleException($throwable),
38+
};
3639
}
3740

3841
private function createHandler(): HandlerInterface

packages/http/src/IsRequest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
use Tempest\Validation\SkipValidation;
1010

1111
use function Tempest\get;
12-
use function Tempest\Support\Arr\every;
1312
use function Tempest\Support\Arr\get_by_key;
1413
use function Tempest\Support\Arr\has_key;
1514
use function Tempest\Support\str;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Http\Responses;
6+
7+
use Tempest\Http\IsResponse;
8+
use Tempest\Http\Response;
9+
use Tempest\Http\Status;
10+
11+
final class NotAcceptable implements Response
12+
{
13+
use IsResponse;
14+
15+
public function __construct()
16+
{
17+
$this->status = Status::NOT_ACCEPTABLE;
18+
}
19+
}

packages/router/src/Exceptions/HttpExceptionHandler.php

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,20 @@ public function __construct(
2828
private ResponseSender $responseSender,
2929
private Container $container,
3030
private ExceptionReporter $exceptionReporter,
31-
private JsonHttpExceptionHandler $jsonHandler,
31+
private JsonHttpExceptionRenderer $jsonHandler,
3232
) {}
3333

3434
public function handle(Throwable $throwable): void
3535
{
36-
if ($this->container->get(Request::class)->accepts(ContentType::JSON)) {
37-
$this->jsonHandler->handle($throwable);
38-
return;
39-
}
36+
$request = $this->container->get(Request::class);
4037

4138
try {
4239
$this->exceptionReporter->report($throwable);
4340

4441
$response = match (true) {
45-
$throwable instanceof ConvertsToResponse => $throwable->toResponse(),
46-
$throwable instanceof AccessWasDenied => $this->renderErrorResponse(Status::FORBIDDEN),
47-
$throwable instanceof HttpRequestFailed => $this->renderErrorResponse($throwable->status, $throwable),
48-
$throwable instanceof CsrfTokenDidNotMatch => $this->renderErrorResponse(Status::UNPROCESSABLE_CONTENT),
49-
default => $this->renderErrorResponse(Status::INTERNAL_SERVER_ERROR),
42+
$request->accepts(ContentType::HTML, ContentType::XHTML) => $this->handleHtml($throwable),
43+
$request->accepts(ContentType::JSON) => $this->jsonHandler->render($throwable),
44+
default => new GenericResponse(status: Status::NOT_ACCEPTABLE),
5045
};
5146

5247
$this->responseSender->send($response);
@@ -55,6 +50,17 @@ public function handle(Throwable $throwable): void
5550
}
5651
}
5752

53+
private function handleHtml(Throwable $throwable): Response
54+
{
55+
return match (true) {
56+
$throwable instanceof ConvertsToResponse => $throwable->toResponse(),
57+
$throwable instanceof AccessWasDenied => $this->renderErrorResponse(Status::FORBIDDEN),
58+
$throwable instanceof HttpRequestFailed => $this->renderErrorResponse($throwable->status, $throwable),
59+
$throwable instanceof CsrfTokenDidNotMatch => $this->renderErrorResponse(Status::UNPROCESSABLE_CONTENT),
60+
default => $this->renderErrorResponse(Status::INTERNAL_SERVER_ERROR),
61+
};
62+
}
63+
5864
private function renderErrorResponse(Status $status, ?HttpRequestFailed $exception = null): Response
5965
{
6066
return new GenericResponse(

packages/router/src/Exceptions/JsonHttpExceptionHandler.php renamed to packages/router/src/Exceptions/JsonHttpExceptionRenderer.php

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use Tempest\Auth\Exceptions\AccessWasDenied;
66
use Tempest\Container\Container;
77
use Tempest\Core\AppConfig;
8-
use Tempest\Core\ExceptionHandler;
98
use Tempest\Core\ExceptionReporter;
109
use Tempest\Core\Kernel;
1110
use Tempest\Http\HttpRequestFailed;
@@ -14,11 +13,14 @@
1413
use Tempest\Http\Session\CsrfTokenDidNotMatch;
1514
use Tempest\Http\Status;
1615
use Tempest\Router\ResponseSender;
16+
use Tempest\Validation\Exceptions\ValidationFailed;
17+
use Tempest\Validation\Rule;
18+
use Tempest\Validation\Validator;
1719
use Throwable;
1820

1921
use function Tempest\Support\arr;
2022

21-
final readonly class JsonHttpExceptionHandler implements ExceptionHandler
23+
final readonly class JsonHttpExceptionRenderer
2224
{
2325
public function __construct(
2426
private AppConfig $appConfig,
@@ -28,26 +30,34 @@ public function __construct(
2830
private ExceptionReporter $exceptionReporter,
2931
) {}
3032

31-
public function handle(Throwable $throwable): void
33+
public function render(Throwable $throwable): Response
3234
{
33-
try {
34-
$this->exceptionReporter->report($throwable);
35+
return match (true) {
36+
$throwable instanceof ConvertsToResponse => $throwable->toResponse(),
37+
$throwable instanceof ValidationFailed => $this->renderValidationErrorResponse($throwable),
38+
$throwable instanceof RouteBindingFailed => $this->renderErrorResponse(Status::NOT_FOUND),
39+
$throwable instanceof AccessWasDenied => $this->renderErrorResponse(Status::FORBIDDEN),
40+
$throwable instanceof HttpRequestFailed => $this->renderErrorResponse($throwable->status),
41+
$throwable instanceof CsrfTokenDidNotMatch => $this->renderErrorResponse(Status::UNPROCESSABLE_CONTENT),
42+
default => $this->renderErrorResponse(
43+
Status::INTERNAL_SERVER_ERROR,
44+
$this->appConfig->environment->isLocal() ? $throwable : null,
45+
),
46+
};
47+
}
3548

36-
$response = match (true) {
37-
$throwable instanceof ConvertsToResponse => $throwable->toResponse(),
38-
$throwable instanceof AccessWasDenied => $this->renderErrorResponse(Status::FORBIDDEN),
39-
$throwable instanceof HttpRequestFailed => $this->renderErrorResponse($throwable->status),
40-
$throwable instanceof CsrfTokenDidNotMatch => $this->renderErrorResponse(Status::UNPROCESSABLE_CONTENT),
41-
default => $this->renderErrorResponse(
42-
Status::INTERNAL_SERVER_ERROR,
43-
$this->appConfig->environment->isLocal() ? $throwable : null,
44-
),
45-
};
49+
private function renderValidationErrorResponse(ValidationFailed $exception): Response
50+
{
51+
$errors = arr($exception->failingRules)->map(
52+
fn (array $failingRulesForField, string $field) => arr($failingRulesForField)->map(
53+
fn (Rule $rule) => $this->container->get(Validator::class)->getErrorMessage($rule, $field),
54+
)->toArray(),
55+
);
4656

47-
$this->responseSender->send($response);
48-
} finally {
49-
$this->kernel->shutdown();
50-
}
57+
return new Json([
58+
'message' => $errors->first()[0],
59+
'errors' => $errors->toArray(),
60+
])->setStatus(Status::UNPROCESSABLE_CONTENT);
5161
}
5262

5363
private function renderErrorResponse(Status $status, ?Throwable $exception = null): Response

packages/router/src/HandleRouteExceptionMiddleware.php

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,19 @@
88
use Tempest\Http\Request;
99
use Tempest\Http\Response;
1010
use Tempest\Http\Responses\Invalid;
11-
use Tempest\Http\Responses\Json;
11+
use Tempest\Http\Responses\NotAcceptable;
1212
use Tempest\Http\Responses\NotFound;
13-
use Tempest\Http\Status;
1413
use Tempest\Router\Exceptions\ConvertsToResponse;
14+
use Tempest\Router\Exceptions\JsonHttpExceptionRenderer;
1515
use Tempest\Router\Exceptions\RouteBindingFailed;
1616
use Tempest\Validation\Exceptions\ValidationFailed;
17-
use Tempest\Validation\Rule;
18-
use Tempest\Validation\Validator;
19-
20-
use function Tempest\get;
21-
use function Tempest\Support\arr;
2217

2318
#[Priority(Priority::FRAMEWORK - 10)]
2419
final readonly class HandleRouteExceptionMiddleware implements HttpMiddleware
2520
{
2621
public function __construct(
2722
private RouteConfig $routeConfig,
23+
private JsonHttpExceptionRenderer $jsonHandler,
2824
) {}
2925

3026
public function __invoke(Request $request, HttpMiddlewareCallable $next): Response
@@ -52,27 +48,18 @@ private function forward(Request $request, HttpMiddlewareCallable $next): Respon
5248
return $next($request);
5349
} catch (ConvertsToResponse $convertsToResponse) {
5450
return $convertsToResponse->toResponse();
55-
} catch (RouteBindingFailed) {
56-
if ($request->accepts(ContentType::JSON)) {
57-
return new NotFound([
58-
'message' => 'The requested resource was not found.',
59-
]);
60-
}
61-
62-
return new NotFound();
51+
} catch (RouteBindingFailed $routeBindingFailed) {
52+
return match (true) {
53+
$request->accepts(ContentType::HTML, ContentType::XHTML) => new NotFound(),
54+
$request->accepts(ContentType::JSON) => $this->jsonHandler->render($routeBindingFailed),
55+
default => new NotAcceptable(),
56+
};
6357
} catch (ValidationFailed $validationException) {
64-
if ($request->accepts(ContentType::JSON)) {
65-
$errors = arr($validationException->failingRules)->map(
66-
fn (array $failingRulesForField, string $field) => arr($failingRulesForField)->map(
67-
fn (Rule $rule) => get(Validator::class)->getErrorMessage($rule, $field),
68-
)->toArray(),
69-
);
70-
71-
return new Json([
72-
'message' => $errors->first()[0],
73-
'errors' => $errors->toArray(),
74-
])->setStatus(Status::UNPROCESSABLE_CONTENT);
75-
}
58+
return match (true) {
59+
$request->accepts(ContentType::HTML, ContentType::XHTML) => new Invalid($validationException->subject, $validationException->failingRules),
60+
$request->accepts(ContentType::JSON) => $this->jsonHandler->render($validationException),
61+
default => new NotAcceptable(),
62+
};
7663

7764
return new Invalid($validationException->subject, $validationException->failingRules);
7865
}

0 commit comments

Comments
 (0)