Passed
Push — master ( 2c3b5d...5ba055 )
by Alexander
01:32
created

Target::getLevels()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log;
6
7
use Psr\Log\LogLevel;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\VarDumper\VarDumper;
10
11
/**
12
 * Target is the base class for all log target classes.
13
 *
14
 * A log target object will filter the messages logged by {@see \Yiisoft\Log\Logger} according
15
 * to its {@see Target::$levels} and {@see Target::$categories}. It may also export the filtered
16
 * messages to specific destination defined by the target, such as emails, files.
17
 *
18
 * Level filter and category filter are combinatorial, i.e., only messages
19
 * satisfying both filter conditions will be handled. Additionally, you
20
 * may specify {@see Target::$except} to exclude messages of certain categories.
21
 *
22
 * For more details and usage information on Target, see the [guide article on logging & targets](guide:runtime-logging).
23
 */
24
abstract class Target
25
{
26
    /**
27
     * @var array list of message categories that this target is interested in. Defaults to empty, meaning all categories.
28
     * You can use an asterisk at the end of a category so that the category may be used to
29
     * match those categories sharing the same common prefix. For example, 'Yiisoft\Db\*' will match
30
     * categories starting with 'Yiisoft\Db\', such as `Yiisoft\Db\Connection`.
31
     */
32
    private $categories = [];
33
    /**
34
     * @var array list of message categories that this target is NOT interested in. Defaults to empty, meaning no uninteresting messages.
35
     * If this property is not empty, then any category listed here will be excluded from {@see Target::$categories}.
36
     * You can use an asterisk at the end of a category so that the category can be used to
37
     * match those categories sharing the same common prefix. For example, 'Yiisoft\Db\*' will match
38
     * categories starting with 'Yiisoft\Db\', such as `Yiisoft\Db\Connection`.
39
     * @see categories
40
     */
41
    private $except = [];
42
    /**
43
     * @var array the message levels that this target is interested in.
44
     *
45
     * The parameter should be an array of interested level names. See {@see LogLevel} constants for valid level names.
46
     *
47
     * For example:
48
     *
49
     * ```php
50
     * ['error', 'warning'],
51
     * // or
52
     * [LogLevel::ERROR, LogLevel::WARNING]
53
     * ```
54
     *
55
     * Defaults is empty array, meaning all available levels.
56
     */
57
    private $levels = [];
58
    /**
59
     * @var array list of the PHP predefined variables that should be logged in a message.
60
     * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged.
61
     *
62
     * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']`.
63
     *
64
     * Each element can also be specified as one of the following:
65
     *
66
     * - `var` - `var` will be logged.
67
     * - `var.key` - only `var[key]` key will be logged.
68
     * - `!var.key` - `var[key]` key will be excluded.
69
     *
70
     * Note that if you need $_SESSION to logged regardless if session was used you have to open it right at
71
     * the start of your request.
72
     *
73
     * @see \Yiisoft\Arrays\ArrayHelper::filter()
74
     */
75
    private $logVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'];
76
    /**
77
     * @var callable a PHP callable that returns a string to be prefixed to every exported message.
78
     *
79
     * If not set, {@see Target::getMessagePrefix()} will be used, which prefixes the message with context information
80
     * such as user IP, user ID and session ID.
81
     *
82
     * The signature of the callable should be `function ($message)`.
83
     */
84
    private $prefix;
85
86
    /**
87
     * @var int how many messages should be accumulated before they are exported.
88
     * Defaults to 1000. Note that messages will always be exported when the application terminates.
89
     * Set this property to be 0 if you don't want to export messages until the application terminates.
90
     */
91
    private $exportInterval = 1000;
92
    /**
93
     * @var array the messages that are retrieved from the logger so far by this log target.
94
     * Please refer to {@see Logger::$messages} for the details about the message structure.
95
     */
96
    private $messages = [];
97
    /**
98
     * @var string The date format for the log timestamp.
99
     * Defaults to Y-m-d H:i:s.u
100
     */
101
    private $timestampFormat = 'Y-m-d H:i:s.u';
102
103
    /**
104
     * @var bool|callable
105
     */
106
    private $enabled = true;
107
108
109
    /**
110
     * Exports log {@see Target::$messages} to a specific destination.
111
     * Child classes must implement this method.
112
     */
113
    abstract public function export(): void;
114
115
    /**
116
     * Processes the given log messages.
117
     * This method will filter the given messages with {@see Target::$levels} and {@see Target::$categories}.
118
     * And if requested, it will also export the filtering result to specific medium (e.g. email).
119
     * @param array $messages log messages to be processed. See {@see Logger::$messages} for the structure
120
     * of each message.
121
     * @param bool $final whether this method is called at the end of the current application
122
     */
123 20
    public function collect(array $messages, bool $final): void
124
    {
125 20
        $this->messages = array_merge(
126 20
            $this->messages,
127 20
            static::filterMessages($messages, $this->levels, $this->categories, $this->except)
128
        );
129
130 20
        $count = count($this->messages);
131 20
        if ($count > 0 && ($final || ($this->exportInterval > 0 && $count >= $this->exportInterval))) {
132 19
            if (($context = $this->getContextMessage()) !== '') {
133
                $this->messages[] = [
134
                    LogLevel::INFO,
135
                    $context,
136
                    [
137
                        'category' => 'application',
138
                        'time' => $_SERVER['REQUEST_TIME_FLOAT']
139
                    ]
140
                ];
141
            }
142
            // set exportInterval to 0 to avoid triggering export again while exporting
143 19
            $oldExportInterval = $this->exportInterval;
144 19
            $this->exportInterval = 0;
145 19
            $this->export();
146 19
            $this->exportInterval = $oldExportInterval;
147
148 19
            $this->messages = [];
149
        }
150 20
    }
151
152
    /**
153
     * Generates the context information to be logged.
154
     * The default implementation will dump user information, system variables, etc.
155
     * @return string the context information. If an empty string, it means no context information.
156
     */
157 20
    protected function getContextMessage(): string
158
    {
159 20
        $context = ArrayHelper::filter($GLOBALS, $this->logVars);
160 20
        $result = [];
161 20
        foreach ($context as $key => $value) {
162 1
            $result[] = "\${$key} = " . VarDumper::create($value)->asString();
163
        }
164
165 20
        return implode("\n\n", $result);
166
    }
167
168
    /**
169
     * Filters the given messages according to their categories and levels.
170
     * @param array $messages messages to be filtered.
171
     * The message structure follows that in {@see Logger::$messages}.
172
     * @param array $levels the message levels to filter by. Empty value means allowing all levels.
173
     * @param array $categories the message categories to filter by. If empty, it means all categories are allowed.
174
     * @param array $except the message categories to exclude. If empty, it means all categories are allowed.
175
     * @return array the filtered messages.
176
     */
177 20
    public static function filterMessages(array $messages, array $levels = [], array $categories = [], array $except = []): array
178
    {
179 20
        foreach ($messages as $i => $message) {
180 20
            if (!empty($levels) && !in_array($message[0], $levels, true)) {
181 7
                unset($messages[$i]);
182 7
                continue;
183
            }
184
185 20
            $matched = empty($categories);
186 20
            foreach ($categories as $category) {
187 12
                if ($message[2]['category'] === $category || (!empty($category) && substr_compare($category, '*', -1, 1) === 0 && strpos($message[2]['category'], rtrim($category, '*')) === 0)) {
188 11
                    $matched = true;
189 11
                    break;
190
                }
191
            }
192
193 20
            if ($matched) {
194 19
                foreach ($except as $category) {
195 3
                    $prefix = rtrim($category, '*');
196 3
                    if (($message[2]['category'] === $category || $prefix !== $category) && strpos($message[2]['category'], $prefix) === 0) {
197 3
                        $matched = false;
198 3
                        break;
199
                    }
200
                }
201
            }
202
203 20
            if (!$matched) {
204 13
                unset($messages[$i]);
205
            }
206
        }
207
208 20
        return $messages;
209
    }
210
211
    /**
212
     * Formats a log message for display as a string.
213
     * @param array $message the log message to be formatted.
214
     * The message structure follows that in {@see Logger::$messages}.
215
     * @return string the formatted message
216
     * @throws \Throwable
217
     */
218 1
    public function formatMessage(array $message): string
219
    {
220 1
        [$level, $text, $context] = $message;
221 1
        $category = $context['category'];
222 1
        $timestamp = $context['time'];
223 1
        $level = Logger::getLevelName($level);
224 1
        $traces = [];
225 1
        if (isset($context['trace'])) {
226
            foreach ($context['trace'] as $trace) {
227
                $traces[] = "in {$trace['file']}:{$trace['line']}";
228
            }
229
        }
230
231 1
        $prefix = $this->getMessagePrefix($message);
232
233 1
        return $this->getTime($timestamp) . " {$prefix}[$level][$category] $text"
234 1
            . (empty($traces) ? '' : "\n    " . implode("\n    ", $traces));
235
    }
236
237
    /**
238
     * Returns a string to be prefixed to the given message.
239
     * If {@see Target::$prefix} is configured it will return the result of the callback.
240
     * The default implementation will return user IP, user ID and session ID as a prefix.
241
     * @param array $message the message being exported.
242
     * The message structure follows that in {@see Logger::$messages}.
243
     * @return string the prefix string
244
     * @throws \Throwable
245
     */
246 1
    public function getMessagePrefix(array $message): string
247
    {
248 1
        if ($this->prefix !== null) {
249
            return call_user_func($this->prefix, $message);
250
        }
251
252 1
        return '';
253
    }
254
255
    /**
256
     * Sets a value indicating whether this log target is enabled.
257
     * @param bool|callable $value a boolean value or a callable to obtain the value from.
258
     *
259
     * A callable may be used to determine whether the log target should be enabled in a dynamic way.
260
     * For example, to only enable a log if the current user is logged in you can configure the target
261
     * as follows:
262
     *
263
     * ```php
264
     * 'enabled' => function() {
265
     *     return !Yii::getApp()->user->isGuest;
266
     * }
267
     * ```
268
     * @return Target
269
     */
270 1
    public function setEnabled($value): self
271
    {
272 1
        $this->enabled = $value;
273
274 1
        return $this;
275
    }
276
277
    /**
278
     * Enables the log target
279
     *
280
     * @return Target
281
     */
282 1
    public function enable(): self
283
    {
284 1
        return $this->setEnabled(true);
285
    }
286
287
    /**
288
     * Disables the log target
289
     *
290
     * @return Target
291
     */
292 1
    public function disable(): self
293
    {
294 1
        return $this->setEnabled(false);
295
    }
296
297
    /**
298
     * Check whether the log target is enabled.
299
     * @return bool A value indicating whether this log target is enabled.
300
     */
301 21
    public function isEnabled(): bool
302
    {
303 21
        if (is_callable($this->enabled)) {
304 1
            return call_user_func($this->enabled, $this);
0 ignored issues
show
Bug introduced by
It seems like $this->enabled can also be of type boolean; however, parameter $function of call_user_func() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

304
            return call_user_func(/** @scrutinizer ignore-type */ $this->enabled, $this);
Loading history...
305
        }
306
307 21
        return $this->enabled;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->enabled could return the type callable which is incompatible with the type-hinted return boolean. Consider adding an additional type-check to rule them out.
Loading history...
308
    }
309
310
    /**
311
     * Returns formatted timestamp for message, according to {@see Target::$timestampFormat}
312
     * @param float|int $timestamp
313
     * @return string
314
     */
315 1
    protected function getTime($timestamp): string
316
    {
317 1
        $timestamp = (string) $timestamp;
318 1
        $format = strpos($timestamp, '.') === false ? 'U' : 'U.u';
319 1
        return \DateTime::createFromFormat($format, $timestamp)->format($this->timestampFormat);
320
    }
321
322 21
    public function setLogVars(array $logVars): self
323
    {
324 21
        $this->logVars = $logVars;
325 21
        return $this;
326
    }
327
328
    public function getLogVars(): array
329
    {
330
        return $this->logVars;
331
    }
332
333 1
    public function setTimestampFormat(string $format): self
334
    {
335 1
        $this->timestampFormat = $format;
336 1
        return $this;
337
    }
338
339
    public function getCategories(): array
340
    {
341
        return $this->categories;
342
    }
343
344 12
    public function setCategories(array $categories): self
345
    {
346 12
        $this->categories = $categories;
347 12
        return $this;
348
    }
349
350
    public function getExcept(): array
351
    {
352
        return $this->except;
353
    }
354
355 3
    public function setExcept(array $except): self
356
    {
357 3
        $this->except = $except;
358 3
        return $this;
359
    }
360
361
    public function getLevels(): array
362
    {
363
        return $this->levels;
364
    }
365
366 9
    public function setLevels(array $levels): self
367
    {
368 9
        $this->levels = $levels;
369 9
        return $this;
370
    }
371
372
    public function getPrefix(): callable
373
    {
374
        return $this->prefix;
375
    }
376
377
    public function setPrefix(callable $prefix): self
378
    {
379
        $this->prefix = $prefix;
380
        return $this;
381
    }
382
383
    public function getExportInterval(): int
384
    {
385
        return $this->exportInterval;
386
    }
387
388 21
    public function setExportInterval(int $exportInterval): self
389
    {
390 21
        $this->exportInterval = $exportInterval;
391 21
        return $this;
392
    }
393
394 19
    public function getMessages(): array
395
    {
396 19
        return $this->messages;
397
    }
398
399 19
    public function setMessages(array $messages): self
400
    {
401 19
        $this->messages = $messages;
402 19
        return $this;
403
    }
404
}
405