ErrorHandlingMiddleware::createErrorView()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 11
nc 2
nop 2
dl 0
loc 19
ccs 13
cts 13
cp 1
crap 2
rs 9.9
c 1
b 0
f 0
1
<?php
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-router
10
 */
11
12
declare(strict_types=1);
13
14
namespace Sunrise\Http\Router\Middleware;
15
16
use Psr\Http\Message\ResponseFactoryInterface;
17
use Psr\Http\Message\ResponseInterface;
18
use Psr\Http\Message\ServerRequestInterface;
19
use Psr\Http\Message\StreamFactoryInterface;
20
use Psr\Http\Server\MiddlewareInterface;
21
use Psr\Http\Server\RequestHandlerInterface;
22
use Psr\Log\LoggerInterface;
23
use Sunrise\Coder\CodecManagerInterface;
24
use Sunrise\Coder\MediaTypeInterface;
25
use Sunrise\Http\Router\Dictionary\HeaderName;
26
use Sunrise\Http\Router\Exception\HttpException;
27
use Sunrise\Http\Router\Exception\HttpExceptionFactory;
28
use Sunrise\Http\Router\LanguageInterface;
29
use Sunrise\Http\Router\ServerRequest;
30
use Sunrise\Http\Router\Validation\ConstraintViolationInterface;
31
use Sunrise\Http\Router\View\ErrorView;
32
use Sunrise\Http\Router\View\ViolationView;
33
use Sunrise\Translator\TranslatorManagerInterface;
34
use Throwable;
35
36
use function sprintf;
37
38
final class ErrorHandlingMiddleware implements MiddlewareInterface
39
{
40 6
    public function __construct(
41
        private readonly ResponseFactoryInterface $responseFactory,
42
        private readonly StreamFactoryInterface $streamFactory,
43
        private readonly CodecManagerInterface $codecManager,
44
        /** @var array<array-key, mixed> */
45
        private readonly array $codecContext,
46
        /** @var list<MediaTypeInterface> */
47
        private readonly array $producedMediaTypes,
48
        private readonly MediaTypeInterface $defaultMediaType,
49
        private readonly TranslatorManagerInterface $translatorManager,
50
        /** @var list<LanguageInterface> */
51
        private readonly array $producedLanguages,
52
        private readonly LanguageInterface $defaultLanguage,
53
        private readonly LoggerInterface $logger,
54
        private readonly ?int $fatalErrorStatusCode = null,
55
        private readonly ?string $fatalErrorMessage = null,
56
    ) {
57 6
    }
58
59
    /**
60
     * @inheritDoc
61
     */
62 6
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
63
    {
64
        try {
65 6
            return $handler->handle($request);
66 5
        } catch (HttpException $e) {
67 1
            return $this->handleHttpError($e, $request);
68 4
        } catch (Throwable $e) {
69 4
            return $this->handleFatalError($e, $request);
70
        }
71
    }
72
73 5
    private function handleHttpError(HttpException $error, ServerRequestInterface $request): ResponseInterface
74
    {
75 5
        $clientPreferredMediaType = ServerRequest::create($request)
76 5
            ->getClientPreferredMediaType(...$this->producedMediaTypes)
77 5
                ?? $this->defaultMediaType;
78
79 5
        $clientPreferredLanguage = ServerRequest::create($request)
80 5
            ->getClientPreferredLanguage(...$this->producedLanguages)
81 5
                ?? $this->defaultLanguage;
82
83 5
        return $this->createErrorResponse($error, $clientPreferredMediaType, $clientPreferredLanguage);
84
    }
85
86 4
    private function handleFatalError(Throwable $error, ServerRequestInterface $request): ResponseInterface
87
    {
88 4
        $this->logger->error($error->getMessage(), [
89 4
            'error' => $error,
90 4
            'request' => $request,
91 4
        ]);
92
93 4
        $httpError = HttpExceptionFactory::internalServerError(
94 4
            message: $this->fatalErrorMessage,
95 4
            code: $this->fatalErrorStatusCode,
96 4
            previous: $error,
97 4
        );
98
99 4
        return $this->handleHttpError($httpError, $request);
100
    }
101
102 5
    private function createErrorResponse(
103
        HttpException $error,
104
        MediaTypeInterface $mediaType,
105
        LanguageInterface $language,
106
    ): ResponseInterface {
107 5
        $response = $this->responseFactory->createResponse($error->getCode());
108 5
        foreach ($error->getHeaderFields() as [$fieldName, $fieldValue]) {
109 1
            $response = $response->withHeader($fieldName, $fieldValue);
110
        }
111
112 5
        $errorView = $this->createErrorView($error, $language);
113 5
        $responseContent = $this->codecManager->encode($mediaType, $errorView, $this->codecContext);
114 5
        $responseContentType = sprintf('%s; charset=UTF-8', $mediaType->getIdentifier());
115
116 5
        return $response->withHeader(HeaderName::CONTENT_TYPE, $responseContentType)
0 ignored issues
show
Bug Best Practice introduced by
The expression return $response->withHe...ream($responseContent)) returns the type Psr\Http\Message\MessageInterface which includes types incompatible with the type-hinted return Psr\Http\Message\ResponseInterface.
Loading history...
117 5
            ->withBody($this->streamFactory->createStream($responseContent));
118
    }
119
120 5
    private function createErrorView(
121
        HttpException $error,
122
        LanguageInterface $language,
123
    ): ErrorView {
124 5
        $message = $this->translatorManager->translate(
125 5
            domain: $error->getTranslationDomain(),
126 5
            locale: $language->getCode(),
127 5
            template: $error->getMessageTemplate(),
128 5
            placeholders: $error->getMessagePlaceholders(),
129 5
        );
130
131 5
        $violationViews = [];
132 5
        foreach ($error->getConstraintViolations() as $violation) {
133 1
            $violationViews[] = $this->createViolationView($violation, $language);
134
        }
135
136 5
        return new ErrorView(
137 5
            message: $message,
138 5
            violations: $violationViews,
139 5
        );
140
    }
141
142 1
    private function createViolationView(
143
        ConstraintViolationInterface $violation,
144
        LanguageInterface $language,
145
    ): ViolationView {
146 1
        $message = $this->translatorManager->translate(
147 1
            domain: $violation->getTranslationDomain(),
148 1
            locale: $language->getCode(),
149 1
            template: $violation->getMessageTemplate(),
150 1
            placeholders: $violation->getMessagePlaceholders(),
151 1
        );
152
153 1
        return new ViolationView(
154 1
            source: $violation->getPropertyPath(),
155 1
            message: $message,
156 1
            code: $violation->getCode(),
157 1
        );
158
    }
159
}
160