Passed
Push — master ( bffc54...3ccd59 )
by Alexander
01:59
created

Target   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 401
Duplicated Lines 0 %

Test Coverage

Coverage 80.17%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 51
eloc 102
c 2
b 0
f 0
dl 0
loc 401
ccs 93
cts 116
cp 0.8017
rs 7.92

25 Methods

Rating   Name   Duplication   Size   Complexity  
A enable() 0 3 1
A disable() 0 3 1
A setTimestampFormat() 0 4 1
A setMessages() 0 4 1
A setEnabled() 0 5 1
A setExcept() 0 4 1
A getExcept() 0 3 1
A setExportInterval() 0 4 1
A getPrefix() 0 3 1
A getCategories() 0 3 1
C filterMessages() 0 46 15
A formatMessage() 0 17 4
A getLogVars() 0 3 1
A setPrefix() 0 4 1
A getMessagePrefix() 0 7 2
A getExportInterval() 0 3 1
A getLevels() 0 3 1
A getMessages() 0 3 1
A getTime() 0 5 2
A collect() 0 26 6
A setLevels() 0 4 1
A isEnabled() 0 7 2
A setCategories() 0 4 1
A setLogVars() 0 4 1
A getContextMessage() 0 9 2

How to fix   Complexity   

Complex Class

Complex classes like Target often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Target, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log;
6
7
use DateTime;
8
use Psr\Log\LogLevel;
9
use Throwable;
10
use Yiisoft\Arrays\ArrayHelper;
11
use Yiisoft\VarDumper\VarDumper;
12
13
use function array_merge;
14
use function count;
15
use function implode;
16
use function in_array;
17
use function is_callable;
18
use function rtrim;
19
use function strpos;
20
use function substr_compare;
21
22
/**
23
 * Target is the base class for all log target classes.
24
 *
25
 * A log target object will filter the messages logged by {@see \Yiisoft\Log\Logger} according
26
 * to its {@see Target::$levels} and {@see Target::$categories}. It may also export the filtered
27
 * messages to specific destination defined by the target, such as emails, files.
28
 *
29
 * Level filter and category filter are combinatorial, i.e., only messages
30
 * satisfying both filter conditions will be handled. Additionally, you
31
 * may specify {@see Target::$except} to exclude messages of certain categories.
32
 *
33
 * For more details and usage information on Target, see the
34
 * [guide article on logging & targets](guide:runtime-logging).
35
 */
