Passed
Push — master ( 1d59e9...66e80e )
by Alexander
09:22 queued 07:43
created

ErrorCatcher::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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