Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
22 / 22
CRAP
100.00% covered (success)
100.00%
1 / 1
Logger
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
22 / 22
38
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addWriters
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addWriter
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getWriters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addProcessors
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addProcessor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getProcessors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setLogLimit
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setTimestampFormat
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTimestampFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLogLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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%
17 / 17
100.00% covered (success)
100.00%
1 / 1
7
 interpolate
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <dev@noladev.com>
8 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Log;
16
17use Pop\Log\Writer\WriterInterface;
18use Psr\Log\LoggerInterface;
19use Psr\Log\LogLevel;
20use Stringable;
21
22/**
23 * Logger class
24 *
25 * @category   Pop
26 * @package    Pop\Log
27 * @author     Nick Sagona, III <dev@noladev.com>
28 * @copyright  Copyright (c) 2009-2027 NOLA Interactive, LLC.
29 * @license    https://www.popphp.org/license     New BSD License
30 * @version    5.0.0
31 */
32class Logger implements LoggerInterface
33{
34
35    /**
36     * Constants for log levels (PSR-3 level strings)
37     * @var string
38     */
39    const EMERGENCY = LogLevel::EMERGENCY;
40    const ALERT     = LogLevel::ALERT;
41    const CRITICAL  = LogLevel::CRITICAL;
42    const ERROR     = LogLevel::ERROR;
43    const WARNING   = LogLevel::WARNING;
44    const NOTICE    = LogLevel::NOTICE;
45    const INFO      = LogLevel::INFO;
46    const DEBUG     = LogLevel::DEBUG;
47
48    /**
49     * Reserved context keys never treated as interpolation placeholders
50     * @var array
51     */
52    protected const RESERVED_CONTEXT_KEYS = ['timestamp', 'name', 'format'];
53
54    /**
55     * Log writers
56     * @var array
57     */
58    protected array $writers = [];
59
60    /**
61     * Context-enrichment processors
62     * @var callable[]
63     */
64    protected array $processors = [];
65
66    /**
67     * Log timestamp format
68     * @var string
69     */
70    protected string $timestampFormat = 'Y-m-d H:i:s';
71
72    /**
73     * Constructor
74     *
75     * Instantiate the logger object
76     *
77     * @param  WriterInterface|array|null $writer
78     * @param  string                     $timestampFormat
79     */
80    public function __construct(WriterInterface|array|null $writer = [], string $timestampFormat = 'Y-m-d H:i:s')
81    {
82        $this->setTimestampFormat($timestampFormat);
83
84        if ($writer !== null) {
85            if (is_array($writer)) {
86                $this->addWriters($writer);
87            } else {
88                $this->addWriter($writer);
89            }
90        }
91    }
92
93    /**
94     * Add log writers
95     *
96     * @param  array $writers
97     * @return Logger
98     */
99    public function addWriters(array $writers): Logger
100    {
101        foreach ($writers as $writer) {
102            $this->addWriter($writer);
103        }
104        return $this;
105    }
106
107    /**
108     * Add a log writer
109     *
110     * @param  Writer\WriterInterface $writer
111     * @return Logger
112     */
113    public function addWriter(Writer\WriterInterface $writer): Logger
114    {
115        $this->writers[] = $writer;
116        return $this;
117    }
118
119    /**
120     * Get all log writers
121     *
122     * @return array
123     */
124    public function getWriters(): array
125    {
126        return $this->writers;
127    }
128
129    /**
130     * Add context-enrichment processors
131     *
132     * @param  array $processors
133     * @return Logger
134     */
135    public function addProcessors(array $processors): Logger
136    {
137        foreach ($processors as $processor) {
138            $this->addProcessor($processor);
139        }
140        return $this;
141    }
142
143    /**
144     * Add a context-enrichment processor
145     *
146     * @param  callable $processor
147     * @return Logger
148     */
149    public function addProcessor(callable $processor): Logger
150    {
151        $this->processors[] = $processor;
152        return $this;
153    }
154
155    /**
156     * Get all context-enrichment processors
157     *
158     * @return callable[]
159     */
160    public function getProcessors(): array
161    {
162        return $this->processors;
163    }
164
165    /**
166     * Set log level limit for all log writers
167     *
168     * @param  string|int $level
169     * @return Logger
170     */
171    public function setLogLimit(string|int $level): Logger
172    {
173        foreach ($this->writers as $writer) {
174            $writer->setLogLimit($level);
175        }
176        return $this;
177    }
178
179    /**
180     * Set timestamp format
181     *
182     * @param  string $format
183     * @return Logger
184     */
185    public function setTimestampFormat(string $format = 'Y-m-d H:i:s'): Logger
186    {
187        $this->timestampFormat = $format;
188        return $this;
189    }
190
191    /**
192     * Get timestamp format
193     *
194     * @return string
195     */
196    public function getTimestampFormat(): string
197    {
198        return $this->timestampFormat;
199    }
200
201    /**
202     * Get level display name
203     *
204     * Accepts either a PSR-3 level string or a legacy severity int (0-7).
205     *
206     * @param  string|int $level
207     * @return string
208     */
209    public function getLevel(string|int $level): string
210    {
211        return Level::toName(Level::fromSeverity(Level::toSeverity($level)));
212    }
213
214    /**
215     * Static method to get log level display name
216     *
217     * @param  string|int $level
218     * @return string
219     */
220    public static function getLogLevel(string|int $level): string
221    {
222        return Level::toName(Level::fromSeverity(Level::toSeverity($level)));
223    }
224
225    /**
226     * Add an EMERGENCY log entry
227     *
228     * @param  string|Stringable $message
229     * @param  array             $context
230     * @return void
231     */
232    public function emergency(string|Stringable $message, array $context = []): void
233    {
234        $this->log(self::EMERGENCY, $message, $context);
235    }
236
237    /**
238     * Add an ALERT log entry
239     *
240     * @param  string|Stringable $message
241     * @param  array             $context
242     * @return void
243     */
244    public function alert(string|Stringable $message, array $context = []): void
245    {
246        $this->log(self::ALERT, $message, $context);
247    }
248
249    /**
250     * Add a CRITICAL log entry
251     *
252     * @param  string|Stringable $message
253     * @param  array             $context
254     * @return void
255     */
256    public function critical(string|Stringable $message, array $context = []): void
257    {
258        $this->log(self::CRITICAL, $message, $context);
259    }
260
261    /**
262     * Add an ERROR log entry
263     *
264     * @param  string|Stringable $message
265     * @param  array             $context
266     * @return void
267     */
268    public function error(string|Stringable $message, array $context = []): void
269    {
270        $this->log(self::ERROR, $message, $context);
271    }
272
273    /**
274     * Add a WARNING log entry
275     *
276     * @param  string|Stringable $message
277     * @param  array             $context
278     * @return void
279     */
280    public function warning(string|Stringable $message, array $context = []): void
281    {
282        $this->log(self::WARNING, $message, $context);
283    }
284
285    /**
286     * Add a NOTICE log entry
287     *
288     * @param  string|Stringable $message
289     * @param  array             $context
290     * @return void
291     */
292    public function notice(string|Stringable $message, array $context = []): void
293    {
294        $this->log(self::NOTICE, $message, $context);
295    }
296
297    /**
298     * Add an INFO log entry
299     *
300     * @param  string|Stringable $message
301     * @param  array             $context
302     * @return void
303     */
304    public function info(string|Stringable $message, array $context = []): void
305    {
306        $this->log(self::INFO, $message, $context);
307    }
308
309    /**
310     * Add a DEBUG log entry
311     *
312     * @param  string|Stringable $message
313     * @param  array             $context
314     * @return void
315     */
316    public function debug(string|Stringable $message, array $context = []): void
317    {
318        $this->log(self::DEBUG, $message, $context);
319    }
320
321    /**
322     * Add a log entry
323     *
324     * $level stays untyped/mixed to match Psr\Log\LoggerInterface::log() exactly (PHP forbids narrowing
325     * an interface parameter's type). Accepts a PSR-3 level string or a legacy severity int (0-7).
326     * Registered processors enrich $context before interpolation runs.
327     *
328     * @param  mixed             $level
329     * @param  string|Stringable $message
330     * @param  array             $context
331     * @return void
332     */
333    public function log(mixed $level, string|Stringable $message, array $context = []): void
334    {
335        $level = Level::fromSeverity(Level::toSeverity($level));
336
337        if (!isset($context['timestamp'])) {
338            $context['timestamp'] = date($this->timestampFormat);
339        }
340        if (!isset($context['name'])) {
341            $context['name'] = Level::toName($level);
342        }
343
344        foreach ($this->processors as $processor) {
345            $context = $processor($context);
346        }
347
348        $context['timestamp'] ??= date($this->timestampFormat);
349        $context['name']      ??= Level::toName($level);
350
351        [$message, $context] = $this->interpolate((string)$message, $context);
352
353        $failure = null;
354
355        foreach ($this->writers as $writer) {
356            try {
357                $writer->writeLog($level, $message, $context);
358            } catch (\Exception $exception) {
359                $failure ??= $exception;
360            }
361        }
362
363        if ($failure !== null) {
364            throw $failure;
365        }
366    }
367
368    /**
369     * Substitute {key} placeholders in the message from scalar/Stringable context values.
370     *
371     * Reserved keys (timestamp, name, format) are never treated as placeholders, since they're
372     * Logger-managed metadata rather than user-supplied context. Consumed keys are removed from the
373     * returned context so writers don't also serialize a value that's already inline in the message.
374     *
375     * @param  string $message
376     * @param  array  $context
377     * @return array
378     */
379    protected function interpolate(string $message, array $context): array
380    {
381        $replace = [];
382
383        foreach ($context as $key => $value) {
384            if (in_array($key, self::RESERVED_CONTEXT_KEYS, true)) {
385                continue;
386            }
387
388            if ((is_scalar($value) || $value instanceof Stringable) && str_contains($message, '{' . $key . '}')) {
389                $replace['{' . $key . '}'] = (string)$value;
390                unset($context[$key]);
391            }
392        }
393
394        return [strtr($message, $replace), $context];
395    }
396
397}