Passed
Push — master ( 6f1760...ae05e1 )
by Alexander
03:10
created

ErrorCatcher::withOnlyRenderer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 2
dl 0
loc 9
ccs 7
cts 7
cp 1
crap 1
rs 10
c 0
b 0
f 0
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
36 8
    public function __construct(
37
        ResponseFactoryInterface $responseFactory,
38
        ErrorHandler $errorHandler,
39
        ContainerInterface $container
40
    ) {
41 8
        $this->responseFactory = $responseFactory;
42 8
        $this->errorHandler = $errorHandler;
43 8
        $this->container = $container;
44 8
    }
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 1
    public function withOnlyRenderer(string $mimeType, string $rendererClass): self
57
    {
58 1
        $this->validateMimeType($mimeType);
59 1
        $this->validateRenderer($rendererClass);
60
61 1
        $new = clone $this;
62 1
        $new->renderers = [];
63 1
        $new->renderers[$this->normalizeMimeType($mimeType)] = $rendererClass;
64 1
        return $new;
65
    }
66
67
    /**
68
     * @param string[] $mimeTypes MIME types or, if not specified, all will be removed.
69
     */
70 2
    public function withoutRenderers(string ...$mimeTypes): self
71
    {
72 2
        $new = clone $this;
73 2
        if (count($mimeTypes) === 0) {
74 1
            $new->renderers = [];
75 1
            return $new;
76
        }
77 1
        foreach ($mimeTypes as $mimeType) {
78 1
            $this->validateMimeType($mimeType);
79 1
            unset($new->renderers[$this->normalizeMimeType($mimeType)]);
80
        }
81 1
        return $new;
82
    }
83
84 6
    private function handleException(\Throwable $e, ServerRequestInterface $request): ResponseInterface
85
    {
86 6
        $contentType = $this->getContentType($request);
87 6
        $renderer = $this->getRenderer(strtolower($contentType));
88 6
        if ($renderer !== null) {
89 4
            $renderer->setRequest($request);
90
        }
91 6
        $content = $this->errorHandler->handleCaughtThrowable($e, $renderer);
92 6
        $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR)
93 6
            ->withHeader(Header::CONTENT_TYPE, $contentType);
94 6
        $response->getBody()->write($content);
95 6
        return $response;
96
    }
97
98 6
    private function getRenderer(string $contentType): ?ThrowableRendererInterface
99
    {
100 6
        if (isset($this->renderers[$contentType])) {
101 4
            return $this->container->get($this->renderers[$contentType]);
102
        }
103 3
        return null;
104
    }
105
106 6
    private function getContentType(ServerRequestInterface $request): string
107
    {
108
        try {
109 6
            foreach (HeaderHelper::getSortedAcceptTypes($request->getHeader('accept')) as $header) {
110 6
                if (array_key_exists($header, $this->renderers)) {
111 3
                    return $header;
112
                }
113
            }
114
        } catch (\InvalidArgumentException $e) {
115
            // The Accept header contains an invalid q factor
116
        }
117 4
        return '*/*';
118
    }
119
120 6
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
121
    {
122
        try {
123 6
            return $handler->handle($request);
124 6
        } catch (\Throwable $e) {
125 6
            return $this->handleException($e, $request);
126
        }
127
    }
128
129
    /**
130
     * @throws \InvalidArgumentException
131
     */
132 7
    private function validateMimeType(string $mimeType): void
133
    {
134 7
        if (strpos($mimeType, '/') === false) {
135 1
            throw new \InvalidArgumentException('Invalid mime type.');
136
        }
137 6
    }
138
139 5
    private function normalizeMimeType(string $mimeType): string
140
    {
141 5
        return strtolower(trim($mimeType));
142
    }
143
144 5
    private function validateRenderer(string $rendererClass): void
145
    {
146 5
        if (trim($rendererClass) === '') {
147
            throw new \InvalidArgumentException('The renderer class cannot be an empty string.');
148
        }
149
150 5
        if ($this->container->has($rendererClass) === false) {
151 1
            throw new \InvalidArgumentException("The renderer \"$rendererClass\" cannot be found.");
152
        }
153 4
    }
154
}
155