Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
23 / 23
91.67% covered (success)
91.67%
22 / 24
61.11% covered (warning)
61.11%
11 / 18
83.33% covered (warning)
83.33%
5 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Logger
100.00% covered (success)
100.00%
23 / 23
91.67% covered (success)
91.67%
22 / 24
61.11% covered (warning)
61.11%
11 / 18
100.00% covered (success)
100.00%
6 / 6
20.47
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 formatter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 withFormatter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 log
100.00% covered (success)
100.00%
11 / 11
81.82% covered (warning)
81.82%
9 / 11
42.86% covered (danger)
42.86%
3 / 7
100.00% covered (success)
100.00%
1 / 1
4.68
 validateLevel
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
3.14
 printLevel
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
5 / 5
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Log;
6
7use Celemas\Log\Formatter\TextFormatter;
8use Override;
9use Psr\Log\InvalidArgumentException;
10use Psr\Log\LoggerInterface as PsrLogger;
11use Psr\Log\LoggerTrait;
12use Psr\Log\LogLevel;
13use Stringable;
14
15/** @api */
16final class Logger implements PsrLogger
17{
18    use LoggerTrait;
19
20    public const string DEBUG = LogLevel::DEBUG;
21    public const string INFO = LogLevel::INFO;
22    public const string NOTICE = LogLevel::NOTICE;
23    public const string WARNING = LogLevel::WARNING;
24    public const string ERROR = LogLevel::ERROR;
25    public const string CRITICAL = LogLevel::CRITICAL;
26    public const string ALERT = LogLevel::ALERT;
27    public const string EMERGENCY = LogLevel::EMERGENCY;
28
29    private const int ERROR_LOG_APPEND_TO_FILE = 3;
30
31    /** @var array<string, positive-int> */
32    private const array LEVEL_SEVERITY = [
33        self::DEBUG => 100,
34        self::INFO => 200,
35        self::NOTICE => 300,
36        self::WARNING => 400,
37        self::ERROR => 500,
38        self::CRITICAL => 600,
39        self::ALERT => 700,
40        self::EMERGENCY => 800,
41    ];
42
43    /** @var array<string, non-empty-string> */
44    private const array LEVEL_LABELS = [
45        self::DEBUG => 'DEBUG',
46        self::INFO => 'INFO',
47        self::NOTICE => 'NOTICE',
48        self::WARNING => 'WARNING',
49        self::ERROR => 'ERROR',
50        self::CRITICAL => 'CRITICAL',
51        self::ALERT => 'ALERT',
52        self::EMERGENCY => 'EMERGENCY',
53    ];
54
55    protected Formatter $formatter;
56
57    public function __construct(
58        protected ?string $file = null,
59        protected string $level = self::DEBUG,
60        ?Formatter $formatter = null,
61    ) {
62        $this->formatter = $formatter ?? new TextFormatter();
63        $this->level = $this->validateLevel($level);
64    }
65
66    public function formatter(Formatter $formatter): void
67    {
68        $this->formatter = $formatter;
69    }
70
71    public function withFormatter(Formatter $formatter): self
72    {
73        $new = clone $this;
74        $new->formatter($formatter);
75
76        return $new;
77    }
78
79    #[Override]
80    public function log(
81        mixed $level,
82        string|Stringable $message,
83        array $context = [],
84    ): void {
85        $level = $this->validateLevel($level);
86
87        if (self::LEVEL_SEVERITY[$level] < self::LEVEL_SEVERITY[$this->level]) {
88            return;
89        }
90
91        $message = $this->formatter->format((string) $message, $context);
92        $message = str_replace("\0", '', $message);
93        $time = date(DATE_ATOM);
94        $line = "[{$time}" . self::LEVEL_LABELS[$level] . "{$message}";
95
96        if (is_string($this->file)) {
97            error_log($line . PHP_EOL, self::ERROR_LOG_APPEND_TO_FILE, $this->file);
98
99            return;
100        }
101
102        error_log(str_replace(["\r\n", "\r", "\n"], ' ', $line));
103    }
104
105    /** @return key-of<self::LEVEL_SEVERITY> */
106    private function validateLevel(mixed $level): string
107    {
108        if (is_string($level) && array_key_exists($level, self::LEVEL_SEVERITY)) {
109            return $level;
110        }
111
112        throw new InvalidArgumentException('Unknown log level: ' . $this->printLevel($level));
113    }
114
115    private function printLevel(mixed $level): string
116    {
117        if (is_scalar($level) || $level instanceof Stringable) {
118            return (string) $level;
119        }
120
121        return get_debug_type($level);
122    }
123}