Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <fabien@symfony.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Symfony\Component\ErrorHandler;
13
 
14
use Psr\Log\LoggerInterface;
15
use Psr\Log\LogLevel;
16
use Symfony\Component\ErrorHandler\Error\FatalError;
17
use Symfony\Component\ErrorHandler\Error\OutOfMemoryError;
18
use Symfony\Component\ErrorHandler\ErrorEnhancer\ClassNotFoundErrorEnhancer;
19
use Symfony\Component\ErrorHandler\ErrorEnhancer\ErrorEnhancerInterface;
20
use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer;
21
use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer;
22
use Symfony\Component\ErrorHandler\ErrorRenderer\CliErrorRenderer;
23
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
24
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
25
 
26
/**
27
 * A generic ErrorHandler for the PHP engine.
28
 *
29
 * Provides five bit fields that control how errors are handled:
30
 * - thrownErrors: errors thrown as \ErrorException
31
 * - loggedErrors: logged errors, when not @-silenced
32
 * - scopedErrors: errors thrown or logged with their local context
33
 * - tracedErrors: errors logged with their stack trace
34
 * - screamedErrors: never @-silenced errors
35
 *
36
 * Each error level can be logged by a dedicated PSR-3 logger object.
37
 * Screaming only applies to logging.
38
 * Throwing takes precedence over logging.
39
 * Uncaught exceptions are logged as E_ERROR.
40
 * E_DEPRECATED and E_USER_DEPRECATED levels never throw.
41
 * E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw.
42
 * Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so.
43
 * As errors have a performance cost, repeated errors are all logged, so that the developer
44
 * can see them and weight them as more important to fix than others of the same level.
45
 *
46
 * @author Nicolas Grekas <p@tchwork.com>
47
 * @author Grégoire Pineau <lyrixx@lyrixx.info>
48
 *
49
 * @final
50
 */
51
class ErrorHandler
52
{
53
    private array $levels = [
54
        \E_DEPRECATED => 'Deprecated',
55
        \E_USER_DEPRECATED => 'User Deprecated',
56
        \E_NOTICE => 'Notice',
57
        \E_USER_NOTICE => 'User Notice',
58
        \E_STRICT => 'Runtime Notice',
59
        \E_WARNING => 'Warning',
60
        \E_USER_WARNING => 'User Warning',
61
        \E_COMPILE_WARNING => 'Compile Warning',
62
        \E_CORE_WARNING => 'Core Warning',
63
        \E_USER_ERROR => 'User Error',
64
        \E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
65
        \E_COMPILE_ERROR => 'Compile Error',
66
        \E_PARSE => 'Parse Error',
67
        \E_ERROR => 'Error',
68
        \E_CORE_ERROR => 'Core Error',
69
    ];
70
 
71
    private array $loggers = [
72
        \E_DEPRECATED => [null, LogLevel::INFO],
73
        \E_USER_DEPRECATED => [null, LogLevel::INFO],
74
        \E_NOTICE => [null, LogLevel::WARNING],
75
        \E_USER_NOTICE => [null, LogLevel::WARNING],
76
        \E_STRICT => [null, LogLevel::WARNING],
77
        \E_WARNING => [null, LogLevel::WARNING],
78
        \E_USER_WARNING => [null, LogLevel::WARNING],
79
        \E_COMPILE_WARNING => [null, LogLevel::WARNING],
80
        \E_CORE_WARNING => [null, LogLevel::WARNING],
81
        \E_USER_ERROR => [null, LogLevel::CRITICAL],
82
        \E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL],
83
        \E_COMPILE_ERROR => [null, LogLevel::CRITICAL],
84
        \E_PARSE => [null, LogLevel::CRITICAL],
85
        \E_ERROR => [null, LogLevel::CRITICAL],
86
        \E_CORE_ERROR => [null, LogLevel::CRITICAL],
87
    ];
88
 
89
    private int $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
90
    private int $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
91
    private int $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE
92
    private int $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE
93
    private int $loggedErrors = 0;
94
    private \Closure $configureException;
95
    private bool $debug;
96
 
97
    private bool $isRecursive = false;
98
    private bool $isRoot = false;
99
    private $exceptionHandler;
100
    private ?BufferingLogger $bootstrappingLogger = null;
101
 
102
    private static ?string $reservedMemory = null;
