Issues (5)

src/ErrorHandler.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\ErrorHandler;
6
7
use Psr\EventDispatcher\EventDispatcherInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Log\LoggerInterface;
10
use Throwable;
11
use Yiisoft\ErrorHandler\Event\ApplicationError;
12
use Yiisoft\ErrorHandler\Exception\ErrorException;
13
use Yiisoft\ErrorHandler\Renderer\PlainTextRenderer;
14
use Yiisoft\Http\Status;
15
16
use function error_get_last;
17
use function error_reporting;
18
use function function_exists;
19
use function http_response_code;
20
use function ini_set;
21
use function register_shutdown_function;
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
A parse error occurred: Syntax error, unexpected T_STRING, expecting ',' or ';' on line 37 at column 40
Loading history...
38
    private string $memoryReserve = '';
39
    private bool $debug = false;
40
    private ?string $workingDirectory = null;
41
    private bool $enabled = false;
42
    private bool $initialized = false;
43
44
    /**
45
     * @param LoggerInterface $logger Logger to write errors to.
46
     * @param ThrowableRendererInterface $defaultRenderer Default throwable renderer.
47
     * @param EventDispatcherInterface|null $eventDispatcher Event dispatcher for error events.
48
     * @param int $exitShutdownHandlerDepth Depth of the exit() shutdown handler to ensure it's executed last.
49
     */
50 19
    public function __construct(
51
        private LoggerInterface $logger,
52
        private ThrowableRendererInterface $defaultRenderer,
53
        private ?EventDispatcherInterface $eventDispatcher = null,
54
        private int $exitShutdownHandlerDepth = 2
55
    ) {
56 19
    }
57
58
    /**
59
     * Handles throwable and returns error data.
60
     *
61
     * @param ThrowableRendererInterface|null $renderer
62
     * @param ServerRequestInterface|null $request
63
     */
64 14
    public function handle(
65
        Throwable $t,
66
        ThrowableRendererInterface $renderer = null,
67
        ServerRequestInterface $request = null
68
    ): ErrorData {
69 14
        $renderer ??= $this->defaultRenderer;
70
71
        try {
72 14
            $this->logger->error(PlainTextRenderer::throwableToString($t), ['throwable' => $t]);
73 14
            return $this->debug ? $renderer->renderVerbose($t, $request) : $renderer->render($t, $request);
74 4
        } catch (Throwable $t) {
75 4
            return new ErrorData((string) $t);
76
        }
77
    }
78
79
    /**
80
     * Enables and disables debug mode.
81
     *
82
     * Ensure that is is disabled in production environment since debug mode exposes sensitive details.
83
     *
84
     * @param bool $enable Enable/disable debugging mode.
85
     */
86 2
    public function debug(bool $enable = true): void
87
    {
88 2
        $this->debug = $enable;
89
    }
90
91
    /**
92
     * Sets the size of the reserved memory.
93
     *
94
     * @param int $size The size of the reserved memory.
95
     *
96
     * @see $memoryReserveSize
97
     */
98 6
    public function memoryReserveSize(int $size): void
99
    {
100 6
        $this->memoryReserveSize = $size;
101
    }
102
103
    /**
104
     * Register PHP exception and error handlers and enable this error handler.
105
     */
106 2
    public function register(): void
107
    {
108 2
        if ($this->enabled) {
109
            return;
110
        }
111
112 2
        if ($this->memoryReserveSize > 0) {
113
            $this->memoryReserve = str_repeat('x', $this->memoryReserveSize);
114
        }
115
116 2
        $this->initializeOnce();
117
118
        // Handles throwable that isn't caught otherwise, echo output and exit.
119 2
        set_exception_handler(function (Throwable $t): void {
120
            if (!$this->enabled) {
121
                return;
122
            }
123
124
            $this->renderThrowableAndTerminate($t);
125 2
        });
126
127
        // Handles PHP execution errors such as warnings and notices.
128 2
        set_error_handler(function (int $severity, string $message, string $file, int $line): bool {
129 2
            if (!$this->enabled) {
130
                return false;
131
            }
132
133 2
            if (!(error_reporting() & $severity)) {
134
                // This error code is not included in error_reporting.
135
                return true;
136
            }
137
138 2
            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
139 2
            throw new ErrorException($message, $severity, $severity, $file, $line, null, $backtrace);
140 2
        });
141
142 2
        $this->enabled = true;
143
    }
144
145
    /**
146
     * Disable this error handler.
147
     */
148 1
    public function unregister(): void
149
    {
150 1
        if (!$this->enabled) {
151
            return;
152
        }
153
154 1
        $this->memoryReserve = '';
155
156 1
        $this->enabled = false;
157
    }
158
159 2
    private function initializeOnce(): void
160
    {
161 2
        if ($this->initialized) {
162
            return;
163
        }
164
165
        // Disables the display of error.
166 2
        if (function_exists('ini_set')) {
167 2
            ini_set('display_errors', '0');
168
        }
169
170
        // Handles fatal error.
171 2
        register_shutdown_function(function (): void {
172
            if (!$this->enabled) {
173
                return;
174
            }
175
176
            $this->memoryReserve = '';
177
            $e = error_get_last();
178
179
            if ($e !== null && ErrorException::isFatalError($e)) {
180
                $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
181
                $error = new ErrorException(
182
                    $e['message'],
183
                    $e['type'],
184
                    $e['type'],
185
                    $e['file'],
186
                    $e['line'],
187
                    null,
188
                    $backtrace
189
                );
190
                $this->renderThrowableAndTerminate($error);
191
            }
192 2
        });
193
194 2
        if (!(PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg')) {
195
            $this->workingDirectory = getcwd();
196
        }
197
198 2
        $this->initialized = true;
199
    }
200
201
    /**
202
     * Renders the throwable and terminates the script.
203
     */
204
    private function renderThrowableAndTerminate(Throwable $t): void
205
    {
206
        if (!empty($this->workingDirectory)) {
207
            chdir($this->workingDirectory);
208
        }
209
        // Disable error capturing to avoid recursive errors while handling exceptions.
210
        $this->unregister();
211
        // Set preventive HTTP status code to 500 in case error handling somehow fails and headers are sent.
212
        http_response_code(Status::INTERNAL_SERVER_ERROR);
213
214
        echo $this->handle($t);
215
        $this->eventDispatcher?->dispatch(new ApplicationError($t));
216
217
        $handler = $this->wrapShutdownHandler(
218
            static function (): void {
219
                exit(1);
220
            },
221
            $this->exitShutdownHandlerDepth
222
        );
223
224
        register_shutdown_function($handler);
225
    }
226
227
    /**
228
     * Wraps shutdown handler into another shutdown handler to ensure it is called last after all other shutdown
229
     * functions, even those added to the end.
230
     *
231
     * @param callable $handler Shutdown handler to wrap.
232
     * @param int $depth Wrapping depth.
233
     * @return callable Wrapped handler.
234
     */
235
    private function wrapShutdownHandler(callable $handler, int $depth): callable
236
    {
237
        $currentDepth = 0;
238
        while ($currentDepth < $depth) {
239
            $handler = static function() use ($handler): void {
240
                register_shutdown_function($handler);
241
            };
242
            $currentDepth++;
243
        }
244
        return $handler;
245
    }
246
}
247