Test Failed
Pull Request — master (#334)
by Alexander
02:54
created

ErrorCatcher::process()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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