103
    private static array $silencedErrorCache = [];
104
    private static int $silencedErrorCount = 0;
105
    private static int $exitCode = 0;
106
 
107
    /**
108
     * Registers the error handler.
109
     */
110
    public static function register(self $handler = null, bool $replace = true): self
111
    {
112
        if (null === self::$reservedMemory) {
113
            self::$reservedMemory = str_repeat('x', 32768);
114
            register_shutdown_function(__CLASS__.'::handleFatalError');
115
        }
116
 
117
        if ($handlerIsNew = null === $handler) {
118
            $handler = new static();
119
        }
120
 
121
        if (null === $prev = set_error_handler([$handler, 'handleError'])) {
122
            restore_error_handler();
123
            // Specifying the error types earlier would expose us to https://bugs.php.net/63206
124
            set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors);
125
            $handler->isRoot = true;
126
        }
127
 
128
        if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) {
129
            $handler = $prev[0];
130
            $replace = false;
131
        }
132
        if (!$replace && $prev) {
133
            restore_error_handler();
134
            $handlerIsRegistered = \is_array($prev) && $handler === $prev[0];
135
        } else {
136
            $handlerIsRegistered = true;
137
        }
138
        if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) {
139
            restore_exception_handler();
140
            if (!$handlerIsRegistered) {
141
                $handler = $prev[0];
142
            } elseif ($handler !== $prev[0] && $replace) {
143
                set_exception_handler([$handler, 'handleException']);
144
                $p = $prev[0]->setExceptionHandler(null);
145
                $handler->setExceptionHandler($p);
146
                $prev[0]->setExceptionHandler($p);
147
            }
148
        } else {
149
            $handler->setExceptionHandler($prev ?? [$handler, 'renderException']);
150
        }
151
 
152
        $handler->throwAt(\E_ALL & $handler->thrownErrors, true);
153
 
154
        return $handler;
155
    }
156
 
157
    /**
158
     * Calls a function and turns any PHP error into \ErrorException.
159
     *
160
     * @throws \ErrorException When $function(...$arguments) triggers a PHP error
161
     */
162
    public static function call(callable $function, mixed ...$arguments): mixed
163
    {
164
        set_error_handler(static function (int $type, string $message, string $file, int $line) {
165
            if (__FILE__ === $file) {
166
                $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 3);
167
                $file = $trace[2]['file'] ?? $file;
168
                $line = $trace[2]['line'] ?? $line;
169
            }
170
 
171
            throw new \ErrorException($message, 0, $type, $file, $line);
172
        });
173
 
174
        try {
175
            return $function(...$arguments);
176
        } finally {
177
            restore_error_handler();
178
        }
179
    }
180
 
181
    public function __construct(BufferingLogger $bootstrappingLogger = null, bool $debug = false)
182
    {
183
        if ($bootstrappingLogger) {
184
            $this->bootstrappingLogger = $bootstrappingLogger;
185
            $this->setDefaultLogger($bootstrappingLogger);
186
        }
187
        $traceReflector = new \ReflectionProperty(\Exception::class, 'trace');
188
        $this->configureException = \Closure::bind(static function ($e, $trace, $file = null, $line = null) use ($traceReflector) {
189
            $traceReflector->setValue($e, $trace);
190
            $e->file = $file ?? $e->file;
191
            $e->line = $line ?? $e->line;
192
        }, null, new class() extends \Exception {
193
        });
194
        $this->debug = $debug;
195
    }
196
 
197
    /**
198
     * Sets a logger to non assigned errors levels.
199
     *
200
     * @param LoggerInterface $logger  A PSR-3 logger to put as default for the given levels
201
     * @param array|int|null  $levels  An array map of E_* to LogLevel::* or an integer bit field of E_* constants
202
     * @param bool            $replace Whether to replace or not any existing logger
203
     */
204
    public function setDefaultLogger(LoggerInterface $logger, array|int|null $levels = \E_ALL, bool $replace = false): void
205
    {
206
        $loggers = [];
207
 
208
        if (\is_array($levels)) {
209
            foreach ($levels as $type => $logLevel) {
210
                if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) {
211
                    $loggers[$type] = [$logger, $logLevel];
212
                }
213
            }
214
        } else {
215
            $levels ??= \E_ALL;
216
            foreach ($this->loggers as $type => $log) {
217
                if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) {
218
                    $log[0] = $logger;
219
                    $loggers[$type] = $log;
220
                }
221
            }
