Passed
Push — main ( 50df58...63f4e5 )
by Nobufumi
02:13
created

LogPsr3   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 62
dl 0
loc 277
rs 10
c 0
b 0
f 0
wmc 23

17 Methods

Rating   Name   Duplication   Size   Complexity  
A write() 0 19 3
A debug() 0 17 1
A notice() 0 3 1
A errorHandler() 0 5 1
A alert() 0 3 1
A emergency() 0 3 1
A critical() 0 3 1
A info() 0 3 1
A interpolate() 0 8 2
A registerHandlers() 0 4 1
A exceptionHandler() 0 9 1
A getLogLevels() 0 11 1
A __construct() 0 4 1
A warning() 0 3 1
A error() 0 3 1
A log() 0 8 2
A rotateLogFile() 0 4 3
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> $message The message to log.
105
     * @param string                           $level   The log level.
106
     *
107
     * @return void
108
     */
109
    public function write(string|array $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
        // Format the message
120
        if (is_array($message)) {
0 ignored issues
show
introduced by
The condition is_array($message) is always true.
Loading history...
121
            $message = var_export($message, true);
122
        }
123
124
        $timestamp = date('Y-m-d H:i:s');
125
        $logEntry = "[$timestamp] [$level] $message" . PHP_EOL;
126
127
        file_put_contents($this->logFile, $logEntry, FILE_APPEND);
128
    }
129
130
    /**
131
     * Logs with an arbitrary level.
132
     *
133
     * @param mixed                $level   The log level.
134
     * @param string|\Stringable   $message The log message.
135
     * @param array<string, mixed> $context Context array.
136
     *
137
     * @return void
138
     */
139
    public function log($level, $message, array $context = []): void
140
    {
141
        if (!in_array($level, $this->getLogLevels(), true)) {
142
            throw new InvalidArgumentException("Invalid log level: $level");
143
        }
144
145
        $message = $this->interpolate($message, $context);
146
        $this->write($message, strtoupper($level));
147
    }
148
149
    /**
150
     * Logs a emergency message.
151
     *
152
     * @param string $message               The message to log.
153
     * @param array<string, mixed> $context Additional context for the log message.
154
     *
155
     * @return void
156
     */
157
    public function emergency($message, array $context = []): void
158
    {
159
        $this->log(LogLevel::EMERGENCY, $message, $context);
160
    }
161
162
    /**
163
     * Logs a alert message.
164
     *
165
     * @param string $message               The message to log.
166
     * @param array<string, mixed> $context Additional context for the log message.
167
     *
168
     * @return void
169
     */
170
    public function alert($message, array $context = []): void
171
    {
172
        $this->log(LogLevel::ALERT, $message, $context);
173
    }
174
175
    /**
176
     * Logs a critical message.
177
     *
178
     * @param string $message               The message to log.
179
     * @param array<string, mixed> $context Additional context for the log message.
180
     *
181
     * @return void
182
     */
183
    public function critical($message, array $context = []): void
184
    {
185
        $this->log(LogLevel::CRITICAL, $message, $context);
186
    }
187
188
    /**
189
     * Logs a error message.
190
     *
191
     * @param string $message               The message to log.
192
     * @param array<string, mixed> $context Additional context for the log message.
193
     *
194
     * @return void
195
     */
196
    public function error($message, array $context = []): void
197
    {
198
        $this->log(LogLevel::ERROR, $message, $context);
199
    }
200
201
    /**
202
     * Logs a warning message.
203
     *
204
     * @param string $message               The message to log.
205
     * @param array<string, mixed> $context Additional context for the log message.
206
     *
207
     * @return void
208
     */
209
    public function warning($message, array $context = []): void
210
    {
211
        $this->log(LogLevel::WARNING, $message, $context);
212
    }
213
214
    /**
215
     * Logs a notice message.
216
     *
217
     * @param string $message               The message to log.
218
     * @param array<string, mixed> $context Additional context for the log message.
219
     *
220
     * @return void
221
     */
222
    public function notice($message, array $context = []): void
223
    {
224
        $this->log(LogLevel::NOTICE, $message, $context);
225
    }
226
227
    /**
228
     * Logs a info message.
229
     *
230
     * @param string $message               The message to log.
231
     * @param array<string, mixed> $context Additional context for the log message.
232
     *
233
     * @return void
234
     */
235
    public function info($message, array $context = []): void
236
    {
237
        $this->log(LogLevel::INFO, $message, $context);
238
    }
239
240
    /**
241
     * Logs a debug message.
242
     *
243
     * @param string $message               The message to log.
244
     * @param array<string, mixed> $context Additional context for the log message.
245
     *
246
     * @return void
247
     */
248
    public function debug($message, array $context = []): void
249
    {
250
        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
251
        $traceInfo = array_map(
252
            function ($trace) {
253
                $file = $trace['file'] ?? '[internal function]';
254
                $line = $trace['line'] ?? '?';
255
                $function = $trace['function'];
256
                $class = $trace['class'] ?? '';
257
                return "$file:$line - {$class}{$function}()";
258
            },
259
            $backtrace
260
        );
261
262
        $message .= "\nTrace:\n" . implode("\n", $traceInfo);
263
264
        $this->write($message, 'DEBUG');
265
    }
266
267
    /**
268
     * Interpolates context values into the message placeholders.
269
     *
270
     * @param string $message               The message with placeholders.
271
     * @param array<string, mixed> $context The context array.
272
     *
273
     * @return string The interpolated message.
274
     */
275
    private function interpolate(string $message, array $context): string
276
    {
277
        $replacements = [];
278
        foreach ($context as $key => $value) {
279
            $replacements['{' . $key . '}'] = $value;
280
        }
281
282
        return strtr($message, $replacements);
283
    }
284
285
    /**
286
     * Get supported log levels.
287
     *
288
     * @return string[]
289
     */
290
    private function getLogLevels(): array
291
    {
292
        return [
293
            LogLevel::EMERGENCY,
294
            LogLevel::ALERT,
295
            LogLevel::CRITICAL,
296
            LogLevel::ERROR,
297
            LogLevel::WARNING,
298
            LogLevel::NOTICE,
299
            LogLevel::INFO,
300
            LogLevel::DEBUG,
301
        ];
302
    }
303
}
304