36
abstract class Target
37
{
38
    /**
39
     * @var array list of message categories that this target is interested in.
40
     * Defaults to empty, meaning all categories.
41
     * You can use an asterisk at the end of a category so that the category may be used to
42
     * match those categories sharing the same common prefix. For example, 'Yiisoft\Db\*' will match
43
     * categories starting with 'Yiisoft\Db\', such as `Yiisoft\Db\Connection`.
44
     */
45
    private array $categories = [];
46
47
    /**
48
     * @var array list of message categories that this target is NOT interested in.
49
     * Defaults to empty, meaning no uninteresting messages.
50
     * If this property is not empty, then any category listed here will be excluded from {@see Target::$categories}.
51
     * You can use an asterisk at the end of a category so that the category can be used to
52
     * match those categories sharing the same common prefix. For example, 'Yiisoft\Db\*' will match
53
     * categories starting with 'Yiisoft\Db\', such as `Yiisoft\Db\Connection`.
54
     * @see categories
55
     */
56
    private array $except = [];
57
58
    /**
59
     * @var array the message levels that this target is interested in.
60
     *
61
     * The parameter should be an array of interested level names. See {@see LogLevel} constants for valid level names.
62
     *
63
     * For example:
64
     *
65
     * ```php
66
     * ['error', 'warning'],
67
     * // or
68
     * [LogLevel::ERROR, LogLevel::WARNING]
69
     * ```
70
     *
71
     * Defaults is empty array, meaning all available levels.
72
     */
73
    private array $levels = [];
74
75
    /**
76
     * @var array list of the PHP predefined variables that should be logged in a message.
77
     * Note that a variable must be accessible via `$GLOBALS`. Otherwise it won't be logged.
78
     *
79
     * Defaults to `['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER']`.
80
     *
81
     * Each element can also be specified as one of the following:
82
     *
83
     * - `var` - `var` will be logged.
84
     * - `var.key` - only `var[key]` key will be logged.
85
     * - `!var.key` - `var[key]` key will be excluded.
86
     *
87
     * Note that if you need $_SESSION to logged regardless if session was used you have to open it right at
88
     * the start of your request.
89
     *
90
     * @see \Yiisoft\Arrays\ArrayHelper::filter()
91
     */
92
    private array $logVars = ['_GET', '_POST', '_FILES', '_COOKIE', '_SESSION', '_SERVER'];
93
94
    /**
95
     * @var callable|null a PHP callable that returns a string to be prefixed to every exported message.
96
     *
97
     * If not set, {@see Target::getMessagePrefix()} will be used, which prefixes the message with context information
98
     * such as user IP, user ID and session ID.
99
     *
100
     * The signature of the callable should be `function ($message)`.
101
     */
102
    private $prefix;
103
104
    /**
105
     * @var int how many messages should be accumulated before they are exported.
106
     * Defaults to 1000. Note that messages will always be exported when the application terminates.
107
     * Set this property to be 0 if you don't want to export messages until the application terminates.
108
     */
109
    private int $exportInterval = 1000;
110
111
    /**
112
     * @var array the messages that are retrieved from the logger so far by this log target.
113
     * Please refer to {@see Logger::$messages} for the details about the message structure.
114
     */
115
    private array $messages = [];
116
117
    /**
118
     * @var string The date format for the log timestamp.
119
     * Defaults to Y-m-d H:i:s.u
120
     */
121
    private string $timestampFormat = 'Y-m-d H:i:s.u';
122
123
124
    /**
125
     * @var bool|callable
126
     */
127
    private $enabled = true;
128
129
    /**
130
     * Exports log {@see Target::$messages} to a specific destination.
131
     * Child classes must implement this method.
132
     */
133
    abstract public function export(): void;
134
135
    /**
136
     * Processes the given log messages.
137
     * This method will filter the given messages with {@see Target::$levels} and {@see Target::$categories}.
138
     * And if requested, it will also export the filtering result to specific medium (e.g. email).
139
     * @param array $messages log messages to be processed. See {@see Logger::$messages} for the structure
140
     * of each message.
141
     * @param bool $final whether this method is called at the end of the current application
142
     */
143 20
    public function collect(array $messages, bool $final): void
144
    {
145 20
        $this->messages = array_merge(
146 20
            $this->messages,
147 20
            static::filterMessages($messages, $this->levels, $this->categories, $this->except)
148
        );
149
150 20
        $count = count($this->messages);
151 20
        if ($count > 0 && ($final || ($this->exportInterval > 0 && $count >= $this->exportInterval))) {
152 19
            if (($context = $this->getContextMessage()) !== '') {
153
                $this->messages[] = [
154
                    LogLevel::INFO,
155
                    $context,
156
                    [
157
                        'category' => 'application',
158
                        'time' => $_SERVER['REQUEST_TIME_FLOAT']
159
                    ]
160
                ];
161
            }
162
            // set exportInterval to 0 to avoid triggering export again while exporting
163 19
            $oldExportInterval = $this->exportInterval;
164 19
            $this->exportInterval = 0;
165 19
            $this->export();
166 19
            $this->exportInterval = $oldExportInterval;
167
168 19
            $this->messages = [];
169
        }
170 20
    }
171
172
    /**
173
     * Generates the context information to be logged.
174
     * The default implementation will dump user information, system variables, etc.
175
     * @return string the context information. If an empty string, it means no context information.
176
     */
177 20
    protected function getContextMessage(): string
178
    {
179 20
        $context = ArrayHelper::filter($GLOBALS, $this->logVars);
180 20
        $result = [];
181 20
        foreach ($context as $key => $value) {
182 1
            $result[] = "\${$key} = " . VarDumper::create($value)->asString();
183
        }
184
185 20
        return implode("\n\n", $result);
186
    }
187
188
    /**
189
     * Filters the given messages according to their categories and levels.
190
     * @param array $messages messages to be filtered.
191
     * The message structure follows that in {@see Logger::$messages}.
192
     * @param array $levels the message levels to filter by. Empty value means allowing all levels.
193
     * @param array $categories the message categories to filter by. If empty, it means all categories are allowed.
194
     * @param array $except the message categories to exclude. If empty, it means all categories are allowed.
195
     * @return array the filtered messages.
196
     */
197 20
    public static function filterMessages(
198
        array $messages,
199
        array $levels = [],
200
        array $categories = [],
201
        array $except = []
202
    ): array {
203 20
        foreach ($messages as $i => $message) {
204 20
            if (!empty($levels) && !in_array($message[0], $levels, true)) {
205 7
                unset($messages[$i]);
206 7
                continue;
207
            }
208
209 20
            $matched = empty($categories);
210 20
            foreach ($categories as $category) {
211
                if (
212 12
                    $message[2]['category'] === $category
213
                    || (
214 12
                        !empty($category)
215 12
                        && substr_compare($category, '*', -1, 1) === 0
216 12
                        && strpos($message[2]['category'], rtrim($category, '*')) === 0
217
                    )
218
                ) {
219 11
                    $matched = true;
220 11
                    break;
221
                }
222
            }
223
224 20
            if ($matched) {
225 19
                foreach ($except as $category) {
226 3
                    $prefix = rtrim($category, '*');
227
                    if (
228 3
                        ($message[2]['category'] === $category || $prefix !== $category)
229 3
                        && strpos($message[2]['category'], $prefix) === 0
230
                    ) {
231 3
                        $matched = false;
232 3
                        break;
233
                    }
234
                }
235
            }
236
237 20
            if (!$matched) {
238 13
                unset($messages[$i]);
239
            }
240
        }
241
242 20
        return $messages;
243
    }
244
245
    /**
246
     * Formats a log message for display as a string.
247
     * @param array $message the log message to be formatted.
248
     * The message structure follows that in {@see Logger::$messages}.
249
     * @return string the formatted message
250
     * @throws Throwable
251
     */
252 1
    public function formatMessage(array $message): string
253
    {
254 1
        [$level, $text, $context] = $message;
255 1
        $category = $context['category'];
256 1
        $timestamp = $context['time'];
257 1
        $level = Logger::getLevelName($level);
258 1
        $traces = [];
259 1
        if (isset($context['trace'])) {
260
            foreach ($context['trace'] as $trace) {
261
                $traces[] = "in {$trace['file']}:{$trace['line']}";
262
            }
263
        }
264
265 1
        $prefix = $this->getMessagePrefix($message);
266
267 1
        return $this->getTime($timestamp) . " {$prefix}[$level][$category] $text"
268 1
            . (empty($traces) ? '' : "\n    " . implode("\n    ", $traces));
269
    }
270
271
    /**
272
     * Returns a string to be prefixed to the given message.
273
     * If {@see Target::$prefix} is configured it will return the result of the callback.
274
     * The default implementation will return user IP, user ID and session ID as a prefix.
275
     * @param array $message the message being exported.
276
     * The message structure follows that in {@see Logger::$messages}.
277
     * @return string the prefix string
278
     * @throws Throwable
279
     */
280 1
    public function getMessagePrefix(array $message): string
281
    {
282 1
        if ($this->prefix !== null) {
283
            return ($this->prefix)($message);
284
        }
285
286 1
        return '';
287
    }
288
289
    /**
290
     * Sets a value indicating whether this log target is enabled.
291
     * @param bool|callable $value a boolean value or a callable to obtain the value from.
292
     *
293
     * A callable may be used to determine whether the log target should be enabled in a dynamic way.
294
     * For example, to only enable a log if the current user is logged in you can configure the target
295
     * as follows:
296
     *
297
     * ```php
298
     * 'enabled' => function() {
299
     *     return !Yii::getApp()->user->isGuest;
300
     * }
301
     * ```
302
     * @return $this
303
     */
304 1
    public function setEnabled($value): self
305
    {
306 1
        $this->enabled = $value;
307
308 1
        return $this;
309
    }
310
311
    /**
312
     * Enables the log target
313
     *
314
     * @return $this
315
     */
316 1
    public function enable(): self
317
    {
318 1
        return $this->setEnabled(true);
319
    }
320
321
    /**
322
     * Disables the log target
323
     *
324
     * @return $this
325
     */
326 1
    public function disable(): self
327
    {
328 1
        return $this->setEnabled(false);
329
    }
330
331
    /**
332
     * Check whether the log target is enabled.
333
     * @return bool A value indicating whether this log target is enabled.
334
     */
335 21
    public function isEnabled(): bool
336
    {
337 21
        if (is_callable($this->enabled)) {
338 1
            return ($this->enabled)($this);
339
        }
340
341 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...
342
    }
343
344
    /**
345
     * Returns formatted timestamp for message, according to {@see Target::$timestampFormat}
346
     * @param float|int $timestamp
347
     * @return string
348
     */
349 1
    protected function getTime($timestamp): string
350
    {
351 1
        $timestamp = (string) $timestamp;
352 1
        $format = strpos($timestamp, '.') === false ? 'U' : 'U.u';
353 1
        return DateTime::createFromFormat($format, $timestamp)->format($this->timestampFormat);
354
    }
355
356 21
    public function setLogVars(array $logVars): self
357
    {
358 21
        $this->logVars = $logVars;
359 21
        return $this;
360
    }
361
362
    public function getLogVars(): array
363
    {
364
        return $this->logVars;
365
    }
366
367 1
    public function setTimestampFormat(string $format): self
368
    {
369 1
        $this->timestampFormat = $format;
370 1
        return $this;
371
    }
372
373
    public function getCategories(): array
374
    {
375
        return $this->categories;
376
    }
377
378 12
    public function setCategories(array $categories): self
379
    {
380 12
        $this->categories = $categories;
381 12
        return $this;
382
    }
383
384
    public function getExcept(): array
385
    {
386
        return $this->except;
387
    }
388
389 3
    public function setExcept(array $except): self
390
    {
391 3
        $this->except = $except;
392 3
        return $this;
393
    }
394
395
    public function getLevels(): array
396
    {
397
        return $this->levels;
398
    }
399
400 9
    public function setLevels(array $levels): self
401
    {
402 9
        $this->levels = $levels;
403 9
        return $this;
404
    }
405
406
    public function getPrefix(): ?callable
407
    {
408
        return $this->prefix;
409
    }
410
411
    public function setPrefix(callable $prefix): self
412
    {
413
        $this->prefix = $prefix;
414
        return $this;
415
    }
416
417
    public function getExportInterval(): int
418
    {
419
        return $this->exportInterval;
420
    }
421
422 21
    public function setExportInterval(int $exportInterval): self
423
    {
424 21
        $this->exportInterval = $exportInterval;
425 21
        return $this;
426
    }
427
428 19
    public function getMessages(): array
429
    {
430 19
        return $this->messages;
431
    }
432
433 19
    public function setMessages(array $messages): self
434
    {
435 19
        $this->messages = $messages;
436 19
        return $this;
437
    }
438
}
439