222
        }
223
 
224
        $this->setLoggers($loggers);
225
    }
226
 
227
    /**
228
     * Sets a logger for each error level.
229
     *
230
     * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map
231
     *
232
     * @throws \InvalidArgumentException
233
     */
234
    public function setLoggers(array $loggers): array
235
    {
236
        $prevLogged = $this->loggedErrors;
237
        $prev = $this->loggers;
238
        $flush = [];
239
 
240
        foreach ($loggers as $type => $log) {
241
            if (!isset($prev[$type])) {
242
                throw new \InvalidArgumentException('Unknown error type: '.$type);
243
            }
244
            if (!\is_array($log)) {
245
                $log = [$log];
246
            } elseif (!\array_key_exists(0, $log)) {
247
                throw new \InvalidArgumentException('No logger provided.');
248
            }
249
            if (null === $log[0]) {
250
                $this->loggedErrors &= ~$type;
251
            } elseif ($log[0] instanceof LoggerInterface) {
252
                $this->loggedErrors |= $type;
253
            } else {
254
                throw new \InvalidArgumentException('Invalid logger provided.');
255
            }
256
            $this->loggers[$type] = $log + $prev[$type];
257
 
258
            if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) {
259
                $flush[$type] = $type;
260
            }
261
        }
262
        $this->reRegister($prevLogged | $this->thrownErrors);
263
 
264
        if ($flush) {
265
            foreach ($this->bootstrappingLogger->cleanLogs() as $log) {
266
                $type = ThrowableUtils::getSeverity($log[2]['exception']);
267
                if (!isset($flush[$type])) {
268
                    $this->bootstrappingLogger->log($log[0], $log[1], $log[2]);
269
                } elseif ($this->loggers[$type][0]) {
270
                    $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]);
271
                }
272
            }
273
        }
274
 
275
        return $prev;
276
    }
277
 
278
    public function setExceptionHandler(?callable $handler): ?callable
279
    {
280
        $prev = $this->exceptionHandler;
281
        $this->exceptionHandler = $handler;
282
 
283
        return $prev;
284
    }
285
 
286
    /**
287
     * Sets the PHP error levels that throw an exception when a PHP error occurs.
288
     *
289
     * @param int  $levels  A bit field of E_* constants for thrown errors
290
     * @param bool $replace Replace or amend the previous value
291
     */
292
    public function throwAt(int $levels, bool $replace = false): int
293
    {
294
        $prev = $this->thrownErrors;
295
        $this->thrownErrors = ($levels | \E_RECOVERABLE_ERROR | \E_USER_ERROR) & ~\E_USER_DEPRECATED & ~\E_DEPRECATED;
296
        if (!$replace) {
297
            $this->thrownErrors |= $prev;
298
        }
299
        $this->reRegister($prev | $this->loggedErrors);
300
 
301
        return $prev;
302
    }
303
 
304
    /**
305
     * Sets the PHP error levels for which local variables are preserved.
306
     *
307
     * @param int  $levels  A bit field of E_* constants for scoped errors
308
     * @param bool $replace Replace or amend the previous value
309
     */
310
    public function scopeAt(int $levels, bool $replace = false): int
311
    {
312
        $prev = $this->scopedErrors;
313
        $this->scopedErrors = $levels;
314
        if (!$replace) {
315
            $this->scopedErrors |= $prev;
316
        }
317
 
318
        return $prev;
319
    }
320
 
321
    /**
322
     * Sets the PHP error levels for which the stack trace is preserved.
323
     *
324
     * @param int  $levels  A bit field of E_* constants for traced errors
325
     * @param bool $replace Replace or amend the previous value
326
     */
327
    public function traceAt(int $levels, bool $replace = false): int
328
    {
329
        $prev = $this->tracedErrors;
330
        $this->tracedErrors = $levels;
331
        if (!$replace) {
332
            $this->tracedErrors |= $prev;
333
        }
334
 
335
        return $prev;
336
    }
337
 
338
    /**
339
     * Sets the error levels where the @-operator is ignored.
340
     *
341
     * @param int  $levels  A bit field of E_* constants for screamed errors
342
     * @param bool $replace Replace or amend the previous value
343
     */
344
    public function screamAt(int $levels, bool $replace = false): int
