Formatter   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 237
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 55
c 2
b 0
f 0
dl 0
loc 237
ccs 66
cts 66
cp 1
rs 10
wmc 23

10 Methods

Rating   Name   Duplication   Size   Complexity  
A setFormat() 0 3 1
A setTimestampFormat() 0 3 1
A setPrefix() 0 3 1
B getContext() 0 29 7
A defaultFormat() 0 9 1
A convertToString() 0 7 3
A getTrace() 0 14 2
A format() 0 16 3
A getPrefix() 0 16 3
A getTime() 0 12 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log\Message;
6
7
use DateTime;
8
use RuntimeException;
9
use Yiisoft\Log\Message;
10
use Yiisoft\VarDumper\VarDumper;
11
12
use function gettype;
13
use function implode;
14
use function is_string;
15
use function is_object;
16
use function method_exists;
17
use function microtime;
18
use function sprintf;
19
20
/**
21
 * Formatter formats log messages.
22
 *
23
 * @internal
24
 *
25
 * @psalm-import-type Backtrace from Message
26
 */
27
final class Formatter
28
{
29
    /**
30
     * @var callable|null PHP callable that returns a string representation of the log message.
31
     *
32
     * If not set, {@see Formatter::defaultFormat()} will be used.
33
     *
34
     * The signature of the callable should be `function (Message $message, array $commonContext): string;`.
35
     */
36
    private $format;
37
38
    /**
39
     * @var callable|null PHP callable that returns a string to be prefixed to every exported message.
40
     *
41
     * If not set, {@see Formatter::getPrefix()} will be used, which prefixes
42
     * the message with context information such as user IP, user ID and session ID.
43
     *
44
     * The signature of the callable should be `function (Message $message, array $commonContext): string;`.
45
     */
46
    private $prefix;
47
48
    /**
49
     * @var string The date format for the log timestamp. Defaults to `Y-m-d H:i:s.u`.
50
     */
51
    private string $timestampFormat = 'Y-m-d H:i:s.u';
52
53
    /**
54
     * Sets the format for the string representation of the log message.
55
     *
56
     * @param callable $format The PHP callable to get a string representation of the log message.
57
     *
58
     * @see Formatter::$format
59
     */
60 35
    public function setFormat(callable $format): void
61
    {
62 35
        $this->format = $format;
63
    }
64
65
    /**
66
     * Sets a PHP callable that returns a string to be prefixed to every exported message.
67
     *
68
     * @param callable $prefix The PHP callable to get a string prefix of the log message.
69
     *
70
     * @see Formatter::$prefix
71
     */
72 21
    public function setPrefix(callable $prefix): void
73
    {
74 21
        $this->prefix = $prefix;
75
    }
76
77
    /**
78
     * Sets a date format for the log timestamp.
79
     *
80
     * @param string $timestampFormat The date format for the log timestamp.
81
     *
82
     * @see Formatter::$timestampFormat
83
     */
84 6
    public function setTimestampFormat(string $timestampFormat): void
85
    {
86 6
        $this->timestampFormat = $timestampFormat;
87
    }
88
89
    /**
90
     * Formats a log message for display as a string.
91
     *
92
     * @param Message $message The log message to be formatted.
93
     * @param array $commonContext The user parameters in the `key => value` format.
94
     *
95
     * @throws RuntimeException for a callable "format" that does not return a string.
96
     *
97
     * @return string The formatted log message.
98
     */
99 84
    public function format(Message $message, array $commonContext): string
100
    {
101 84
        if ($this->format === null) {
102 53
            return $this->defaultFormat($message, $commonContext);
103
        }
104
105 31
        $formatted = ($this->format)($message, $commonContext);
106
107 31
        if (!is_string($formatted)) {
108 13
            throw new RuntimeException(sprintf(
109 13
                'The PHP callable "format" must return a string, %s received.',
110 13
                gettype($formatted)
111 13
            ));
112
        }
113
114 18
        return $this->getPrefix($message, $commonContext) . $formatted;
115
    }
116
117
    /**
118
     * Default formats a log message for display as a string.
119
     *
120
     * @param Message $message The log message to be default formatted.
121
     * @param array $commonContext The user parameters in the `key => value` format.
122
     *
123
     * @return string The default formatted log message.
124
     */
125 53
    private function defaultFormat(Message $message, array $commonContext): string
126
    {
127 53
        $time = $this->getTime($message);
128 53
        $prefix = $this->getPrefix($message, $commonContext);
129 40
        $context = $this->getContext($message, $commonContext);
130
        /** @var string $category */
131 40
        $category = $message->context('category', CategoryFilter::DEFAULT);
132
133 40
        return "{$time} {$prefix}[{$message->level()}][{$category}] {$message->message()}{$context}";
134
    }
135
136
    /**
137
     * Gets formatted timestamp for message, according to {@see Formatter::$timestampFormat}.
138
     *
139
     * @param Message $message The log message.
140
     *
141
     * @return string Formatted timestamp for message.
142
     */
143 53
    private function getTime(Message $message): string
144
    {
145
        /** @psalm-suppress PossiblyInvalidCast */
146 53
        $timestamp = (string) $message->context('time', microtime(true));
147
148 53
        $format = match (true) {
149 53
            str_contains($timestamp, '.') => 'U.u',
150 53
            str_contains($timestamp, ',') => 'U,u',
151 53
            default => 'U',
152 53
        };
153
154 53
        return DateTime::createFromFormat($format, $timestamp)->format($this->timestampFormat);
155
    }
156
157
    /**
158
     * Gets a string to be prefixed to the given message.
159
     *
160
     * If {@see Formatter::$prefix} is configured it will return the result of the callback.
161
     * The default implementation will return user IP, user ID and session ID as a prefix.
162
     *
163
     * @param Message $message The log message being exported.
164
     * @param array $commonContext The user parameters in the `key => value` format.
165
     *
166
     * @throws RuntimeException for a callable "prefix" that does not return a string.
167
     *
168
     * @return string The log prefix string.
169
     */
170 71
    private function getPrefix(Message $message, array $commonContext): string
171
    {
172 71
        if ($this->prefix === null) {
173 50
            return '';
174
        }
175
176 21
        $prefix = ($this->prefix)($message, $commonContext);
177
178 21
        if (!is_string($prefix)) {
179 13
            throw new RuntimeException(sprintf(
180 13
                'The PHP callable "prefix" must return a string, %s received.',
181 13
                gettype($prefix)
182 13
            ));
183
        }
184
185 8
        return $prefix;
186
    }
187
188
    /**
189
     * Gets the context information to be logged.
190
     *
191
     * @param Message $message The log message.
192
     * @param array $commonContext The user parameters in the `key => value` format.
193
     *
194
     * @return string The context information. If an empty string, it means no context information.
195
     */
196 40
    private function getContext(Message $message, array $commonContext): string
197
    {
198 40
        $trace = $this->getTrace($message);
199 40
        $context = [];
200 40
        $common = [];
201
202 40
        if ($trace !== '') {
203 2
            $context[] = $trace;
204
        }
205
206
        /**
207
         * @var array-key $name
208
         * @var mixed $value
209
         */
210 40
        foreach ($message->context() as $name => $value) {
211 40
            if ($name !== 'trace') {
212 40
                $context[] = "{$name}: " . $this->convertToString($value);
213
            }
214
        }
215
216
        /**
217
         * @var mixed $value
218
         */
219 40
        foreach ($commonContext as $name => $value) {
220 16
            $common[] = "{$name}: " . $this->convertToString($value);
221
        }
222
223 40
        return (empty($context) ? '' : "\n\nMessage context:\n\n" . implode("\n", $context))
224 40
            . (empty($common) ? '' : "\n\nCommon context:\n\n" . implode("\n", $common)) . "\n";
225
    }
226
227
    /**
228
     * Gets debug backtrace in string representation.
229
     *
230
     * @param Message $message The log message.
231
     *
232
     * @return string Debug backtrace in string representation.
233
     */
234 40
    private function getTrace(Message $message): string
235
    {
236
        /** @psalm-var Backtrace $traces */
237 40
        $traces = $message->context('trace', []);
238 40
        if (empty($traces)) {
239 38
            return '';
240
        }
241
242 2
        $lines = array_map(
243 2
            static fn (array $trace): string => "in {$trace['file']}:{$trace['line']}",
244 2
            $traces,
245 2
        );
246
247 2
        return "trace:\n    " . implode("\n    ", $lines);
248
    }
249
250
    /**
251
     * Converts a value to a string.
252
     *
253
     * @param mixed $value The value to convert.
254
     *
255
     * @return string Converted string.
256
     */
257 40
    private function convertToString(mixed $value): string
258
    {
259 40
        if (is_object($value) && method_exists($value, '__toString')) {
260 8
            return (string) $value;
261
        }
262
263 40
        return VarDumper::create($value)->asString();
264
    }
265
}
266