Logger::log()   A
last analyzed

Complexity

Conditions 6
Paths 5

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
dl 0
loc 31
ccs 16
cts 16
cp 1
rs 9.0777
c 1
b 0
f 0
cc 6
nc 5
nop 3
crap 6
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Conia\Chuck;
6
7
use DateTimeInterface;
8
use Psr\Log\InvalidArgumentException;
9
use Psr\Log\LoggerInterface as PsrLogger;
10
use Stringable;
11
use Throwable;
12
13
/** @psalm-api */
14
class Logger implements PsrLogger
15
{
16
    public const DEBUG = 100;
17
    public const INFO = 200;
18
    public const NOTICE = 300;
19
    public const WARNING = 400;
20
    public const ERROR = 500;
21
    public const CRITICAL = 600;
22
    public const ALERT = 700;
23
    public const EMERGENCY = 800;
24
25
    /** @psalm-var array<int, non-empty-string> */
26
    protected array $levelLabels;
27
28 7
    public function __construct(
29
        protected int $minimumLevel = self::DEBUG,
30
        protected ?string $logfile = null,
31
    ) {
32 7
        $this->levelLabels = [
33 7
            self::DEBUG => 'DEBUG',
34 7
            self::INFO => 'INFO',
35 7
            self::NOTICE => 'NOTICE',
36 7
            self::WARNING => 'WARNING',
37 7
            self::ERROR => 'ERROR',
38 7
            self::CRITICAL => 'CRITICAL',
39 7
            self::ALERT => 'ALERT',
40 7
            self::EMERGENCY => 'EMERGENCY',
41 7
        ];
42
    }
43
44 6
    public function log(
45
        mixed $level,
46
        string|Stringable $message,
47
        array $context = [],
48
    ): void {
49 6
        $message = (string)$message;
50 6
        assert(is_int($level) || is_numeric($level));
51 6
        $level = (int)$level;
52
53 6
        if ($level < $this->minimumLevel) {
54 1
            return;
55
        }
56
57
        try {
58 6
            $levelLabel = $this->levelLabels[$level];
59 1
        } catch (Throwable) {
60 1
            throw new InvalidArgumentException('Unknown log level: ' . (string)$level);
61
        }
62
63 5
        $message = $this->interpolate(str_replace("\0", '', $message), $context);
64
65 5
        if (is_string($this->logfile)) {
66 4
            $time = date('Y-m-d H:i:s D T');
67 4
            error_log("[{$time}] {$levelLabel}: {$message}", 3, $this->logfile);
68
69 4
            if (PHP_SAPI == 'cli') {
70
                // print it additionally to stderr
71 4
                error_log("{$levelLabel}: {$message}");
72
            }
73
        } else {
74 1
            error_log("{$levelLabel}: {$message}");
75
        }
76
    }
77
78 3
    public function debug(string|Stringable $message, array $context = []): void
79
    {
80 3
        $this->log(self::DEBUG, $message, $context);
81
    }
82
83 3
    public function info(string|Stringable $message, array $context = []): void
84
    {
85 3
        $this->log(self::INFO, $message, $context);
86
    }
87
88 2
    public function notice(string|Stringable $message, array $context = []): void
89
    {
90 2
        $this->log(self::NOTICE, $message, $context);
91
    }
92
93 4
    public function warning(string|Stringable $message, array $context = []): void
94
    {
95 4
        $this->log(self::WARNING, $message, $context);
96
    }
97
98 4
    public function error(string|Stringable $message, array $context = []): void
99
    {
100 4
        $this->log(self::ERROR, $message, $context);
101
    }
102
103 2
    public function critical(string|Stringable $message, array $context = []): void
104
    {
105 2
        $this->log(self::CRITICAL, $message, $context);
106
    }
107
108 3
    public function alert(string|Stringable $message, array $context = []): void
109
    {
110 3
        $this->log(self::ALERT, $message, $context);
111
    }
112
113 2
    public function emergency(string|Stringable $message, array $context = []): void
114
    {
115 2
        $this->log(self::EMERGENCY, $message, $context);
116
    }
117
118 5
    protected function interpolate(string $template, array $context): string
119
    {
120 5
        $substitudes = [];
121
122
        /**
123
         * @psalm-suppress MixedAssignment
124
         *
125
         * $value types are exhaustively checked
126
         */
127 5
        foreach ($context as $key => $value) {
128 2
            $placeholder = '{' . $key . '}';
129
130 2
            if (strpos($template, $placeholder) === false) {
131 2
                continue;
132
            }
133
134 1
            $substitudes[$placeholder] = match (true) {
135 1
                (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) => (string)$value,
136 1
                $value instanceof DateTimeInterface => $value->format('Y-m-d H:i:s T'),
137 1
                is_object($value) => '[Instance of ' . $value::class . ']',
138 1
                is_array($value) => '[Array ' . json_encode($value, JSON_UNESCAPED_SLASHES) . ']',
139 1
                is_null($value) => '[null]',
140 1
                default => '[' . get_debug_type($value) . ']',
141 1
            };
142
        }
143
144 5
        $message = strtr($template, $substitudes);
145 5
        $message .= $this->getExceptionMessage($context);
146
147 5
        return $message;
148
    }
149
150 5
    protected function getExceptionMessage(array $context): string
151
    {
152 5
        $message = '';
153
154
        if (
155 5
            array_key_exists('exception', $context)
156 5
            && $context['exception'] instanceof Throwable
157
        ) {
158 2
            $message .= "\n    Exception Message: " . $context['exception']->getMessage() . "\n\n";
159 2
            $message .= implode('    #', explode('#', $context['exception']->getTraceAsString()));
160
        }
161
162 5
        return $message;
163
    }
164
}
165