345
    {
346
        $prev = $this->screamedErrors;
347
        $this->screamedErrors = $levels;
348
        if (!$replace) {
349
            $this->screamedErrors |= $prev;
350
        }
351
 
352
        return $prev;
353
    }
354
 
355
    /**
356
     * Re-registers as a PHP error handler if levels changed.
357
     */
358
    private function reRegister(int $prev): void
359
    {
360
        if ($prev !== ($this->thrownErrors | $this->loggedErrors)) {
361
            $handler = set_error_handler('is_int');
362
            $handler = \is_array($handler) ? $handler[0] : null;
363
            restore_error_handler();
364
            if ($handler === $this) {
365
                restore_error_handler();
366
                if ($this->isRoot) {
367
                    set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors);
368
                } else {
369
                    set_error_handler([$this, 'handleError']);
370
                }
371
            }
372
        }
373
    }
374
 
375
    /**
376
     * Handles errors by filtering then logging them according to the configured bit fields.
377
     *
378
     * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself
379
     *
380
     * @throws \ErrorException When $this->thrownErrors requests so
381
     *
382
     * @internal
383
     */
384
    public function handleError(int $type, string $message, string $file, int $line): bool
385
    {
386
        if (\E_WARNING === $type && '"' === $message[0] && str_contains($message, '" targeting switch is equivalent to "break')) {
387
            $type = \E_DEPRECATED;
388
        }
389
 
390
        // Level is the current error reporting level to manage silent error.
391
        $level = error_reporting();
392
        $silenced = 0 === ($level & $type);
393
        // Strong errors are not authorized to be silenced.
394
        $level |= \E_RECOVERABLE_ERROR | \E_USER_ERROR | \E_DEPRECATED | \E_USER_DEPRECATED;
395
        $log = $this->loggedErrors & $type;
396
        $throw = $this->thrownErrors & $type & $level;
397
        $type &= $level | $this->screamedErrors;
398
 
399
        // Never throw on warnings triggered by assert()
400
        if (\E_WARNING === $type && 'a' === $message[0] && 0 === strncmp($message, 'assert(): ', 10)) {
401
            $throw = 0;
402
        }
403
 
404
        if (!$type || (!$log && !$throw)) {
405
            return false;
406
        }
407
 
408
        $logMessage = $this->levels[$type].': '.$message;
409
 
410
        if (!$throw && !($type & $level)) {
411
            if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) {
412
                $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5), $type, $file, $line, false) : [];
413
                $errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace);
414
            } elseif (isset(self::$silencedErrorCache[$id][$message])) {
415
                $lightTrace = null;
416
                $errorAsException = self::$silencedErrorCache[$id][$message];
417
                ++$errorAsException->count;
418
            } else {
419
                $lightTrace = [];
420
                $errorAsException = null;
421
            }
422
 
423
            if (100 < ++self::$silencedErrorCount) {
424
                self::$silencedErrorCache = $lightTrace = [];
425
                self::$silencedErrorCount = 1;
426
            }
427
            if ($errorAsException) {
428
                self::$silencedErrorCache[$id][$message] = $errorAsException;
429
            }
430
            if (null === $lightTrace) {
431
                return true;
432
            }
433
        } else {
434
            if (str_contains($message, '@anonymous')) {
435
                $backtrace = debug_backtrace(false, 5);
436
 
437
                for ($i = 1; isset($backtrace[$i]); ++$i) {
438
                    if (isset($backtrace[$i]['function'], $backtrace[$i]['args'][0])
439
                        && ('trigger_error' === $backtrace[$i]['function'] || 'user_error' === $backtrace[$i]['function'])
440
                    ) {
441
                        if ($backtrace[$i]['args'][0] !== $message) {
442
                            $message = $this->parseAnonymousClass($backtrace[$i]['args'][0]);
443
                            $logMessage = $this->levels[$type].': '.$message;
444
                        }
445
 
446
                        break;
447
                    }
448
                }
449
            }
450
 
451
            $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line);
452
 
453
            if ($throw || $this->tracedErrors & $type) {
454
                $backtrace = $errorAsException->getTrace();
455
                $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw);
456
                ($this->configureException)($errorAsException, $lightTrace, $file, $line);
457
            } else {
458
                ($this->configureException)($errorAsException, []);
459
                $backtrace = [];
460
            }
