Passed
Push — master ( 9a27aa...bae487 )
by Alexander
27:21 queued 24:19
created

ErrorCatcher   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 138
Duplicated Lines 0 %

Test Coverage

Coverage 96.77%

Importance

Changes 0
Metric Value
eloc 63
dl 0
loc 138
ccs 60
cts 62
cp 0.9677
rs 10
c 0
b 0
f 0
wmc 23

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A forceContentType() 0 10 2
A process() 0 6 2
A withoutRenderers() 0 12 3
A validateMimeType() 0 4 2
A validateRenderer() 0 8 3
A withRenderer() 0 8 1
A getRenderer() 0 6 2
A handleException() 0 12 2
A normalizeMimeType() 0 3 1
A getContentType() 0 12 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Web\ErrorHandler;
6
7
use Psr\Container\ContainerInterface;
8
use Psr\Http\Message\ResponseFactoryInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Psr\Http\Server\MiddlewareInterface;
12
use Psr\Http\Server\RequestHandlerInterface;
13
use Yiisoft\Http\Header;
14
use Yiisoft\Http\HeaderHelper;
15
use Yiisoft\Http\Status;
16
17
/**
18
 * ErrorCatcher catches all throwables from the next middlewares and renders it
19
 * according to the content type passed by the client.
20
 */
21
final class ErrorCatcher implements MiddlewareInterface
22
{
23
    private array $renderers = [
24
        'application/json' => JsonRenderer::class,
25
        'application/xml' => XmlRenderer::class,
26
        'text/xml' => XmlRenderer::class,
27
        'text/plain' => PlainTextRenderer::class,
28
        'text/html' => HtmlRenderer::class,
29
        '*/*' => HtmlRenderer::class,
30
    ];
31
32
    private ResponseFactoryInterface $responseFactory;
33
    private ErrorHandler $errorHandler;
34
    private ContainerInterface $container;
35
    private ?string $contentType = null;
36
37 9
    public function __construct(
38
        ResponseFactoryInterface $responseFactory,
39
        ErrorHandler $errorHandler,
40
        ContainerInterface $container
41
    ) {
42 9
        $this->responseFactory = $responseFactory;
43 9
        $this->errorHandler = $errorHandler;
44 9
        $this->container = $container;
45 9
    }
46
47 5
    public function withRenderer(string $mimeType, string $rendererClass): self
48
    {
49 5
        $this->validateMimeType($mimeType);
50 4
        $this->validateRenderer($rendererClass);
51
52 3
        $new = clone $this;
53 3
        $new->renderers[$this->normalizeMimeType($mimeType)] = $rendererClass;
54 3
        return $new;
55
    }
56
57
    /**
58
     * @param string[] $mimeTypes MIME types or, if not specified, all will be removed.
59
     */
60 2
    public function withoutRenderers(string ...$mimeTypes): self
61
    {
62 2
        $new = clone $this;
63 2
        if (count($mimeTypes) === 0) {
64 1
            $new->renderers = [];
65 1
            return $new;
66
        }
67 1
        foreach ($mimeTypes as $mimeType) {
68 1
            $this->validateMimeType($mimeType);
69 1
            unset($new->renderers[$this->normalizeMimeType($mimeType)]);
70
        }
71 1
        return $new;
72
    }
73
74
    /**
75
     * Force content type to respond with regardless of request
76
     * @param string $contentType
77
     * @return $this
78
     */
79 2
    public function forceContentType(string $contentType): self
80
    {
81 2
        $this->validateMimeType($contentType);
82 2
        if (!isset($this->renderers[$contentType])) {
83 1
            throw new \InvalidArgumentException(sprintf('The renderer for %s is not set.', $contentType));
84
        }
85
86 1
        $new = clone $this;
87 1
        $new->contentType = $contentType;
88 1
        return $new;
89
    }
90
91 6
    private function handleException(\Throwable $e, ServerRequestInterface $request): ResponseInterface
92
    {
93 6
        $contentType = $this->contentType ?? $this->getContentType($request);
94 6
        $renderer = $this->getRenderer(strtolower($contentType));
95 6
        if ($renderer !== null) {
96 4
            $renderer->setRequest($request);
97
        }
98 6
        $content = $this->errorHandler->handleCaughtThrowable($e, $renderer);
99 6
        $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR)
100 6
            ->withHeader(Header::CONTENT_TYPE, $contentType);
101 6
        $response->getBody()->write($content);
102 6
        return $response;
103
    }
104
105 6
    private function getRenderer(string $contentType): ?ThrowableRendererInterface
106
    {
107 6
        if (isset($this->renderers[$contentType])) {
108 4
            return $this->container->get($this->renderers[$contentType]);
109
        }
110 2
        return null;
111
    }
112
113 5
    private function getContentType(ServerRequestInterface $request): string
114
    {
115
        try {
116 5
            foreach (HeaderHelper::getSortedAcceptTypes($request->getHeader('accept')) as $header) {
117 5
                if (array_key_exists($header, $this->renderers)) {
118 2
                    return $header;
119
                }
120
            }
121
        } catch (\InvalidArgumentException $e) {
122
            // The Accept header contains an invalid q factor
123
        }
124 3
        return '*/*';
125
    }
126
127 6
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
128
    {
129
        try {
130 6
            return $handler->handle($request);
131 6
        } catch (\Throwable $e) {
132 6
            return $this->handleException($e, $request);
133
        }
134
    }
135
136
    /**
137
     * @throws \InvalidArgumentException
138
     */
139 8
    private function validateMimeType(string $mimeType): void
140
    {
141 8
        if (strpos($mimeType, '/') === false) {
142 1
            throw new \InvalidArgumentException('Invalid mime type.');
143
        }
144 7
    }
145
146 4
    private function normalizeMimeType(string $mimeType): string
147
    {
148 4
        return strtolower(trim($mimeType));
149
    }
150
151 4
    private function validateRenderer(string $rendererClass): void
152
    {
153 4
        if (trim($rendererClass) === '') {
154
            throw new \InvalidArgumentException('The renderer class cannot be an empty string.');
155
        }
156
157 4
        if ($this->container->has($rendererClass) === false) {
158 1
            throw new \InvalidArgumentException("The renderer \"$rendererClass\" cannot be found.");
159
        }
160 3
    }
161
}
162