Passed
Push — main ( bf57ac...d3a7fe )
by Nobufumi
02:11
created

LogPsr3::write()   B

Complexity

Conditions 10
Paths 32

Size

Total Lines 40
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 26
c 1
b 0
f 0
nc 32
nop 2
dl 0
loc 40
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * LogPsr3 class
5
 *
6
 * Provides logging functional.
7
 *
8
 * This class adheres to PSR-1 and PSR-12 coding standards and implements
9
 * PSR-3 (Logger Interface) for compatibility with PSR-3 consumers.
10
 *
11
 * @category Utility
12
 * @package  jidaikobo/log
13
 * @author   jidaikobo-shibata <[email protected]>
14
 * @license  Unlicense <https://unlicense.org/>
15
 * @link     https://github.com/jidaikobo-shibata/log
16
 */
17
18
namespace Jidaikobo;
19
20
use Psr\Log\LoggerInterface;
21
use Psr\Log\LogLevel;
22
use InvalidArgumentException;
23
24
class LogPsr3 implements LoggerInterface
25
{
26
    private string $logFile;
27
    private int $maxFileSize;
28
29
    /**
30
     * Determine the location of the log file and the size at which the log should be rotated.
31
     *
32
     * @param string $logFile     path of log file
33
     * @param int    $maxFileSize log rotation size
34
     *
35
     * @return void
36
     */
37
    public function __construct(string $logFile, int $maxFileSize)
38
    {
39
        $this->logFile = $logFile;
40
        $this->maxFileSize = $maxFileSize;
41
    }
42
43
    /**
44
     * Register error and exception handlers.
45
     *
46
     * @return void
47
     */
48
    public function registerHandlers(): void
49
    {
50
        set_error_handler([$this, 'errorHandler']);
51
        set_exception_handler([$this, 'exceptionHandler']);
52
    }
53
54
    /**
55
     * Handle PHP errors and log them.
56
     *
57
     * @param int    $errno   The level of the error raised.
58
     * @param string $errstr  The error message.
59
     * @param string $errfile The filename that the error was raised in.
60
     * @param int    $errline The line number the error was raised at.
61
     *
62
     * @return bool Always returns false to allow PHP's default error handler.
63
     */
64
    public function errorHandler(int $errno, string $errstr, string $errfile, int $errline): bool
65
    {
66
        $errorMessage = "PHP Error [Level $errno]: $errstr in $errfile on line $errline";
67
        $this->write($errorMessage, 'ERROR');
68
        return false;
69
    }
70
71
    /**
72
     * Handle uncaught exceptions and log them.
73
     *
74
     * @param \Throwable $exception The uncaught exception.
75
     *
76
     * @return void
77
     */
78
    public function exceptionHandler(\Throwable $exception): void
79
    {
80
        $errorMessage = sprintf(
81
            "Uncaught Exception: %s in %s on line %d",
82
            $exception->getMessage(),
83
            $exception->getFile(),
84
            $exception->getLine()
85
        );
86
        $this->write($errorMessage, 'ERROR');
87
    }
88
89
    /**
90
     * Rotate the log file if it exceeds the maximum size.
91
     *
92
     * @return void
93
     */
94
    private function rotateLogFile(): void
95
    {
96
        if (file_exists($this->logFile) && filesize($this->logFile) > $this->maxFileSize) {
97
            rename($this->logFile, $this->logFile . '.' . time());
98
        }
99
    }
100
101
    /**
102
     * Write a message to the log file.
103
     *
104
     * @param string|array<string|int, string>|object|int|bool|null $message The message to log.
105
     * @param string                                $level   The log level.
106
     *
107
     * @return void
108
     */
109
    public function write(string|array|object|int|bool|null $message, string $level = 'INFO'): void
110
    {
111
        $this->rotateLogFile();
112
113
        // Ensure the log directory exists
114
        $logDir = dirname($this->logFile);
115
        if (!is_dir($logDir)) {
116
            mkdir($logDir, 0777, true);
117
        }
118
119
        // Determine the type of $message
120
        $type = gettype($message);
121
122
        // Format the message based on its type
123
        if ($message === null) {
0 ignored issues
show
introduced by
The condition $message === null is always false.
Loading history...
124
            $formattedMessage = 'null'; // Handle null
125
        } elseif (is_bool($message)) {
0 ignored issues
show
introduced by
The condition is_bool($message) is always false.
Loading history...
126
            $formattedMessage = $message ? 'true' : 'false'; // Handle boolean
127
        } elseif (is_int($message)) {
0 ignored issues
show
introduced by
The condition is_int($message) is always false.
Loading history...
128
            $formattedMessage = (string) $message; // Handle integer
129
        } elseif (is_array($message)) {
0 ignored issues
show
introduced by
The condition is_array($message) is always true.
Loading history...
130
            $formattedMessage = var_export($message, true); // Handle array
131
        } elseif (is_object($message)) {
132
            // Handle object
133
            $formattedMessage = method_exists($message, '__toString')
134
                ? (string) $message
135
                : json_encode($message, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
136
            $type = get_class($message); // Use the class name for objects
137
        } else {
138
            $formattedMessage = (string)$message; // Handle string or other types
139
        }
140
141
        $timestamp = date('Y-m-d H:i:s');
142
        if ($level === 'INFO') {
143
            $logEntry = "[$timestamp] [$level] [$type] $formattedMessage" . PHP_EOL;
144
        } else {
145
            $logEntry = "[$timestamp] [$level] $formattedMessage" . PHP_EOL;
146
        }
147
148
        file_put_contents($this->logFile, $logEntry, FILE_APPEND);
149
    }
150
151
    /**
152
     * Logs with an arbitrary level.
153
     *
154
     * @param mixed                $level   The log level.
155
     * @param string|\Stringable   $message The log message.
156
     * @param array<string, mixed> $context Context array.
157
     *
158
     * @return void
159
     */
160
    public function log($level, $message, array $context = []): void
161
    {
162
        if (!in_array($level, $this->getLogLevels(), true)) {
163
            throw new InvalidArgumentException("Invalid log level: $level");
164
        }
165
166
        $message = $this->interpolate($message, $context);
167
        $this->write($message, strtoupper($level));
168
    }
169
170
    /**
171
     * Logs a emergency message.
172
     *
173
     * @param string $message               The message to log.
174
     * @param array<string, mixed> $context Additional context for the log message.
175
     *
176
     * @return void
177
     */
178
    public function emergency($message, array $context = []): void
179
    {
180
        $this->log(LogLevel::EMERGENCY, $message, $context);
181
    }
182
183
    /**
184
     * Logs a alert message.
185
     *
186
     * @param string $message               The message to log.
187
     * @param array<string, mixed> $context Additional context for the log message.
188
     *
189
     * @return void
190
     */
191
    public function alert($message, array $context = []): void
192
    {
193
        $this->log(LogLevel::ALERT, $message, $context);
194
    }
195
196
    /**
197
     * Logs a critical message.
198
     *
199
     * @param string $message               The message to log.
200
     * @param array<string, mixed> $context Additional context for the log message.
201
     *
202
     * @return void
203
     */
204
    public function critical($message, array $context = []): void
205
    {
206
        $this->log(LogLevel::CRITICAL, $message, $context);
207
    }
208
209
    /**
210
     * Logs a error message.
211
     *
212
     * @param string $message               The message to log.
213
     * @param array<string, mixed> $context Additional context for the log message.
214
     *
215
     * @return void
216
     */
217
    public function error($message, array $context = []): void
218
    {
219
        $this->log(LogLevel::ERROR, $message, $context);
220
    }
221
222
    /**
223
     * Logs a warning message.
224
     *
225
     * @param string $message               The message to log.
226
     * @param array<string, mixed> $context Additional context for the log message.
227
     *
228
     * @return void
229
     */
230
    public function warning($message, array $context = []): void
231
    {
232
        $this->log(LogLevel::WARNING, $message, $context);
233
    }
234
235
    /**
236
     * Logs a notice message.
237
     *
238
     * @param string $message               The message to log.
239
     * @param array<string, mixed> $context Additional context for the log message.
240
     *
241
     * @return void
242
     */
243
    public function notice($message, array $context = []): void
244
    {
245
        $this->log(LogLevel::NOTICE, $message, $context);
246
    }
247
248
    /**
249
     * Logs a info message.
250
     *
251
     * @param string $message               The message to log.
252
     * @param array<string, mixed> $context Additional context for the log message.
253
     *
254
     * @return void
255
     */
256
    public function info($message, array $context = []): void
257
    {
258
        $this->log(LogLevel::INFO, $message, $context);
259
    }
260
261
    /**
262
     * Logs a debug message.
263
     *
264
     * @param string $message               The message to log.
265
     * @param array<string, mixed> $context Additional context for the log message.
266
     *
267
     * @return void
268
     */
269
    public function debug($message, array $context = []): void
270
    {
271
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
272
        $traceInfo = array_map(
273
            function ($trace) {
274
                $file = $trace['file'] ?? '[internal function]';
275
                $line = $trace['line'] ?? '?';
276
                $function = $trace['function'];
277
                $class = $trace['class'] ?? '';
278
                return "$file:$line - {$class}{$function}()";
279
            },
280
            $backtrace
281
        );
282
283
        $message .= "\nTrace:\n" . implode("\n", $traceInfo);
284
285
        $this->write($message, 'DEBUG');
286
    }
287
288
    /**
289
     * Interpolates context values into the message placeholders.
290
     *
291
     * @param string $message               The message with placeholders.
292
     * @param array<string, mixed> $context The context array.
293
     *
294
     * @return string The interpolated message.
295
     */
296
    private function interpolate(string $message, array $context): string
297
    {
298
        $replacements = [];
299
        foreach ($context as $key => $value) {
300
            $replacements['{' . $key . '}'] = $value;
301
        }
302
303
        return strtr($message, $replacements);
304
    }
305
306
    /**
307
     * Get supported log levels.
308
     *
309
     * @return string[]
310
     */
311
    private function getLogLevels(): array
312
    {
313
        return [
314
            LogLevel::EMERGENCY,
315
            LogLevel::ALERT,
316
            LogLevel::CRITICAL,
317
            LogLevel::ERROR,
318
            LogLevel::WARNING,
319
            LogLevel::NOTICE,
320
            LogLevel::INFO,
321
            LogLevel::DEBUG,
322
        ];
323
    }
324
}
325