461
        }
462
 
463
        if ($throw) {
464
            throw $errorAsException;
465
        }
466
 
467
        if ($this->isRecursive) {
468
            $log = 0;
469
        } else {
470
            try {
471
                $this->isRecursive = true;
472
                $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG;
473
                $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []);
474
            } finally {
475
                $this->isRecursive = false;
476
            }
477
        }
478
 
479
        return !$silenced && $type && $log;
480
    }
481
 
482
    /**
483
     * Handles an exception by logging then forwarding it to another handler.
484
     *
485
     * @internal
486
     */
487
    public function handleException(\Throwable $exception)
488
    {
489
        $handlerException = null;
490
 
491
        if (!$exception instanceof FatalError) {
492
            self::$exitCode = 255;
493
 
494
            $type = ThrowableUtils::getSeverity($exception);
495
        } else {
496
            $type = $exception->getError()['type'];
497
        }
498
 
499
        if ($this->loggedErrors & $type) {
500
            if (str_contains($message = $exception->getMessage(), "@anonymous\0")) {
501
                $message = $this->parseAnonymousClass($message);
502
            }
503
 
504
            if ($exception instanceof FatalError) {
505
                $message = 'Fatal '.$message;
506
            } elseif ($exception instanceof \Error) {
507
                $message = 'Uncaught Error: '.$message;
508
            } elseif ($exception instanceof \ErrorException) {
509
                $message = 'Uncaught '.$message;
510
            } else {
511
                $message = 'Uncaught Exception: '.$message;
512
            }
513
 
514
            try {
515
                $this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]);
516
            } catch (\Throwable $handlerException) {
517
            }
518
        }
519
 
520
        if (!$exception instanceof OutOfMemoryError) {
521
            foreach ($this->getErrorEnhancers() as $errorEnhancer) {
522
                if ($e = $errorEnhancer->enhance($exception)) {
523
                    $exception = $e;
524
                    break;
525
                }
526
            }
527
        }
528
 
529
        $exceptionHandler = $this->exceptionHandler;
530
        $this->exceptionHandler = [$this, 'renderException'];
531
 
532
        if (null === $exceptionHandler || $exceptionHandler === $this->exceptionHandler) {
533
            $this->exceptionHandler = null;
534
        }
535
 
536
        try {
537
            if (null !== $exceptionHandler) {
538
                return $exceptionHandler($exception);
539
            }
540
            $handlerException ??= $exception;
541
        } catch (\Throwable $handlerException) {
542
        }
543
        if ($exception === $handlerException && null === $this->exceptionHandler) {
544
            self::$reservedMemory = null; // Disable the fatal error handler
545
            throw $exception; // Give back $exception to the native handler
546
        }
547
 
548
        $loggedErrors = $this->loggedErrors;
549
        if ($exception === $handlerException) {
550
            $this->loggedErrors &= ~$type;
551
        }
552
 
553
        try {
554
            $this->handleException($handlerException);
555
        } finally {
556
            $this->loggedErrors = $loggedErrors;
557
        }
558
    }
559
 
560
    /**
561
     * Shutdown registered function for handling PHP fatal errors.
562
     *
563
     * @param array|null $error An array as returned by error_get_last()
564
     *
565
     * @internal
566
     */
567
    public static function handleFatalError(array $error = null): void
568
    {
569
        if (null === self::$reservedMemory) {
570
            return;
571
        }
572
 
573
        $handler = self::$reservedMemory = null;
574
        $handlers = [];
575
        $previousHandler = null;
576
        $sameHandlerLimit = 10;
577
 
578
        while (!\is_array($handler) || !$handler[0] instanceof self) {
579
            $handler = set_exception_handler('is_int');
580
            restore_exception_handler();
581
 
582
            if (!$handler) {
583
                break;
584
            }
585
            restore_exception_handler();
586
 
587
            if ($handler !== $previousHandler) {
588
                array_unshift($handlers, $handler);
589
                $previousHandler = $handler;
590
            } elseif (0 === --$sameHandlerLimit) {
591
                $handler = null;
592
                break;
593
            }
594
        }
595
        foreach ($handlers as $h) {
596
            set_exception_handler($h);
597
        }
598
        if (!$handler) {
599
            return;
600
        }
601
        if ($handler !== $h) {
602
            $handler[0]->setExceptionHandler($h);
603
        }
604
        $handler = $handler[0];
605
        $handlers = [];
606
 
607
        if ($exit = null === $error) {
608
            $error = error_get_last();
609
        }
610
 
611
        if ($error && $error['type'] &= \E_PARSE | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR) {
612
            // Let's not throw anymore but keep logging
613
            $handler->throwAt(0, true);
614
            $trace = $error['backtrace'] ?? null;
615
 
616
            if (str_starts_with($error['message'], 'Allowed memory') || str_starts_with($error['message'], 'Out of memory')) {
617
                $fatalError = new OutOfMemoryError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, false, $trace);
618
            } else {
619
                $fatalError = new FatalError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, true, $trace);
620
            }
