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

ErrorHandler::handleThrowable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 10
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\Log\LoggerAwareInterface;
8
use Psr\Log\LoggerAwareTrait;
9
use Psr\Log\LoggerInterface;
10
use Yiisoft\Http\Status;
11
12
final class ErrorHandler implements LoggerAwareInterface
13
{
14
    use LoggerAwareTrait;
15
16
    /**
17
     * @var int the size of the reserved memory. A portion of memory is pre-allocated so that
18
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
19
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
20
     * Defaults to 256KB.
21
     */
22
    private int $memoryReserveSize = 262_144;
0 ignored issues
show
Bug introduced by
The constant Yiisoft\Yii\Web\ErrorHandler\262_144 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
23
    private string $memoryReserve = '';
24
    private bool $exposeDetails = true;
25
26
    private ThrowableRendererInterface $defaultRenderer;
27
28
    public function __construct(LoggerInterface $logger, ThrowableRendererInterface $defaultRenderer)
29
    {
30
        $this->logger = $logger;
31
        $this->defaultRenderer = $defaultRenderer;
32
    }
33
34
    /**
35
     * Handles PHP execution errors such as warnings and notices.
36
     *
37
     * This method is used as a PHP error handler. It will raise an [[\ErrorException]].
38
     *
39
     * @param int $severity the level of the error raised.
40
     * @param string $message the error message.
41
     * @param string $file the filename that the error was raised in.
42
     * @param int $line the line number the error was raised at.
43
     *
44
     * @throws ErrorException
45
     */
46
    public function handleError(int $severity, string $message, string $file, int $line): void
47
    {
48
        if (!(error_reporting() & $severity)) {
49
            // This error code is not included in error_reporting
50
            return;
51
        }
52
53
        throw new ErrorException($message, $severity, $severity, $file, $line);
54
    }
55
56
    /**
57
     * Handle throwable and return output
58
     *
59
     * @param \Throwable $t
60
     * @param ThrowableRendererInterface|null $renderer
61
     *
62
     * @return string
63
     */
64
    public function handleCaughtThrowable(\Throwable $t, ThrowableRendererInterface $renderer = null): string
65
    {
66
        if ($renderer === null) {
67
            $renderer = $this->defaultRenderer;
68
        }
69
70
        try {
71
            $this->log($t);
72
            return $this->exposeDetails ? $renderer->renderVerbose($t) : $renderer->render($t);
73
        } catch (\Throwable $t) {
74
            return (string)$t;
75
        }
76
    }
77
78
    /**
79
     * Handle throwable, echo output and exit
80
     *
81
     * @param \Throwable $t
82
     */
83
    public function handleThrowable(\Throwable $t): void
84
    {
85
        // disable error capturing to avoid recursive errors while handling exceptions
86
        $this->unregister();
87
88
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
89
        http_response_code(Status::INTERNAL_SERVER_ERROR);
90
91
        echo $this->handleCaughtThrowable($t);
92
        exit(1);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
93
    }
94
95
    /**
96
     * Register this error handler.
97
     */
98
    public function register(): void
99
    {
100
        $this->disableDisplayErrors();
101
        set_exception_handler([$this, 'handleThrowable']);
102
        /** @psalm-suppress InvalidArgument */
103
        set_error_handler([$this, 'handleError']);
104
105
        if ($this->memoryReserveSize > 0) {
106
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
107
        }
108
        register_shutdown_function([$this, 'handleFatalError']);
109
    }
110
111
    private function disableDisplayErrors(): void
112
    {
113
        if (function_exists('ini_set')) {
114
            ini_set('display_errors', '0');
115
        }
116
    }
117
118
    /**
119
     * Unregisters this error handler by restoring the PHP error and exception handlers.
120
     */
121
    public function unregister(): void
122
    {
123
        restore_error_handler();
124
        restore_exception_handler();
125
    }
126
127
    public function handleFatalError(): void
128
    {
129
        unset($this->memoryReserve);
130
        $error = error_get_last();
131
        if ($error !== null && ErrorException::isFatalError($error)) {
132
            $exception = new ErrorException(
133
                $error['message'],
134
                $error['type'],
135
                $error['type'],
136
                $error['file'],
137
                $error['line']
138
            );
139
            $this->handleThrowable($exception);
140
            exit(1);
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
141
        }
142
    }
143
144
    private function log(\Throwable $t/*, ServerRequestInterface $request*/): void
145
    {
146
        $renderer = new PlainTextRenderer();
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...
147
        $this->logger->error(
148
            $renderer->renderVerbose($t),
149
            [
150
                'throwable' => $t,
151
                //'request' => $request,
152
            ]
153
        );
154
    }
155
156
    public function withExposedDetails(): self
157
    {
158
        $new = clone $this;
159
        $new->exposeDetails = true;
160
        return $new;
161
    }
162
163
    public function withoutExposedDetails(): self
164
    {
165
        $new = clone $this;
166
        $new->exposeDetails = false;
167
        return $new;
168
    }
169
170
    public function setRenderer(ThrowableRendererInterface $defaultRenderer): void
171
    {
172
        $this->defaultRenderer = $defaultRenderer;
173
    }
174
}
175