Lines 92.98% 53 / 57
Methods 80.00% 12 / 15
Classes 0.00% 0 / 1
Name Lines Methods CRAP
 __construct 100.00% 6 / 6 100.00% 1 / 1 2
 getName 0.00% 0 / 1 0.00% 0 / 1 2
 highlightLine 83.33% 5 / 6 0.00% 0 / 1 4.07
 formatMessage 100.00% 20 / 20 100.00% 1 / 1 2
 write 100.00% 1 / 1 100.00% 1 / 1 1
 interpolate 100.00% 9 / 9 100.00% 1 / 1 7
 log 66.66% 4 / 6 0.00% 0 / 1 10.37
 emergency 100.00% 1 / 1 100.00% 1 / 1 1
 alert 100.00% 1 / 1 100.00% 1 / 1 1
 critical 100.00% 1 / 1 100.00% 1 / 1 1
 error 100.00% 1 / 1 100.00% 1 / 1 1
 warning 100.00% 1 / 1 100.00% 1 / 1 1
 notice 100.00% 1 / 1 100.00% 1 / 1 1
 info 100.00% 1 / 1 100.00% 1 / 1 1
 debug 100.00% 1 / 1 100.00% 1 / 1 1
18class Channel implements LoggerInterface
19{
20    private string $channel;
21    private Driver $driver;
22    private bool $useColors;
23
24    private const VALID_LEVELS = [
25        LogLevel::EMERGENCY,
26        LogLevel::ALERT,
27        LogLevel::CRITICAL,
28        LogLevel::ERROR,
29        LogLevel::WARNING,
30        LogLevel::NOTICE,
31        LogLevel::INFO,
32        LogLevel::DEBUG,
33    ];
34
35    // Simplified color scheme to start with
36    private array $levelColors = [
37        'emergency' => "\033[1;37;41m", // Bold white on red
38        'alert' => "\033[1;31m",    // Bold red
39        'critical' => "\033[0;31m",    // Red
40        'error' => "\033[0;31m",    // Red
41        'warning' => "\033[0;33m",    // Yellow
42        'notice' => "\033[0;36m",    // Cyan
43        'info' => "\033[0;32m",    // Green
44        'debug' => "\033[0;37m"     // White
45    ];
46
47    /**
48     * A list of highlighter's.
49     * @var Highlighter[]
50     */
51    private array $highlighters;
52
53    public function __construct(string $channel, Driver $driver, bool $useColors = true)
54    {
55        $this->channel = $channel;
56        $this->driver = $driver;
57        $this->useColors = $useColors && PHP_SAPI === 'cli';
58        $this->highlighters = [
59            new SqlHighlighter(),
60        ];
61    }
62
63    /**
64     * The channel's name, used both in the formatted log output and as the
65     * lookup key when registered via Application::addLoggingChannel().
66     */
67    public function getName(): string
68    {
69        return $this->channel;
70    }
71
72    private function highlightLine(string $line): string
73    {
74        foreach ($this->highlighters as $highlighter) {
75            if (!($highlighter instanceof Highlighter)) {
76                continue;
77            }
78
79            if ($highlighter->shouldHighlight($line, $line)) {
80                $line = $highlighter->highlight($line, $line);
81            }
82        }
83
84        return $line;
85    }
86
87    private function formatMessage(string $level, string $message): string
88    {
89        $timestamp = date('Y-m-d H:i:s');
90        $levelUpper = strtoupper($level);
91
92        if ($this->useColors) {
93            $levelColor = $this->levelColors[$level] ?? "\033[0m";
94            $formattedMessage = $this->highlightLine($message);
95            return sprintf(
96                "[%s] %s%s\033[0m | %s | %s\n",
97                $timestamp,
98                $levelColor,
99                $levelUpper,
100                $this->channel,
101                $formattedMessage
102            );
103        }
104
105        return sprintf(
106            "[%s] %s | %s | %s\n",
107            $timestamp,
108            $levelUpper,
109            $this->channel,
110            $message
111        );
112    }
113
114    private function write(string $level, string $message): void
115    {
116        $this->driver->write($this->formatMessage($level, $message));
117    }
118
119    /**
120     * Interpolate context values into {placeholder} tokens.
121     *
122     * Follows the PSR-3 spec recommendation: a value whose key matches a
123     * placeholder is stringified (if stringable); otherwise the placeholder
124     * is left untouched. The "exception" key is never interpolated, so
125     * exception stack traces are not inlined into the message line.
126     */
127    private function interpolate(string|\Stringable $message, array $context): string
128    {
129        if ($context === []) {
130            return (string) $message;
131        }
132
133        $replace = [];
134        foreach ($context as $key => $value) {
135            if ($key === 'exception') {
136                continue;
137            }
138
139            if (is_string($value) || is_scalar($value) || $value instanceof \Stringable) {
140                $replace['{' . $key . '}'] = (string) $value;
141            }
142        }
143
144        return strtr((string) $message, $replace);
145    }
146
147    /**
148     * Route a log call to the matching level-specific method.
149     *
150     * PSR-3 requires `log()` to behave identically to the level-specific
151     * method for known levels, and to throw an InvalidArgumentException for
152     * unknown levels.
153     */
154    public function log($level, string|\Stringable $message, array $context = []): void
155    {
156        if (!in_array($level, self::VALID_LEVELS, true)) {
157            $levelLabel = match (true) {
158                is_string($level) || is_int($level) || is_float($level) || is_bool($level) => (string) $level,
159                $level instanceof \Stringable => (string) $level,
160                default => gettype($level),
161            };
162            throw new InvalidArgumentException("Unknown log level: {$levelLabel}");
163        }
164
165        $this->write($level, $this->interpolate($message, $context));
166    }
167
168    // PSR-3 log levels
169    public function emergency(string|\Stringable $message, array $context = []): void
170    {
171        $this->log(LogLevel::EMERGENCY, $message, $context);
172    }
173
174    public function alert(string|\Stringable $message, array $context = []): void
175    {
176        $this->log(LogLevel::ALERT, $message, $context);
177    }
178
179    public function critical(string|\Stringable $message, array $context = []): void
180    {
181        $this->log(LogLevel::CRITICAL, $message, $context);
182    }
183
184    public function error(string|\Stringable $message, array $context = []): void
185    {
186        $this->log(LogLevel::ERROR, $message, $context);
187    }
188
189    public function warning(string|\Stringable $message, array $context = []): void
190    {
191        $this->log(LogLevel::WARNING, $message, $context);
192    }
193
194    public function notice(string|\Stringable $message, array $context = []): void
195    {
196        $this->log(LogLevel::NOTICE, $message, $context);
197    }
198
199    public function info(string|\Stringable $message, array $context = []): void
200    {
201        $this->log(LogLevel::INFO, $message, $context);
202    }
203
204    public function debug(string|\Stringable $message, array $context = []): void
205    {
206        $this->log(LogLevel::DEBUG, $message, $context);
207    }
208}