Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.44% covered (success)
97.44%
38 / 39
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExceptionMiddleware
97.44% covered (success)
97.44%
38 / 39
75.00% covered (warning)
75.00%
3 / 4
21
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 process
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
4
 getHttpStatusCode
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
8
 getErrorLabel
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2
3declare(strict_types=1);
4
5namespace App\Middleware;
6
7use App\Domain\Exception\ForbiddenException;
8use App\Domain\Exception\NotFoundException;
9use App\Renderer\JsonRenderer;
10use DomainException;
11use Fig\Http\Message\StatusCodeInterface;
12use InvalidArgumentException;
13use Psr\Http\Message\ResponseFactoryInterface;
14use Psr\Http\Message\ResponseInterface;
15use Psr\Http\Message\ServerRequestInterface;
16use Psr\Http\Server\MiddlewareInterface;
17use Psr\Http\Server\RequestHandlerInterface;
18use Psr\Log\LoggerInterface;
19use RuntimeException;
20use Slim\Exception\HttpException;
21use Throwable;
22
23use function sprintf;
24
25final class ExceptionMiddleware implements MiddlewareInterface
26{
27    public function __construct(
28        private readonly ResponseFactoryInterface $responseFactory,
29        private readonly JsonRenderer $renderer,
30        private readonly ?LoggerInterface $logger = null,
31    ) {}
32
33    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
34    {
35        try {
36            return $handler->handle($request);
37        } catch (Throwable $exception) {
38            if (isset($this->logger)) {
39                $this->logger->error(
40                    sprintf(
41                        '%s;Code %s;File: %s;Line: %s',
42                        $exception->getMessage(),
43                        $exception->getCode(),
44                        $exception->getFile(),
45                        $exception->getLine(),
46                    ),
47                    $exception->getTrace(),
48                );
49            }
50
51            $statusCode = $this->getHttpStatusCode($exception);
52            $errorLabel = $this->getErrorLabel($exception, $statusCode);
53            $message = $statusCode >= 500
54                ? 'An unexpected error occurred'
55                : $exception->getMessage();
56
57            $response = $this->responseFactory->createResponse($statusCode);
58
59            return $this->renderer->json($response, [
60                'success' => false,
61                'error' => $errorLabel,
62                'message' => $message,
63            ], $statusCode);
64        }
65    }
66
67    private function getHttpStatusCode(Throwable $exception): int
68    {
69        return match (true) {
70            $exception instanceof InvalidArgumentException => StatusCodeInterface::STATUS_BAD_REQUEST,
71            $exception instanceof ForbiddenException => StatusCodeInterface::STATUS_FORBIDDEN,
72            $exception instanceof NotFoundException => StatusCodeInterface::STATUS_NOT_FOUND,
73            $exception instanceof DomainException => StatusCodeInterface::STATUS_CONFLICT,
74            $exception instanceof HttpException => $exception->getCode(),
75            $exception instanceof RuntimeException => StatusCodeInterface::STATUS_UNAUTHORIZED,
76            default => StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR,
77        };
78    }
79
80    private function getErrorLabel(Throwable $exception, int $statusCode): string
81    {
82        return match (true) {
83            $exception instanceof InvalidArgumentException => 'Validation Error',
84            $exception instanceof ForbiddenException => 'Forbidden',
85            $exception instanceof NotFoundException => 'Not Found',
86            $exception instanceof DomainException => 'Business Rule Violation',
87            $exception instanceof HttpException => $exception->getMessage(),
88            $exception instanceof RuntimeException => 'Authentication Error',
89            default => 'Server Error',
90        };
91    }
92}