Passed
Pull Request — master (#25)
by Evgeniy
02:24
created

ErrorCatcher   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 137
Duplicated Lines 0 %

Test Coverage

Coverage 96.67%

Importance

Changes 0
Metric Value
wmc 22
eloc 60
dl 0
loc 137
ccs 58
cts 60
cp 0.9667
rs 10
c 0
b 0
f 0

11 Methods

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