Passed
Push — master ( 3dd254...ab4e12 )
by Alexander
02:54
created

ErrorHandler::handleCaughtThrowable()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 7
c 1
b 0
f 0
dl 0
loc 14
ccs 7
cts 7
cp 1
rs 10
cc 4
nc 8
nop 3
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler;
6
7
use Psr\Http\Message\ServerRequestInterface;
8
use Psr\Log\LoggerInterface;
9
use Throwable;
10
use Yiisoft\ErrorHandler\Exception\ErrorException;
11
use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer;
12
use Yiisoft\Http\Status;
13
14
use function error_get_last;
15
use function error_reporting;
16
use function function_exists;
17
use function ini_set;
18
use function http_response_code;
19
use function register_shutdown_function;
20
use function restore_error_handler;
21
use function restore_exception_handler;
22
use function set_error_handler;
23
use function set_exception_handler;
24
use function str_repeat;
25
26
/**
27
 * ErrorHandler handles out of memory errors, fatals, warnings, notices and exceptions.
28
 */
29
final class ErrorHandler
30
{
31
    /**
32
     * @var int The size of the reserved memory. A portion of memory is pre-allocated so that
33
     * when an out-of-memory issue occurs, the error handler is able to handle the error with
34
     * the help of this reserved memory. If you set this value to be 0, no memory will be reserved.
35
     * Defaults to 256KB.
36
     */
37
    private int $memoryReserveSize = 262_144;
0 ignored issues
show
Bug introduced by
The constant Yiisoft\ErrorHandler\262_144 was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
38
    private string $memoryReserve = '';
39
    private bool $debug = false;
40
41
    private LoggerInterface $logger;
42
    private ThrowableRendererInterface $defaultRenderer;
43
44 18
    public function __construct(LoggerInterface $logger, ThrowableRendererInterface $defaultRenderer)
45
    {
46 18
        $this->logger = $logger;
47 18
        $this->defaultRenderer = $defaultRenderer;
48 18
    }
49
50
    /**
51
     * Handles PHP execution errors such as warnings and notices.
52
     *
53
     * This method is used as a PHP error handler. It will raise an [[\ErrorException]].
54
     *
55
     * @param int $severity the level of the error raised.
56
     * @param string $message the error message.
57
     * @param string $file the filename that the error was raised in.
58
     * @param int $line the line number the error was raised at.
59
     *
60
     * @throws ErrorException
61
     */
62 3
    public function handleError(int $severity, string $message, string $file, int $line): void
63
    {
64 3
        if (!(error_reporting() & $severity)) {
65
            // This error code is not included in error_reporting
66 1
            return;
67
        }
68
69 2
        throw new ErrorException($message, $severity, $severity, $file, $line);
70
    }
71
72
    /**
73
     * Handle throwable and return output.
74
     *
75
     * @param Throwable $t
76
     * @param ThrowableRendererInterface|null $renderer
77
     * @param ServerRequestInterface|null $request
78
     *
79
     * @return ErrorData
80
     */
81 12
    public function handleCaughtThrowable(
82
        Throwable $t,
83
        ThrowableRendererInterface $renderer = null,
84
        ServerRequestInterface $request = null
85
    ): ErrorData {
86 12
        if ($renderer === null) {
87 5
            $renderer = $this->defaultRenderer;
88
        }
89
90
        try {
91 12
            $this->log($t, $request);
92 12
            return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request);
93 4
        } catch (Throwable $t) {
94 4
            return new ErrorData((string) $t);
95
        }
96
    }
97
98
    /**
99
     * Handle throwable, echo output and exit.
100
     *
101
     * @param Throwable $t
102
     */
103
    public function handleThrowable(Throwable $t): void
104
    {
105
        // disable error capturing to avoid recursive errors while handling exceptions
106
        $this->unregister();
107
        // set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent
108
        http_response_code(Status::INTERNAL_SERVER_ERROR);
109
110
        echo $this->handleCaughtThrowable($t);
111
        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...
112
    }
113
114
    /**
115
     * Enables and disables debug mode.
116
     *
117
     * Ensure that is is disabled in production environment since debug mode exposes sensitive details.
118
     *
119
     * @param bool $enable Enable/disable debugging mode.
120
     */
121 2
    public function debug(bool $enable = true): void
122
    {
123 2
        $this->debug = $enable;
124 2
    }
125
126
    /**
127
     * Register this error handler.
128
     */
129 1
    public function register(): void
130
    {
131 1
        $this->disableDisplayErrors();
132
133 1
        set_exception_handler([$this, 'handleThrowable']);
134
        /** @psalm-suppress InvalidArgument */
135 1
        set_error_handler([$this, 'handleError']);
136
137 1
        if ($this->memoryReserveSize > 0) {
138 1
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
139
        }
140
141 1
        register_shutdown_function([$this, 'handleFatalError']);
142 1
    }
143
144
    /**
145
     * Unregisters this error handler by restoring the PHP error and exception handlers.
146
     */
147
    public function unregister(): void
148
    {
149
        restore_error_handler();
150
        restore_exception_handler();
151
    }
152
153
    /**
154
     * Handle fatal error, echo output and exit.
155
     */
156
    public function handleFatalError(): void
157
    {
158
        unset($this->memoryReserve);
159
        $error = error_get_last();
160
161
        if ($error !== null && ErrorException::isFatalError($error)) {
162
            $exception = new ErrorException(
163
                $error['message'],
164
                $error['type'],
165
                $error['type'],
166
                $error['file'],
167
                $error['line']
168
            );
169
170
            $this->handleThrowable($exception);
171
        }
172
    }
173
174
    /**
175
     * Logs information about the error.
176
     *
177
     * @param Throwable $t
178
     * @param ServerRequestInterface|null $request
179
     */
180 12
    private function log(Throwable $t, ServerRequestInterface $request = null): void
181
    {
182 12
        $this->logger->error(
183 12
            (string) (new PlainTextRenderer())->renderVerbose($t, $request),
184 12
            ['throwable' => $t]
185
        );
186 12
    }
187
188
    /**
189
     * Disables the display of error.
190
     */
191 1
    private function disableDisplayErrors(): void
192
    {
193 1
        if (function_exists('ini_set')) {
194 1
            ini_set('display_errors', '0');
195
        }
196 1
    }
197
}
198