ErrorCatcher   A
last analyzed

Complexity

Total Complexity 22

Size/Duplication

Total Lines 167
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 22
eloc 58
c 4
b 0
f 0
dl 0
loc 167
ccs 56
cts 56
cp 1
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A withoutRenderers() 0 14 3
A withRenderer() 0 13 2
A forceContentType() 0 11 2
A __construct() 0 8 1
A normalizeContentType() 0 7 2
A getContentType() 0 13 4
A getRenderer() 0 8 2
A process() 0 11 3
A generateErrorResponse() 0 11 3
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\EventDispatcher\EventDispatcherInterface;
10
use Psr\Http\Message\ResponseFactoryInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\ServerRequestInterface;
13
use Psr\Http\Server\MiddlewareInterface;
14
use Psr\Http\Server\RequestHandlerInterface;
15
use Throwable;
16
use Yiisoft\ErrorHandler\CompositeException;
17
use Yiisoft\ErrorHandler\ErrorHandler;
0 ignored issues
show
Bug introduced by
The type Yiisoft\ErrorHandler\ErrorHandler was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use Yiisoft\ErrorHandler\Event\ApplicationError;
19
use Yiisoft\ErrorHandler\HeadersProvider;
20
use Yiisoft\ErrorHandler\Renderer\HeaderRenderer;
21
use Yiisoft\ErrorHandler\Renderer\HtmlRenderer;
22
use Yiisoft\ErrorHandler\Renderer\JsonRenderer;
23
use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer;
24
use Yiisoft\ErrorHandler\Renderer\XmlRenderer;
25
use Yiisoft\ErrorHandler\ThrowableRendererInterface;
26
use Yiisoft\Http\Header;
27
use Yiisoft\Http\HeaderValueHelper;
28
use Yiisoft\Http\Method;
29
use Yiisoft\Http\Status;
30
31
use function array_key_exists;
32
use function count;
33
use function is_subclass_of;
34
use function sprintf;
35
use function strtolower;
36
use function trim;
37
38
/**
39
 * `ErrorCatcher` catches all throwables from the next middlewares and renders it
40
 * according to the content type passed by the client.
41
 */
