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