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