42
final class ErrorCatcher implements MiddlewareInterface
43
{
44
    private HeadersProvider $headersProvider;
45
46
    /**
47
     * @psalm-var array<string,class-string<ThrowableRendererInterface>>
48
     */
49
    private array $renderers = [
50
        'application/json' => JsonRenderer::class,
51
        'application/xml' => XmlRenderer::class,
52
        'text/xml' => XmlRenderer::class,
53
        'text/plain' => PlainTextRenderer::class,
54
        'text/html' => HtmlRenderer::class,
55
        '*/*' => HtmlRenderer::class,
56
    ];
57
    private ?string $contentType = null;
58
59 13
    public function __construct(
60
        private ResponseFactoryInterface $responseFactory,
61
        private ErrorHandler $errorHandler,
62
        private ContainerInterface $container,
63
        private ?EventDispatcherInterface $eventDispatcher = null,
64
        HeadersProvider $headersProvider = null,
65
    ) {
66 13
        $this->headersProvider = $headersProvider ?? new HeadersProvider();
67
    }
68
69
    /**
70
     * Returns a new instance with the specified content type and renderer class.
71
     *
72
     * @param string $contentType The content type to add associated renderers for.
73
     * @param string $rendererClass The classname implementing the {@see ThrowableRendererInterface}.
74
     */
75 6
    public function withRenderer(string $contentType, string $rendererClass): self
76
    {
77 6
        if (!is_subclass_of($rendererClass, ThrowableRendererInterface::class)) {
78 1
            throw new InvalidArgumentException(sprintf(
79 1
                'Class "%s" does not implement "%s".',
80 1
                $rendererClass,
81 1
                ThrowableRendererInterface::class,
82 1
            ));
83
        }
84
85 5
        $new = clone $this;
86 5
        $new->renderers[$this->normalizeContentType($contentType)] = $rendererClass;
87 4
        return $new;
88
    }
89
90
    /**
91
     * Returns a new instance without renderers by the specified content types.
92
     *
93
     * @param string[] $contentTypes The content types to remove associated renderers for.
94
     * If not specified, all renderers will be removed.
95
     */
96 2
    public function withoutRenderers(string ...$contentTypes): self
97
    {
98 2
        $new = clone $this;
99
100 2
        if (count($contentTypes) === 0) {
101 1
            $new->renderers = [];
102 1
            return $new;
103
        }
104
105 1
        foreach ($contentTypes as $contentType) {
106 1
            unset($new->renderers[$this->normalizeContentType($contentType)]);
107
        }
108
109 1
        return $new;
110
    }
111
112
    /**
113
     * Force content type to respond with regardless of request.
114
     *
115
     * @param string $contentType The content type to respond with regardless of request.
116
     */
117 2
    public function forceContentType(string $contentType): self
118
    {
119 2
        $contentType = $this->normalizeContentType($contentType);
120
121 2
        if (!isset($this->renderers[$contentType])) {
122 1
            throw new InvalidArgumentException(sprintf('The renderer for %s is not set.', $contentType));
123
        }
124
125 1
        $new = clone $this;
126 1
        $new->contentType = $contentType;
127 1
        return $new;
128
    }
129
130 10
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
131
    {
132
        try {
133 10
            return $handler->handle($request);
134 10
        } catch (Throwable $t) {
135
            try {
136 10
                $this->eventDispatcher?->dispatch(new ApplicationError($t));
137 1
            } catch (Throwable $e) {
138 1
                $t = new CompositeException($e, $t);
139
            }
140 10
            return $this->generateErrorResponse($t, $request);
141
        }
142
    }
143
144
    /**
145
     * Generates a response with error information.
146
     */
147 10
    private function generateErrorResponse(Throwable $t, ServerRequestInterface $request): ResponseInterface
148
    {
149 10
        $contentType = $this->contentType ?? $this->getContentType($request);
150 10
        $renderer = $request->getMethod() === Method::HEAD ? new HeaderRenderer() : $this->getRenderer($contentType);
151
152 10
        $data = $this->errorHandler->handle($t, $renderer, $request);
153 10
        $response = $this->responseFactory->createResponse(Status::INTERNAL_SERVER_ERROR);
154 10
        foreach ($this->headersProvider->getAll() as $name => $value) {
155 1
            $response = $response->withHeader($name, $value);
156
        }
157 10
        return $data->addToResponse($response->withHeader(Header::CONTENT_TYPE, $contentType));
158
    }
159
160
    /**
161
     * Returns the renderer by the specified content type, or null if the renderer was not set.
162
     *
163
     * @param string $contentType The content type associated with the renderer.
164
     */
165 9
    private function getRenderer(string $contentType): ?ThrowableRendererInterface
166
    {
167 9
        if (isset($this->renderers[$contentType])) {
168
            /** @var ThrowableRendererInterface */
169 7
            return $this->container->get($this->renderers[$contentType]);
170
        }
171
172 2
        return null;
173
    }
174
175
    /**
176
     * Returns the priority content type from the accept request header.
177
     *
178
     * @return string The priority content type.
179
     */
180 9
    private function getContentType(ServerRequestInterface $request): string
181
    {
182
        try {
183 9
            foreach (HeaderValueHelper::getSortedAcceptTypes($request->getHeader(Header::ACCEPT)) as $header) {
184 7
                if (array_key_exists($header, $this->renderers)) {
185 2
                    return $header;
186
                }
187
            }
188 2
        } catch (InvalidArgumentException) {
189
            // The Accept header contains an invalid q factor.
190
        }
191
192 7
        return '*/*';
193
    }
194
195
    /**
196
     * Normalizes the content type.
197
     *
198
     * @param string $contentType The raw content type.
199
     *
200
     * @return string Normalized content type.
201
     */
202 8
    private function normalizeContentType(string $contentType): string
203
    {
204 8
        if (!str_contains($contentType, '/')) {
205 1
            throw new InvalidArgumentException('Invalid content type.');
206
        }
207
208 7
        return strtolower(trim($contentType));
209
    }
210
}
211