Passed
Pull Request — master (#49)
by Evgeniy
01:53
created

Target::getTime()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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