Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 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 Psr\Log\LoggerInterface;
15
use 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
 */
26
class 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 ?array{type: int, message: string, file: string, line: int, trace: mixed} */
50
    private $lastFatalData = null;
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->lastFatalData = ['type' => $code, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => $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
        if (is_array($this->lastFatalData)) {
246
            $lastError = $this->lastFatalData;
247
        } else {
248
            $lastError = error_get_last();
249
        }
250
 
251
        if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
252
            $trace = $lastError['trace'] ?? null;
253
            $this->logger->log(
254
                $this->fatalLevel,
255
                'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
256
                ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $trace]
257
            );
258
 
259
            if ($this->logger instanceof Logger) {
260
                foreach ($this->logger->getHandlers() as $handler) {
261
                    $handler->close();
262
                }
263
            }
264
        }
265
    }
266
 
267
    /**
268
     * @param int $code
269
     */
270
    private static function codeToString($code): string
271
    {
272
        switch ($code) {
273
            case E_ERROR:
274
                return 'E_ERROR';
275
            case E_WARNING:
276
                return 'E_WARNING';
277
            case E_PARSE:
278
                return 'E_PARSE';
279
            case E_NOTICE:
280
                return 'E_NOTICE';
281
            case E_CORE_ERROR:
282
                return 'E_CORE_ERROR';
283
            case E_CORE_WARNING:
284
                return 'E_CORE_WARNING';
285
            case E_COMPILE_ERROR:
286
                return 'E_COMPILE_ERROR';
287
            case E_COMPILE_WARNING:
288
                return 'E_COMPILE_WARNING';
289
            case E_USER_ERROR:
290
                return 'E_USER_ERROR';
291
            case E_USER_WARNING:
292
                return 'E_USER_WARNING';
293
            case E_USER_NOTICE:
294
                return 'E_USER_NOTICE';
295
            case E_STRICT:
296
                return 'E_STRICT';
297
            case E_RECOVERABLE_ERROR:
298
                return 'E_RECOVERABLE_ERROR';
299
            case E_DEPRECATED:
300
                return 'E_DEPRECATED';
301
            case E_USER_DEPRECATED:
302
                return 'E_USER_DEPRECATED';
303
        }
304
 
305
        return 'Unknown PHP error';
306
    }
307
}