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