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