Passed
Push — main ( 113eee...af6a91 )
by Thomas
04:24
created

Logger::getExceptionMessage()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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