1<?php declare(strict_types=1); 2 3/* 4 * This file is part of the Monolog package. 5 * 6 * (c) Jordi Boggiano <j.boggiano@seld.be> 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 Monolog; 13 14use Psr\Log\LoggerInterface; 15use Psr\Log\LogLevel; 16 17/** 18 * Monolog error handler 19 * 20 * A facility to enable logging of runtime errors, exceptions and fatal errors. 21 * 22 * Quick setup: <code>ErrorHandler::register($logger);</code> 23 * 24 * @author Jordi Boggiano <j.boggiano@seld.be> 25 */ 26class ErrorHandler 27{ 28 /** @var LoggerInterface */ 29 private $logger; 30 31 /** @var ?callable */ 32 private $previousExceptionHandler = null; 33 /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */ 34 private $uncaughtExceptionLevelMap = []; 35 36 /** @var callable|true|null */ 37 private $previousErrorHandler = null; 38 /** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */ 39 private $errorLevelMap = []; 40 /** @var bool */ 41 private $handleOnlyReportedErrors = true; 42 43 /** @var bool */ 44 private $hasFatalErrorHandler = false; 45 /** @var LogLevel::* */ 46 private $fatalLevel = LogLevel::ALERT; 47 /** @var ?string */ 48 private $reservedMemory = null; 49 /** @var ?mixed */ 50 private $lastFatalTrace; 51 /** @var int[] */ 52 private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; 53 54 public function __construct(LoggerInterface $logger) 55 { 56 $this->logger = $logger; 57 } 58 59 /** 60 * Registers a new ErrorHandler for a given Logger 61 * 62 * By default it will handle errors, exceptions and fatal errors 63 * 64 * @param LoggerInterface $logger 65 * @param array<int, LogLevel::*>|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling 66 * @param array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling 67 * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling 68 * @return ErrorHandler 69 */ 70 public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self 71 { 72 /** @phpstan-ignore-next-line */ 73 $handler = new static($logger); 74 if ($errorLevelMap !== false) { 75 $handler->registerErrorHandler($errorLevelMap); 76 } 77 if ($exceptionLevelMap !== false) { 78 $handler->registerExceptionHandler($exceptionLevelMap); 79 } 80 if ($fatalLevel !== false) { 81 $handler->registerFatalHandler($fatalLevel); 82 } 83 84 return $handler; 85 } 86 87 /** 88 * @param array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping 89 * @return $this 90 */ 91 public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self 92 { 93 $prev = set_exception_handler(function (\Throwable $e): void { 94 $this->handleException($e); 95 }); 96 $this->uncaughtExceptionLevelMap = $levelMap; 97 foreach ($this->defaultExceptionLevelMap() as $class => $level) { 98 if (!isset($this->uncaughtExceptionLevelMap[$class])) { 99 $this->uncaughtExceptionLevelMap[$class] = $level; 100 } 101 } 102 if ($callPrevious && $prev) { 103 $this->previousExceptionHandler = $prev; 104 } 105 106 return $this; 107 } 108 109 /** 110 * @param array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping 111 * @return $this 112 */ 113 public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self 114 { 115 $prev = set_error_handler([$this, 'handleError'], $errorTypes); 116 $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap); 117 if ($callPrevious) { 118 $this->previousErrorHandler = $prev ?: true; 119 } else { 120 $this->previousErrorHandler = null; 121 } 122 123 $this->handleOnlyReportedErrors = $handleOnlyReportedErrors; 124 125 return $this; 126 } 127 128 /** 129 * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT 130 * @param int $reservedMemorySize Amount of KBs to reserve in memory so that it can be freed when handling fatal errors giving Monolog some room in memory to get its job done 131 */ 132 public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self 133 { 134 register_shutdown_function([$this, 'handleFatalError']); 135 136 $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize); 137 $this->fatalLevel = null === $level ? LogLevel::ALERT : $level; 138 $this->hasFatalErrorHandler = true; 139 140 return $this; 141 } 142 143 /** 144 * @return array<class-string, LogLevel::*> 145 */ 146 protected function defaultExceptionLevelMap(): array 147 { 148 return [ 149 'ParseError' => LogLevel::CRITICAL, 150 'Throwable' => LogLevel::ERROR, 151 ]; 152 } 153 154 /** 155 * @return array<int, LogLevel::*> 156 */ 157 protected function defaultErrorLevelMap(): array 158 { 159 return [ 160 E_ERROR => LogLevel::CRITICAL, 161 E_WARNING => LogLevel::WARNING, 162 E_PARSE => LogLevel::ALERT, 163 E_NOTICE => LogLevel::NOTICE, 164 E_CORE_ERROR => LogLevel::CRITICAL, 165 E_CORE_WARNING => LogLevel::WARNING, 166 E_COMPILE_ERROR => LogLevel::ALERT, 167 E_COMPILE_WARNING => LogLevel::WARNING, 168 E_USER_ERROR => LogLevel::ERROR, 169 E_USER_WARNING => LogLevel::WARNING, 170 E_USER_NOTICE => LogLevel::NOTICE, 171 E_STRICT => LogLevel::NOTICE, 172 E_RECOVERABLE_ERROR => LogLevel::ERROR, 173 E_DEPRECATED => LogLevel::NOTICE, 174 E_USER_DEPRECATED => LogLevel::NOTICE, 175 ]; 176 } 177 178 /** 179 * @phpstan-return never 180 */ 181 private function handleException(\Throwable $e): void 182 { 183 $level = LogLevel::ERROR; 184 foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) { 185 if ($e instanceof $class) { 186 $level = $candidate; 187 break; 188 } 189 } 190 191 $this->logger->log( 192 $level, 193 sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), 194 ['exception' => $e] 195 ); 196 197 if ($this->previousExceptionHandler) { 198 ($this->previousExceptionHandler)($e); 199 } 200 201 if (!headers_sent() && !ini_get('display_errors')) { 202 http_response_code(500); 203 } 204 205 exit(255); 206 } 207 208 /** 209 * @private 210 * 211 * @param mixed[] $context 212 */ 213 public function handleError(int $code, string $message, string $file = '', int $line = 0, ?array $context = []): bool 214 { 215 if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) { 216 return false; 217 } 218 219 // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries 220 if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) { 221 $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL; 222 $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]); 223 } else { 224 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 225 array_shift($trace); // Exclude handleError from trace 226 $this->lastFatalTrace = $trace; 227 } 228 229 if ($this->previousErrorHandler === true) { 230 return false; 231 } elseif ($this->previousErrorHandler) { 232 return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context); 233 } 234 235 return true; 236 } 237 238 /** 239 * @private 240 */ 241 public function handleFatalError(): void 242 { 243 $this->reservedMemory = ''; 244 245 $lastError = error_get_last(); 246 if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) { 247 $this->logger->log( 248 $this->fatalLevel, 249 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], 250 ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace] 251 ); 252 253 if ($this->logger instanceof Logger) { 254 foreach ($this->logger->getHandlers() as $handler) { 255 $handler->close(); 256 } 257 } 258 } 259 } 260 261 /** 262 * @param int $code 263 */ 264 private static function codeToString($code): string 265 { 266 switch ($code) { 267 case E_ERROR: 268 return 'E_ERROR'; 269 case E_WARNING: 270 return 'E_WARNING'; 271 case E_PARSE: 272 return 'E_PARSE'; 273 case E_NOTICE: 274 return 'E_NOTICE'; 275 case E_CORE_ERROR: 276 return 'E_CORE_ERROR'; 277 case E_CORE_WARNING: 278 return 'E_CORE_WARNING'; 279 case E_COMPILE_ERROR: 280 return 'E_COMPILE_ERROR'; 281 case E_COMPILE_WARNING: 282 return 'E_COMPILE_WARNING'; 283 case E_USER_ERROR: 284 return 'E_USER_ERROR'; 285 case E_USER_WARNING: 286 return 'E_USER_WARNING'; 287 case E_USER_NOTICE: 288 return 'E_USER_NOTICE'; 289 case E_STRICT: 290 return 'E_STRICT'; 291 case E_RECOVERABLE_ERROR: 292 return 'E_RECOVERABLE_ERROR'; 293 case E_DEPRECATED: 294 return 'E_DEPRECATED'; 295 case E_USER_DEPRECATED: 296 return 'E_USER_DEPRECATED'; 297 } 298 299 return 'Unknown PHP error'; 300 } 301} 302