Passed
Pull Request — master (#77)
by Wilmer
02:34
created

Formatter::getTrace()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 5
nc 6
nop 1
dl 0
loc 11
ccs 6
cts 6
cp 1
crap 4
rs 10
c 1
b 0
f 0
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
use function strpos;
20
21
/**
22
 * Formatter formats log messages.
23
 *
24
 * @internal
25
 */
26
final class Formatter
27
{
28
    /**
29
     * @var callable|null PHP callable that returns a string representation of the log message.
30
     *
31
     * If not set, {@see Formatter::defaultFormat()} will be used.
32
     *
33
     * The signature of the callable should be `function (Message $message, array $commonContext): string;`.
34
     */
35
    private $format;
36
37
    /**
38
     * @var callable|null PHP callable that returns a string to be prefixed to every exported message.
39
     *
40
     * If not set, {@see Formatter::getPrefix()} will be used, which prefixes
41
     * the message with context information such as user IP, user ID and session ID.
42
     *
43
     * The signature of the callable should be `function (Message $message, array $commonContext): string;`.
44
     */
45
    private $prefix;
46
47
    /**
48
     * @var string The date format for the log timestamp. Defaults to `Y-m-d H:i:s.u`.
49
     */
50
    private string $timestampFormat = 'Y-m-d H:i:s.u';
51
52
    /**
53
     * Sets the format for the string representation of the log message.
54
     *
55
     * @param callable $format The PHP callable to get a string representation of the log message.
56
     *
57
     * @see Formatter::$format
58
     */
59 35
    public function setFormat(callable $format): void
60
    {
61 35
        $this->format = $format;
62
    }
63
64
    /**
65
     * Sets a PHP callable that returns a string to be prefixed to every exported message.
66
     *
67
     * @param callable $prefix The PHP callable to get a string prefix of the log message.
68
     *
69
     * @see Formatter::$prefix
70
     */
71 21
    public function setPrefix(callable $prefix): void
72
    {
73 21
        $this->prefix = $prefix;
74
    }
75
76
    /**
77
     * Sets a date format for the log timestamp.
78
     *
79
     * @param string $timestampFormat The date format for the log timestamp.
80
     *
81
     * @see Formatter::$timestampFormat
82
     */
83 6
    public function setTimestampFormat(string $timestampFormat): void
84
    {
85 6
        $this->timestampFormat = $timestampFormat;
86
    }
87
88
    /**
89
     * Formats a log message for display as a string.
90
     *
91
     * @param Message $message The log message to be formatted.
92
     * @param array $commonContext The user parameters in the `key => value` format.
93
     *
94
     * @throws RuntimeException for a callable "format" that does not return a string.
95
     *
96
     * @return string The formatted log message.
97
     */
98 84
    public function format(Message $message, array $commonContext): string
99
    {
100 84
        if ($this->format === null) {
101 53
            return $this->defaultFormat($message, $commonContext);
102
        }
103
104 31
        $formatted = ($this->format)($message, $commonContext);
105
106 31
        if (!is_string($formatted)) {
107 13
            throw new RuntimeException(sprintf(
108
                'The PHP callable "format" must return a string, %s received.',
109 13
                gettype($formatted)
110
            ));
111
        }
112
113 18
        return $this->getPrefix($message, $commonContext) . $formatted;
114
    }
115
116
    /**
117
     * Default formats a log message for display as a string.
118
     *
119
     * @param Message $message The log message to be default formatted.
120
     * @param array $commonContext The user parameters in the `key => value` format.
121
     *
122
     * @return string The default formatted log message.
123
     */
124 53
    private function defaultFormat(Message $message, array $commonContext): string
125
    {
126 53
        $time = $this->getTime($message);
127 53
        $prefix = $this->getPrefix($message, $commonContext);
128 40
        $context = $this->getContext($message, $commonContext);
129 40
        $category = $message->context('category', CategoryFilter::DEFAULT);
130
131 40
        return "{$time} {$prefix}[{$message->level()}][{$category}] {$message->message()}{$context}";
132
    }
133
134
    /**
135
     * Gets formatted timestamp for message, according to {@see Formatter::$timestampFormat}.
136
     *
137
     * @param Message $message The log message.
138
     *
139
     * @return string Formatted timestamp for message.
140
     */
141 53
    private function getTime(Message $message): string
142
    {
143 53
        $timestamp = (string) $message->context('time', microtime(true));
144 53
        $format = 'U';
145
146 53
        if (strpos($timestamp, '.') !== false) {
147 50
            $format = 'U.u';
148
        }
149
150 53
        if (strpos($timestamp, ',') !== false) {
151 1
            $format = 'U,u';
152
        }
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
                'The PHP callable "prefix" must return a string, %s received.',
181 13
                gettype($prefix)
182
            ));
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 40
        foreach ($message->context() as $name => $value) {
207 40
            if ($name !== 'trace') {
208 40
                $context[] = "{$name}: " . $this->convertToString($value);
209
            }
210
        }
211
212 40
        foreach ($commonContext as $name => $value) {
213 16
            $common[] = "{$name}: " . $this->convertToString($value);
214
        }
215
216 40
        return (empty($context) ? '' : "\n\nMessage context:\n\n" . implode("\n", $context))
217 40
            . (empty($common) ? '' : "\n\nCommon context:\n\n" . implode("\n", $common)) . "\n";
218
    }
219
220
    /**
221
     * Gets debug backtrace in string representation.
222
     *
223
     * @param Message $message The log message.
224
     *
225
     * @return string Debug backtrace in string representation.
226
     */
227 40
    private function getTrace(Message $message): string
228
    {
229 40
        $traces = (array) $message->context('trace', []);
230
231 40
        foreach ($traces as $key => $trace) {
232 2
            if (isset($trace['file'], $trace['line'])) {
233 2
                $traces[$key] = "in {$trace['file']}:{$trace['line']}";
234
            }
235
        }
236
237 40
        return empty($traces) ? '' : "trace:\n    " . implode("\n    ", $traces);
238
    }
239
240
    /**
241
     * Converts a value to a string.
242
     *
243
     * @param mixed $value The value to convert
244
     *
245
     * @return string Converted string.
246
     */
247 40
    private function convertToString($value): string
248
    {
249 40
        if (is_object($value) && method_exists($value, '__toString')) {
250 8
            return $value->__toString();
251
        }
252
253 40
        return VarDumper::create($value)->asString();
254
    }
255
}
256