Passed
Push — master ( 8b7547...d71e56 )
by Alexander
02:06
created

ErrorCatcher::handleException()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

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