621
        } else {
622
            $fatalError = null;
623
        }
624
 
625
        try {
626
            if (null !== $fatalError) {
627
                self::$exitCode = 255;
628
                $handler->handleException($fatalError);
629
            }
630
        } catch (FatalError) {
631
            // Ignore this re-throw
632
        }
633
 
634
        if ($exit && self::$exitCode) {
635
            $exitCode = self::$exitCode;
636
            register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); });
637
        }
638
    }
639
 
640
    /**
641
     * Renders the given exception.
642
     *
643
     * As this method is mainly called during boot where nothing is yet available,
644
     * the output is always either HTML or CLI depending where PHP runs.
645
     */
646
    private function renderException(\Throwable $exception): void
647
    {
648
        $renderer = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliErrorRenderer() : new HtmlErrorRenderer($this->debug);
649
 
650
        $exception = $renderer->render($exception);
651
 
652
        if (!headers_sent()) {
653
            http_response_code($exception->getStatusCode());
654
 
655
            foreach ($exception->getHeaders() as $name => $value) {
656
                header($name.': '.$value, false);
657
            }
658
        }
659
 
660
        echo $exception->getAsString();
661
    }
662
 
663
    /**
664
     * Override this method if you want to define more error enhancers.
665
     *
666
     * @return ErrorEnhancerInterface[]
667
     */
668
    protected function getErrorEnhancers(): iterable
669
    {
670
        return [
671
            new UndefinedFunctionErrorEnhancer(),
672
            new UndefinedMethodErrorEnhancer(),
673
            new ClassNotFoundErrorEnhancer(),
674
        ];
675
    }
676
 
677
    /**
678
     * Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader.
679
     */
680
    private function cleanTrace(array $backtrace, int $type, string &$file, int &$line, bool $throw): array
681
    {
682
        $lightTrace = $backtrace;
683
 
684
        for ($i = 0; isset($backtrace[$i]); ++$i) {
685
            if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) {
686
                $lightTrace = \array_slice($lightTrace, 1 + $i);
687
                break;
688
            }
689
        }
690
        if (\E_USER_DEPRECATED === $type) {
691
            for ($i = 0; isset($lightTrace[$i]); ++$i) {
692
                if (!isset($lightTrace[$i]['file'], $lightTrace[$i]['line'], $lightTrace[$i]['function'])) {
693
                    continue;
694
                }
695
                if (!isset($lightTrace[$i]['class']) && 'trigger_deprecation' === $lightTrace[$i]['function']) {
696
                    $file = $lightTrace[$i]['file'];
697
                    $line = $lightTrace[$i]['line'];
698
                    $lightTrace = \array_slice($lightTrace, 1 + $i);
699
                    break;
700
                }
701
            }
702
        }
703
        if (class_exists(DebugClassLoader::class, false)) {
704
            for ($i = \count($lightTrace) - 2; 0 < $i; --$i) {
705
                if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) {
706
                    array_splice($lightTrace, --$i, 2);
707
                }
708
            }
709
        }
710
        if (!($throw || $this->scopedErrors & $type)) {
711
            for ($i = 0; isset($lightTrace[$i]); ++$i) {
712
                unset($lightTrace[$i]['args'], $lightTrace[$i]['object']);
713
            }
714
        }
715
 
716
        return $lightTrace;
717
    }
718
 
719
    /**
720
     * Parse the error message by removing the anonymous class notation
721
     * and using the parent class instead if possible.
722
     */
723
    private function parseAnonymousClass(string $message): string
724
    {
725
        return preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', static function ($m) {
726
            return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
727
        }, $message);
728
    }
729
}