Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.15% covered (success)
96.15%
25 / 26
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileLogger
96.15% covered (success)
96.15%
25 / 26
91.67% covered (success)
91.67%
11 / 12
16
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 emergency
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 critical
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 error
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 warning
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notice
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 info
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 debug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 log
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 shouldLog
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 formatLogEntry
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10namespace Phexium\Plugin\Logger\Adapter;
11
12use Override;
13use Phexium\Plugin\Logger\Port\LoggerInterface;
14use Psr\Log\InvalidArgumentException;
15use Psr\Log\LogLevel;
16use Stringable;
17
18final readonly class FileLogger implements LoggerInterface
19{
20    private const int LOG_DIR_PERMISSIONS = 0o755;
21
22    private const int MAX_LEVEL_PRIORITY = 8; // = max($levelPriority) + 1
23
24    private const array LEVEL_PRIORITY = [
25        LogLevel::EMERGENCY => 0,
26        LogLevel::ALERT => 1,
27        LogLevel::CRITICAL => 2,
28        LogLevel::ERROR => 3,
29        LogLevel::WARNING => 4,
30        LogLevel::NOTICE => 5,
31        LogLevel::INFO => 6,
32        LogLevel::DEBUG => 7,
33    ];
34
35    public function __construct(private string $logFile, private string $minLevel = LogLevel::DEBUG)
36    {
37        $logDir = dirname($this->logFile);
38
39        if (!is_dir($logDir)) {
40            mkdir($logDir, self::LOG_DIR_PERMISSIONS, true);
41        }
42    }
43
44    #[Override]
45    public function emergency(string|Stringable $message, array $context = []): void
46    {
47        $this->log(LogLevel::EMERGENCY, $message, $context);
48    }
49
50    #[Override]
51    public function alert(string|Stringable $message, array $context = []): void
52    {
53        $this->log(LogLevel::ALERT, $message, $context);
54    }
55
56    #[Override]
57    public function critical(string|Stringable $message, array $context = []): void
58    {
59        $this->log(LogLevel::CRITICAL, $message, $context);
60    }
61
62    #[Override]
63    public function error(string|Stringable $message, array $context = []): void
64    {
65        $this->log(LogLevel::ERROR, $message, $context);
66    }
67
68    #[Override]
69    public function warning(string|Stringable $message, array $context = []): void
70    {
71        $this->log(LogLevel::WARNING, $message, $context);
72    }
73
74    #[Override]
75    public function notice(string|Stringable $message, array $context = []): void
76    {
77        $this->log(LogLevel::NOTICE, $message, $context);
78    }
79
80    #[Override]
81    public function info(string|Stringable $message, array $context = []): void
82    {
83        $this->log(LogLevel::INFO, $message, $context);
84    }
85
86    #[Override]
87    public function debug(string|Stringable $message, array $context = []): void
88    {
89        $this->log(LogLevel::DEBUG, $message, $context);
90    }
91
92    #[Override]
93    public function log($level, string|Stringable $message, array $context = []): void
94    {
95        if (!is_string($level)) {
96            throw new InvalidArgumentException('Log level must be a string.');
97        }
98
99        if (!$this->shouldLog($level)) {
100            return;
101        }
102
103        $logEntry = $this->formatLogEntry($level, $message, $context);
104        file_put_contents($this->logFile, $logEntry.PHP_EOL, FILE_APPEND | LOCK_EX);
105    }
106
107    private function shouldLog(string $level): bool
108    {
109        $currentPriority = self::LEVEL_PRIORITY[$level] ?? self::MAX_LEVEL_PRIORITY;
110        $minPriority = self::LEVEL_PRIORITY[$this->minLevel] ?? self::MAX_LEVEL_PRIORITY;
111
112        return $currentPriority <= $minPriority;
113    }
114
115    private function formatLogEntry(string $level, string|Stringable $message, array $context): string
116    {
117        $timestamp = date(DATE_ATOM);
118
119        $levelUpper = strtoupper($level);
120
121        $contextStr = '';
122
123        if ($context !== []) {
124            $contextStr = ' '.json_encode($context);
125        }
126
127        return sprintf('[%s] [%s] %s%s', $timestamp, $levelUpper, $message, $contextStr);
128    }
129}