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
12namespace Symfony\Component\ErrorHandler;
13
14use Psr\Log\LoggerInterface;
15use Psr\Log\LogLevel;
16use Symfony\Component\ErrorHandler\Error\FatalError;
17use Symfony\Component\ErrorHandler\Error\OutOfMemoryError;
18use Symfony\Component\ErrorHandler\ErrorEnhancer\ClassNotFoundErrorEnhancer;
19use Symfony\Component\ErrorHandler\ErrorEnhancer\ErrorEnhancerInterface;
20use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer;
21use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer;
22use Symfony\Component\ErrorHandler\ErrorRenderer\CliErrorRenderer;
23use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
24use 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 */
51class ErrorHandler
52{
53    private $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 $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 $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
90    private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
91    private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE
92    private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE
93    private $loggedErrors = 0;
94    private $configureException;
95    private $debug;
96
97    private $isRecursive = 0;
98    private $isRoot = false;
99    private $exceptionHandler;
100    private $bootstrappingLogger;
101
102    private static $reservedMemory;
103    private static $toStringException;
104    private static $silencedErrorCache = [];
105    private static $silencedErrorCount = 0;
106    private static $exitCode = 0;
107
108    /**
109     * Registers the error handler.
110     */
111    public static function register(self $handler = null, bool $replace = true): self
112    {
113        if (null === self::$reservedMemory) {
114            self::$reservedMemory = str_repeat('x', 10240);
115            register_shutdown_function(__CLASS__.'::handleFatalError');
116        }
117
118        if ($handlerIsNew = null === $handler) {
119            $handler = new static();
120        }
121
122        if (null === $prev = set_error_handler([$handler, 'handleError'])) {
123            restore_error_handler();
124            // Specifying the error types earlier would expose us to https://bugs.php.net/63206
125            set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors);
126            $handler->isRoot = true;
127        }
128
129        if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) {
130            $handler = $prev[0];
131            $replace = false;
132        }
133        if (!$replace && $prev) {
134            restore_error_handler();
135            $handlerIsRegistered = \is_array($prev) && $handler === $prev[0];
136        } else {
137            $handlerIsRegistered = true;
138        }
139        if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) {
140            restore_exception_handler();
141            if (!$handlerIsRegistered) {
142                $handler = $prev[0];
143            } elseif ($handler !== $prev[0] && $replace) {
144                set_exception_handler([$handler, 'handleException']);
145                $p = $prev[0]->setExceptionHandler(null);
146                $handler->setExceptionHandler($p);
147                $prev[0]->setExceptionHandler($p);
148            }
149        } else {
150            $handler->setExceptionHandler($prev ?? [$handler, 'renderException']);
151        }
152
153        $handler->throwAt(\E_ALL & $handler->thrownErrors, true);
154
155        return $handler;
156    }
157
158    /**
159     * Calls a function and turns any PHP error into \ErrorException.
160     *
161     * @return mixed What $function(...$arguments) returns
162     *
163     * @throws \ErrorException When $function(...$arguments) triggers a PHP error
164     */
165    public static function call(callable $function, ...$arguments)
166    {
167        set_error_handler(static function (int $type, string $message, string $file, int $line) {
168            if (__FILE__ === $file) {
169                $trace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 3);
170                $file = $trace[2]['file'] ?? $file;
171                $line = $trace[2]['line'] ?? $line;
172            }
173
174            throw new \ErrorException($message, 0, $type, $file, $line);
175        });
176
177        try {
178            return $function(...$arguments);
179        } finally {
180            restore_error_handler();
181        }
182    }
183
184    public function __construct(BufferingLogger $bootstrappingLogger = null, bool $debug = false)
185    {
186        if ($bootstrappingLogger) {
187            $this->bootstrappingLogger = $bootstrappingLogger;
188            $this->setDefaultLogger($bootstrappingLogger);
189        }
190        $traceReflector = new \ReflectionProperty(\Exception::class, 'trace');
191        $traceReflector->setAccessible(true);
192        $this->configureException = \Closure::bind(static function ($e, $trace, $file = null, $line = null) use ($traceReflector) {
193            $traceReflector->setValue($e, $trace);
194            $e->file = $file ?? $e->file;
195            $e->line = $line ?? $e->line;
196        }, null, new class() extends \Exception {
197        });
198        $this->debug = $debug;
199    }
200
201    /**
202     * Sets a logger to non assigned errors levels.
203     *
204     * @param LoggerInterface $logger  A PSR-3 logger to put as default for the given levels
205     * @param array|int       $levels  An array map of E_* to LogLevel::* or an integer bit field of E_* constants
206     * @param bool            $replace Whether to replace or not any existing logger
207     */
208    public function setDefaultLogger(LoggerInterface $logger, $levels = \E_ALL, bool $replace = false): void
209    {
210        $loggers = [];
211
212        if (\is_array($levels)) {
213            foreach ($levels as $type => $logLevel) {
214                if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) {
215                    $loggers[$type] = [$logger, $logLevel];
216                }
217            }
218        } else {
219            if (null === $levels) {
220                $levels = \E_ALL;
221            }
222            foreach ($this->loggers as $type => $log) {
223                if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) {
224                    $log[0] = $logger;
225                    $loggers[$type] = $log;
226                }
227            }
228        }
229
230        $this->setLoggers($loggers);
231    }
232
233    /**
234     * Sets a logger for each error level.
235     *
236     * @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map
237     *
238     * @return array The previous map
239     *
240     * @throws \InvalidArgumentException
241     */
242    public function setLoggers(array $loggers): array
243    {
244        $prevLogged = $this->loggedErrors;
245        $prev = $this->loggers;
246        $flush = [];
247
248        foreach ($loggers as $type => $log) {
249            if (!isset($prev[$type])) {
250                throw new \InvalidArgumentException('Unknown error type: '.$type);
251            }
252            if (!\is_array($log)) {
253                $log = [$log];
254            } elseif (!\array_key_exists(0, $log)) {
255                throw new \InvalidArgumentException('No logger provided.');
256            }
257            if (null === $log[0]) {
258                $this->loggedErrors &= ~$type;
259            } elseif ($log[0] instanceof LoggerInterface) {
260                $this->loggedErrors |= $type;
261            } else {
262                throw new \InvalidArgumentException('Invalid logger provided.');
263            }
264            $this->loggers[$type] = $log + $prev[$type];
265
266            if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) {
267                $flush[$type] = $type;
268            }
269        }
270        $this->reRegister($prevLogged | $this->thrownErrors);
271
272        if ($flush) {
273            foreach ($this->bootstrappingLogger->cleanLogs() as $log) {
274                $type = ThrowableUtils::getSeverity($log[2]['exception']);
275                if (!isset($flush[$type])) {
276                    $this->bootstrappingLogger->log($log[0], $log[1], $log[2]);
277                } elseif ($this->loggers[$type][0]) {
278                    $this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]);
279                }
280            }
281        }
282
283        return $prev;
284    }
285
286    /**
287     * Sets a user exception handler.
288     *
289     * @param callable(\Throwable $e)|null $handler
290     *
291     * @return callable|null The previous exception handler
292     */
293    public function setExceptionHandler(?callable $handler): ?callable
294    {
295        $prev = $this->exceptionHandler;
296        $this->exceptionHandler = $handler;
297
298        return $prev;
299    }
300
301    /**
302     * Sets the PHP error levels that throw an exception when a PHP error occurs.
303     *
304     * @param int  $levels  A bit field of E_* constants for thrown errors
305     * @param bool $replace Replace or amend the previous value
306     *
307     * @return int The previous value
308     */
309    public function throwAt(int $levels, bool $replace = false): int
310    {
311        $prev = $this->thrownErrors;
312        $this->thrownErrors = ($levels | \E_RECOVERABLE_ERROR | \E_USER_ERROR) & ~\E_USER_DEPRECATED & ~\E_DEPRECATED;
313        if (!$replace) {
314            $this->thrownErrors |= $prev;
315        }
316        $this->reRegister($prev | $this->loggedErrors);
317
318        return $prev;
319    }
320
321    /**
322     * Sets the PHP error levels for which local variables are preserved.
323     *
324     * @param int  $levels  A bit field of E_* constants for scoped errors
325     * @param bool $replace Replace or amend the previous value
326     *
327     * @return int The previous value
328     */
329    public function scopeAt(int $levels, bool $replace = false): int
330    {
331        $prev = $this->scopedErrors;
332        $this->scopedErrors = $levels;
333        if (!$replace) {
334            $this->scopedErrors |= $prev;
335        }
336
337        return $prev;
338    }
339
340    /**
341     * Sets the PHP error levels for which the stack trace is preserved.
342     *
343     * @param int  $levels  A bit field of E_* constants for traced errors
344     * @param bool $replace Replace or amend the previous value
345     *
346     * @return int The previous value
347     */
348    public function traceAt(int $levels, bool $replace = false): int
349    {
350        $prev = $this->tracedErrors;
351        $this->tracedErrors = (int) $levels;
352        if (!$replace) {
353            $this->tracedErrors |= $prev;
354        }
355
356        return $prev;
357    }
358
359    /**
360     * Sets the error levels where the @-operator is ignored.
361     *
362     * @param int  $levels  A bit field of E_* constants for screamed errors
363     * @param bool $replace Replace or amend the previous value
364     *
365     * @return int The previous value
366     */
367    public function screamAt(int $levels, bool $replace = false): int
368    {
369        $prev = $this->screamedErrors;
370        $this->screamedErrors = $levels;
371        if (!$replace) {
372            $this->screamedErrors |= $prev;
373        }
374
375        return $prev;
376    }
377
378    /**
379     * Re-registers as a PHP error handler if levels changed.
380     */
381    private function reRegister(int $prev): void
382    {
383        if ($prev !== $this->thrownErrors | $this->loggedErrors) {
384            $handler = set_error_handler('var_dump');
385            $handler = \is_array($handler) ? $handler[0] : null;
386            restore_error_handler();
387            if ($handler === $this) {
388                restore_error_handler();
389                if ($this->isRoot) {
390                    set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors);
391                } else {
392                    set_error_handler([$this, 'handleError']);
393                }
394            }
395        }
396    }
397
398    /**
399     * Handles errors by filtering then logging them according to the configured bit fields.
400     *
401     * @return bool Returns false when no handling happens so that the PHP engine can handle the error itself
402     *
403     * @throws \ErrorException When $this->thrownErrors requests so
404     *
405     * @internal
406     */
407    public function handleError(int $type, string $message, string $file, int $line): bool
408    {
409        if (\PHP_VERSION_ID >= 70300 && \E_WARNING === $type && '"' === $message[0] && false !== strpos($message, '" targeting switch is equivalent to "break')) {
410            $type = \E_DEPRECATED;
411        }
412
413        // Level is the current error reporting level to manage silent error.
414        $level = error_reporting();
415        $silenced = 0 === ($level & $type);
416        // Strong errors are not authorized to be silenced.
417        $level |= \E_RECOVERABLE_ERROR | \E_USER_ERROR | \E_DEPRECATED | \E_USER_DEPRECATED;
418        $log = $this->loggedErrors & $type;
419        $throw = $this->thrownErrors & $type & $level;
420        $type &= $level | $this->screamedErrors;
421
422        // Never throw on warnings triggered by assert()
423        if (\E_WARNING === $type && 'a' === $message[0] && 0 === strncmp($message, 'assert(): ', 10)) {
424            $throw = 0;
425        }
426
427        if (!$type || (!$log && !$throw)) {
428            return false;
429        }
430
431        $logMessage = $this->levels[$type].': '.$message;
432
433        if (null !== self::$toStringException) {
434            $errorAsException = self::$toStringException;
435            self::$toStringException = null;
436        } elseif (!$throw && !($type & $level)) {
437            if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) {
438                $lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 5), $type, $file, $line, false) : [];
439                $errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace);
440            } elseif (isset(self::$silencedErrorCache[$id][$message])) {
441                $lightTrace = null;
442                $errorAsException = self::$silencedErrorCache[$id][$message];
443                ++$errorAsException->count;
444            } else {
445                $lightTrace = [];
446                $errorAsException = null;
447            }
448
449            if (100 < ++self::$silencedErrorCount) {
450                self::$silencedErrorCache = $lightTrace = [];
451                self::$silencedErrorCount = 1;
452            }
453            if ($errorAsException) {
454                self::$silencedErrorCache[$id][$message] = $errorAsException;
455            }
456            if (null === $lightTrace) {
457                return true;
458            }
459        } else {
460            if (false !== strpos($message, '@anonymous')) {
461                $backtrace = debug_backtrace(false, 5);
462
463                for ($i = 1; isset($backtrace[$i]); ++$i) {
464                    if (isset($backtrace[$i]['function'], $backtrace[$i]['args'][0])
465                        && ('trigger_error' === $backtrace[$i]['function'] || 'user_error' === $backtrace[$i]['function'])
466                    ) {
467                        if ($backtrace[$i]['args'][0] !== $message) {
468                            $message = $this->parseAnonymousClass($backtrace[$i]['args'][0]);
469                            $logMessage = $this->levels[$type].': '.$message;
470                        }
471
472                        break;
473                    }
474                }
475            }
476
477            $errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line);
478
479            if ($throw || $this->tracedErrors & $type) {
480                $backtrace = $errorAsException->getTrace();
481                $lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw);
482                ($this->configureException)($errorAsException, $lightTrace, $file, $line);
483            } else {
484                ($this->configureException)($errorAsException, []);
485                $backtrace = [];
486            }
487        }
488
489        if ($throw) {
490            if (\PHP_VERSION_ID < 70400 && \E_USER_ERROR & $type) {
491                for ($i = 1; isset($backtrace[$i]); ++$i) {
492                    if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function'])
493                        && '__toString' === $backtrace[$i]['function']
494                        && '->' === $backtrace[$i]['type']
495                        && !isset($backtrace[$i - 1]['class'])
496                        && ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function'])
497                    ) {
498                        // Here, we know trigger_error() has been called from __toString().
499                        // PHP triggers a fatal error when throwing from __toString().
500                        // A small convention allows working around the limitation:
501                        // given a caught $e exception in __toString(), quitting the method with
502                        // `return trigger_error($e, E_USER_ERROR);` allows this error handler
503                        // to make $e get through the __toString() barrier.
504
505                        $context = 4 < \func_num_args() ? (func_get_arg(4) ?: []) : [];
506
507                        foreach ($context as $e) {
508                            if ($e instanceof \Throwable && $e->__toString() === $message) {
509                                self::$toStringException = $e;
510
511                                return true;
512                            }
513                        }
514
515                        // Display the original error message instead of the default one.
516                        $this->handleException($errorAsException);
517
518                        // Stop the process by giving back the error to the native handler.
519                        return false;
520                    }
521                }
522            }
523
524            throw $errorAsException;
525        }
526
527        if ($this->isRecursive) {
528            $log = 0;
529        } else {
530            if (\PHP_VERSION_ID < (\PHP_VERSION_ID < 70400 ? 70316 : 70404)) {
531                $currentErrorHandler = set_error_handler('var_dump');
532                restore_error_handler();
533            }
534
535            try {
536                $this->isRecursive = true;
537                $level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG;
538                $this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []);
539            } finally {
540                $this->isRecursive = false;
541
542                if (\PHP_VERSION_ID < (\PHP_VERSION_ID < 70400 ? 70316 : 70404)) {
543                    set_error_handler($currentErrorHandler);
544                }
545            }
546        }
547
548        return !$silenced && $type && $log;
549    }
550
551    /**
552     * Handles an exception by logging then forwarding it to another handler.
553     *
554     * @internal
555     */
556    public function handleException(\Throwable $exception)
557    {
558        $handlerException = null;
559
560        if (!$exception instanceof FatalError) {
561            self::$exitCode = 255;
562
563            $type = ThrowableUtils::getSeverity($exception);
564        } else {
565            $type = $exception->getError()['type'];
566        }
567
568        if ($this->loggedErrors & $type) {
569            if (false !== strpos($message = $exception->getMessage(), "@anonymous\0")) {
570                $message = $this->parseAnonymousClass($message);
571            }
572
573            if ($exception instanceof FatalError) {
574                $message = 'Fatal '.$message;
575            } elseif ($exception instanceof \Error) {
576                $message = 'Uncaught Error: '.$message;
577            } elseif ($exception instanceof \ErrorException) {
578                $message = 'Uncaught '.$message;
579            } else {
580                $message = 'Uncaught Exception: '.$message;
581            }
582
583            try {
584                $this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]);
585            } catch (\Throwable $handlerException) {
586            }
587        }
588
589        if (!$exception instanceof OutOfMemoryError) {
590            foreach ($this->getErrorEnhancers() as $errorEnhancer) {
591                if ($e = $errorEnhancer->enhance($exception)) {
592                    $exception = $e;
593                    break;
594                }
595            }
596        }
597
598        $exceptionHandler = $this->exceptionHandler;
599        $this->exceptionHandler = [$this, 'renderException'];
600
601        if (null === $exceptionHandler || $exceptionHandler === $this->exceptionHandler) {
602            $this->exceptionHandler = null;
603        }
604
605        try {
606            if (null !== $exceptionHandler) {
607                return $exceptionHandler($exception);
608            }
609            $handlerException = $handlerException ?: $exception;
610        } catch (\Throwable $handlerException) {
611        }
612        if ($exception === $handlerException && null === $this->exceptionHandler) {
613            self::$reservedMemory = null; // Disable the fatal error handler
614            throw $exception; // Give back $exception to the native handler
615        }
616
617        $loggedErrors = $this->loggedErrors;
618        $this->loggedErrors = $exception === $handlerException ? 0 : $this->loggedErrors;
619
620        try {
621            $this->handleException($handlerException);
622        } finally {
623            $this->loggedErrors = $loggedErrors;
624        }
625    }
626
627    /**
628     * Shutdown registered function for handling PHP fatal errors.
629     *
630     * @param array|null $error An array as returned by error_get_last()
631     *
632     * @internal
633     */
634    public static function handleFatalError(array $error = null): void
635    {
636        if (null === self::$reservedMemory) {
637            return;
638        }
639
640        $handler = self::$reservedMemory = null;
641        $handlers = [];
642        $previousHandler = null;
643        $sameHandlerLimit = 10;
644
645        while (!\is_array($handler) || !$handler[0] instanceof self) {
646            $handler = set_exception_handler('var_dump');
647            restore_exception_handler();
648
649            if (!$handler) {
650                break;
651            }
652            restore_exception_handler();
653
654            if ($handler !== $previousHandler) {
655                array_unshift($handlers, $handler);
656                $previousHandler = $handler;
657            } elseif (0 === --$sameHandlerLimit) {
658                $handler = null;
659                break;
660            }
661        }
662        foreach ($handlers as $h) {
663            set_exception_handler($h);
664        }
665        if (!$handler) {
666            return;
667        }
668        if ($handler !== $h) {
669            $handler[0]->setExceptionHandler($h);
670        }
671        $handler = $handler[0];
672        $handlers = [];
673
674        if ($exit = null === $error) {
675            $error = error_get_last();
676        }
677
678        if ($error && $error['type'] &= \E_PARSE | \E_ERROR | \E_CORE_ERROR | \E_COMPILE_ERROR) {
679            // Let's not throw anymore but keep logging
680            $handler->throwAt(0, true);
681            $trace = $error['backtrace'] ?? null;
682
683            if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) {
684                $fatalError = new OutOfMemoryError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, false, $trace);
685            } else {
686                $fatalError = new FatalError($handler->levels[$error['type']].': '.$error['message'], 0, $error, 2, true, $trace);
687            }
688        } else {
689            $fatalError = null;
690        }
691
692        try {
693            if (null !== $fatalError) {
694                self::$exitCode = 255;
695                $handler->handleException($fatalError);
696            }
697        } catch (FatalError $e) {
698            // Ignore this re-throw
699        }
700
701        if ($exit && self::$exitCode) {
702            $exitCode = self::$exitCode;
703            register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); });
704        }
705    }
706
707    /**
708     * Renders the given exception.
709     *
710     * As this method is mainly called during boot where nothing is yet available,
711     * the output is always either HTML or CLI depending where PHP runs.
712     */
713    private function renderException(\Throwable $exception): void
714    {
715        $renderer = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliErrorRenderer() : new HtmlErrorRenderer($this->debug);
716
717        $exception = $renderer->render($exception);
718
719        if (!headers_sent()) {
720            http_response_code($exception->getStatusCode());
721
722            foreach ($exception->getHeaders() as $name => $value) {
723                header($name.': '.$value, false);
724            }
725        }
726
727        echo $exception->getAsString();
728    }
729
730    /**
731     * Override this method if you want to define more error enhancers.
732     *
733     * @return ErrorEnhancerInterface[]
734     */
735    protected function getErrorEnhancers(): iterable
736    {
737        return [
738            new UndefinedFunctionErrorEnhancer(),
739            new UndefinedMethodErrorEnhancer(),
740            new ClassNotFoundErrorEnhancer(),
741        ];
742    }
743
744    /**
745     * Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader.
746     */
747    private function cleanTrace(array $backtrace, int $type, string &$file, int &$line, bool $throw): array
748    {
749        $lightTrace = $backtrace;
750
751        for ($i = 0; isset($backtrace[$i]); ++$i) {
752            if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) {
753                $lightTrace = \array_slice($lightTrace, 1 + $i);
754                break;
755            }
756        }
757        if (\E_USER_DEPRECATED === $type) {
758            for ($i = 0; isset($lightTrace[$i]); ++$i) {
759                if (!isset($lightTrace[$i]['file'], $lightTrace[$i]['line'], $lightTrace[$i]['function'])) {
760                    continue;
761                }
762                if (!isset($lightTrace[$i]['class']) && 'trigger_deprecation' === $lightTrace[$i]['function']) {
763                    $file = $lightTrace[$i]['file'];
764                    $line = $lightTrace[$i]['line'];
765                    $lightTrace = \array_slice($lightTrace, 1 + $i);
766                    break;
767                }
768            }
769        }
770        if (class_exists(DebugClassLoader::class, false)) {
771            for ($i = \count($lightTrace) - 2; 0 < $i; --$i) {
772                if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) {
773                    array_splice($lightTrace, --$i, 2);
774                }
775            }
776        }
777        if (!($throw || $this->scopedErrors & $type)) {
778            for ($i = 0; isset($lightTrace[$i]); ++$i) {
779                unset($lightTrace[$i]['args'], $lightTrace[$i]['object']);
780            }
781        }
782
783        return $lightTrace;
784    }
785
786    /**
787     * Parse the error message by removing the anonymous class notation
788     * and using the parent class instead if possible.
789     */
790    private function parseAnonymousClass(string $message): string
791    {
792        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) {
793            return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0];
794        }, $message);
795    }
796}
797