Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.37% covered (warning)
79.37%
50 / 63
61.54% covered (warning)
61.54%
8 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
SimpleLogger
79.37% covered (warning)
79.37%
50 / 63
61.54% covered (warning)
61.54%
8 / 13
42.57
0.00% covered (danger)
0.00%
0 / 1
 __construct
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 log
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 validateLevel
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 interpolate
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
9
 isException
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
4.94
 formatObject
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 formatException
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 createLine
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 writeLine
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 writeToErrorLog
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 writeLineToFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 logStore
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setLogFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Projom\Storage\Logger;
6
7use Stringable;
8
9use Psr\Log\AbstractLogger;
10use Psr\Log\InvalidArgumentException;
11use Psr\Log\LogLevel;
12
13class SimpleLogger extends AbstractLogger
14{
15    const RFC5424_LEVELS = [
16        LogLevel::EMERGENCY => 0,
17        LogLevel::ALERT => 1,
18        LogLevel::CRITICAL => 2,
19        LogLevel::ERROR => 3,
20        LogLevel::WARNING => 4,
21        LogLevel::NOTICE => 5,
22        LogLevel::INFO => 6,
23        LogLevel::DEBUG => 7,
24    ];
25
26    private readonly string $absoluteFilePath;
27    private readonly string $logLevel;
28    private readonly LoggerType $type;
29    private array $logStore = [];
30    private string $logFormat = '[{datetime}] [{level}] {message}';
31
32    public function __construct(
33        LoggerType $type = LoggerType::ERROR_LOG,
34        string $logLevel = LogLevel::DEBUG,
35        string $absoluteFilePath = ''
36    ) {
37
38        if ($type === LoggerType::FILE)
39            if (!file_exists($absoluteFilePath))
40                throw new InvalidArgumentException("File: $absoluteFilePath does not exist.");
41
42        $this->validateLevel($logLevel);
43
44        $this->absoluteFilePath = $absoluteFilePath;
45        $this->logLevel = $logLevel;
46        $this->type = $type;
47    }
48
49    public function log($level, string|Stringable $message, array $context = []): void
50    {
51        $this->validateLevel($level);
52
53        $level = strtolower($level);
54        if (static::RFC5424_LEVELS[$level] > static::RFC5424_LEVELS[$this->logLevel])
55            return;
56
57        $message = $this->interpolate($message, $context);
58        $line = $this->createLine($level, $message);
59        $this->writeLine($line);
60    }
61
62    private function validateLevel(string $level): void
63    {
64        $level = strtolower($level);
65        if (!array_key_exists($level, static::RFC5424_LEVELS))
66            throw new InvalidArgumentException("Invalid log level: $level");
67    }
68
69    private function interpolate(string $message, array $context): string
70    {
71        $replace = [];
72        foreach ($context as $key => $val) {
73
74            $val = match (true) {
75                is_null($val) => 'null',
76                is_bool($val) => $val ? 'true' : 'false',
77                is_array($val) => json_encode($val),
78                $this->isException($key, $val) => $this->formatException($val),
79                is_object($val) => $this->formatObject($val),
80                default => (string) $val,
81            };
82
83            $key = '{' . $key . '}';
84            $replace[$key] = $val;
85        }
86
87        return strtr($message, $replace);
88    }
89
90    private function isException(string $key, mixed $exception): bool
91    {
92        if ($key !== 'exception')
93            return false;
94        if (!is_subclass_of($exception, \Throwable::class))
95            return false;
96        return true;
97    }
98
99    private function formatObject(object $object): string
100    {
101        if ($object instanceof Stringable || method_exists($object, '__toString'))
102            return (string) $object;
103
104        $class = get_class($object);
105        return "Class: $class.";
106    }
107
108    private function formatException(\Throwable $exception): string
109    {
110        $trace = $exception->getTraceAsString();
111        $code = $exception->getCode();
112        $message = $exception->getMessage();
113        $file = $exception->getFile();
114        $line = $exception->getLine();
115        return "Exception \"$message\" with code $code in \"$file\" on line $line.\nStack trace:\n$trace";
116    }
117
118    private function createLine(string $level, string $message): string
119    {
120        $vars = [
121            '{datetime}' => date('Y-m-d H:i:s'),
122            '{level}' => strtoupper($level),
123            '{message}' => $message
124        ];
125
126        $line = strtr($this->logFormat, $vars);
127
128        return $line . PHP_EOL;
129    }
130
131    private function writeLine(string $line): void
132    {
133        match ($this->type) {
134            LoggerType::ERROR_LOG => $this->writeToErrorLog($line),
135            LoggerType::FILE => $this->writeLineToFile($line),
136            LoggerType::LOG_STORE => $this->logStore[] = $line
137        };
138    }
139
140    private function writeToErrorLog(string $line): void
141    {
142        error_log($line, message_type: 0);
143    }
144
145    private function writeLineToFile(string $line): void
146    {
147        file_put_contents($this->absoluteFilePath, $line, FILE_APPEND | LOCK_EX);
148    }
149
150    public function logStore(bool $asString = true): string|array
151    {
152        if ($asString)
153            return implode(', ', $this->logStore);
154        return $this->logStore;
155    }
156
157    public function setLogFormat(string $logFormat): void
158    {
159        $this->logFormat = $logFormat;
160    }
161}