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