55use Tempest \Auth \Exceptions \AccessWasDenied ;
66use Tempest \Container \Container ;
77use Tempest \Core \AppConfig ;
8- use Tempest \Core \ExceptionHandler ;
98use Tempest \Core \ExceptionReporter ;
109use Tempest \Core \Kernel ;
1110use Tempest \Http \HttpRequestFailed ;
1413use Tempest \Http \Session \CsrfTokenDidNotMatch ;
1514use Tempest \Http \Status ;
1615use Tempest \Router \ResponseSender ;
16+ use Tempest \Validation \Exceptions \ValidationFailed ;
17+ use Tempest \Validation \Rule ;
18+ use Tempest \Validation \Validator ;
1719use Throwable ;
1820
1921use 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
0 commit comments