Passed
Push — master ( 61f3a0...6b4c98 )
by Alexander
01:39
created

Target   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Test Coverage

Coverage 79.27%

Importance

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

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