StreamLogger::log()   C
last analyzed

Complexity

Conditions 12
Paths 29

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 12

Importance

Changes 0
Metric Value
cc 12
nc 29
nop 3
dl 0
loc 41
ccs 27
cts 27
cp 1
crap 12
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2018 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Cli;
9
10
use Psr\Log\InvalidArgumentException;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\LoggerTrait;
13
use Psr\Log\LogLevel;
14
15
class StreamLogger implements LoggerInterface {
16
    use LoggerTrait;
17
18
    /**
19
     * @var string The line format.
20
     */
21
    private $lineFormat = '[{time}] {message}';
22
23
    /**
24
     * @var callable The level formatting function.
25
     */
26
    private $levelFormat;
27
28
    /**
29
     * @var callable The time formatting function.
30
     */
31
    private $timeFormatter;
32
33
    /**
34
     * @var string The end of line string to use.
35
     */
36
    private $eol = PHP_EOL;
37
38
    /**
39
     * @var bool Whether or not to format output.
40
     */
41
    private $colorizeOutput;
42
43
    /**
44
     * @var resource The output file handle.
45
     */
46
    private $outputHandle;
47
48
    /**
49
     * @var bool Whether or not the console is on a new line.
50
     */
51
    private $inBegin;
52
53
    /**
54
     * @var bool Whether or not to show durations for tasks.
55
     */
56
    private $showDurations = true;
57
58
    /**
59
     * @var bool Whether or not to buffer begin logs.
60
     */
61
    private $bufferBegins = true;
62
63
    private $wraps = [
64
        LogLevel::DEBUG => ["\033[0;37m", "\033[0m"],
65
        LogLevel::INFO => ['', ''],
66
        LogLevel::NOTICE => ["\033[1m", "\033[0m"],
67
        LogLevel::WARNING => ["\033[1;33m", "\033[0m"],
68
        LogLevel::ERROR => ["\033[1;31m", "\033[0m"],
69
        LogLevel::CRITICAL => ["\033[1;35m", "\033[0m"],
70
        LogLevel::ALERT => ["\033[1;35m", "\033[0m"],
71
        LogLevel::EMERGENCY => ["\033[1;35m", "\033[0m"],
72
    ];
73
74
    /**
75
     * @var resource Whether or not the default stream was opened.
76
     */
77
    private $defaultStream = null;
78
79
    /**
80
     * LogFormatter constructor.
81
     *
82
     * @param mixed $out Either a path or a stream resource for the output.
83
     */
84 51
    public function __construct($out = STDOUT) {
85 51
        if (is_string($out)) {
86
            try {
87 23
                $this->defaultStream = $out = fopen($out, 'a+');
0 ignored issues
show
Documentation Bug introduced by
It seems like $out = fopen($out, 'a+') can also be of type false. However, the property $defaultStream is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
88 1
            } catch (\Throwable $ex) {
89 1
                throw new \InvalidArgumentException($ex->getMessage(), 500);
90
            }
91 23
            if (!is_resource($out)) {
92 23
                throw new \InvalidArgumentException("The supplied path could not be opened: $out", 500);
93
            }
94 30
        } elseif (!is_resource($out)) {
95 1
            throw new \InvalidArgumentException('The value supplied for $out must be either a stream resource or a path.', 500);
96
        }
97
98 51
        $this->outputHandle = $out;
99 51
        $this->colorizeOutput = Cli::guessFormatOutput($this->outputHandle);
100 51
        $this->setTimeFormat('%F %T');
101 51
        $this->setLevelFormat(function ($l) {
102 42
            return $l;
103 51
        });
104
105 51
    }
106
107
    /**
108
     * Set the time formatter.
109
     *
110
     * This method takes either a format string for **strftime()** or a callable that must format a timestamp.
111
     *
112
     * @param string|callable $format The new format.
113
     * @return $this
114
     * @see strftime()
115
     */
116 51
    public function setTimeFormat($format) {
117 51
        if (is_string($format)) {
118 51
            $this->timeFormatter = function ($t) use ($format) {
119 42
                return strftime($format, $t);
120 51
            };
121
        } else {
122 1
            $this->timeFormatter = $format;
123
        }
124
125 51
        return $this;
126
    }
127
128
    /**
129
     * Clean up the default stream if it was use.
130
     */
131
    public function __destruct() {
132
        if (is_resource($this->defaultStream)) {
133
            fclose($this->defaultStream);
134
        }
135
    }
136
137
    /**
138
     * Logs with an arbitrary level.
139
     *
140
     * @param mixed $level
141
     * @param string $message
142
     * @param array $context
143
     *
144
     * @return void
145
     */
146 44
    public function log($level, $message, array $context = array()) {
147 44
        if (!isset($this->wraps[$level])) {
148 1
            throw new InvalidArgumentException("Invalid log level: $level", 400);
149
        }
150
151 43
        $msg = $this->replaceContext($message, $context);
152
153 43
        $eol = true;
154 43
        $fullLine = true;
155 43
        $str = ''; // queue everything in a string to avoid race conditions
156
157 43
        if ($this->bufferBegins()) {
158 42
            if (!empty($context[TaskLogger::FIELD_BEGIN])) {
159 6
                if ($this->inBegin) {
160 1
                    $str .= $this->eol;
161
                } else {
162 6
                    $this->inBegin = true;
163
                }
164 6
                $eol = false;
165 42
            } elseif (!empty($context[TaskLogger::FIELD_END]) && strpos($msg, "\n") === false) {
166 6
                if ($this->inBegin) {
167 4
                    $msg = ' '.$msg;
168 4
                    $fullLine = false;
169 6
                    $this->inBegin = false;
170
                }
171 38
            } elseif ($this->inBegin) {
172 2
                $str .= $this->eol;
173 2
                $this->inBegin = false;
174
            }
175
        }
176
177 43
        $str .= $this->fullMessageStr($level, $msg, $context, $fullLine);
178
179 43
        if ($eol) {
180 43
            $str .= $this->eol;
181
        }
182
183 43
        if (!is_resource($this->outputHandle) || feof($this->outputHandle)) {
184 1
            trigger_error('The StreamLogger output handle is not valid.', E_USER_WARNING);
185
        } else {
186 42
            fwrite($this->outputHandle, $str);
187
        }
188 43
    }
189
190
    /**
191
     * Replace a message format with context information.
192
     *
193
     * The message format contains fields wrapped in curly braces.
194
     *
195
     * @param string $format The message format to replace.
196
     * @param array $context The context data.
197
     * @return string Returns the formatted message.
198
     */
199
    private function replaceContext(string $format, array $context): string {
200 43
        $msg = preg_replace_callback('`({[^\s{}]+})`', function ($m) use ($context) {
201 43
            $field = trim($m[1], '{}');
202 43
            if (array_key_exists($field, $context)) {
203 43
                return $context[$field];
204
            } else {
205 2
                return $m[1];
206
            }
207 43
        }, $format);
208 43
        return $msg;
209
    }
210
211
    /**
212
     * Whether not to buffer the newline for begins.
213
     *
214
     * When logging a begin this setting will buffer the newline and output the end of the task on the same line if possible.
215
     *
216
     * @return bool Returns the bufferBegins.
217
     */
218 43
    public function bufferBegins(): bool {
219 43
        return $this->bufferBegins;
220
    }
221
222 43
    private function fullMessageStr($level, $message, $context, $fullLine = true): string {
223 43
        $levelStr = call_user_func($this->getLevelFormat(), $level);
224
225 43
        $timeStr = call_user_func($this->getTimeFormat(), $context[TaskLogger::FIELD_TIME] ?? microtime(true));
226
227 43
        $indent = $context[TaskLogger::FIELD_INDENT] ?? 0;
228 43
        if ($indent <= 0) {
229 41
            $indentStr = '';
230
        } else {
231 2
            $indentStr = str_repeat('  ', $indent - 1).'- ';
232
        }
233
234
        // Explode on "\n" because the input string may have a variety of newlines.
235 43
        $lines = explode("\n", $message);
236 43
        if ($fullLine) {
237 43
            foreach ($lines as &$line) {
238 43
                $line = rtrim($line);
239 43
                $line = $this->replaceContext($this->getLineFormat(), [
240 43
                    'level' => $levelStr,
241 43
                    'time' => $timeStr,
242 43
                    'message' => $indentStr.$line
243
                ]);
244
            }
245
        }
246 43
        $result = implode($this->getEol(), $lines);
247
248 43
        $wrap = $this->wraps[$level] ?? ['', ''];
249 43
        $result = $this->formatString($result, $wrap);
250
251 43
        if (isset($context[TaskLogger::FIELD_DURATION]) && $this->showDurations()) {
252 2
            if ($result && !preg_match('`\s$`', $result)) {
253 1
                $result .= ' ';
254
            }
255
256 2
            $result .= $this->formatString($this->formatDuration($context[TaskLogger::FIELD_DURATION]), ["\033[1;34m", "\033[0m"]);
257
        }
258
259 43
        return $result;
260
    }
261
262
    /**
263
     * Get the level formatting function.
264
     *
265
     * @return callable Returns the levelFormat.
266
     */
267 43
    public function getLevelFormat(): callable {
268 43
        return $this->levelFormat;
269
    }
270
271
    /**
272
     * Set the level formatting function.
273
     *
274
     * @param callable $levelFormat The new level format.
275
     * @return $this
276
     */
277 51
    public function setLevelFormat(callable $levelFormat) {
278 51
        $this->levelFormat = $levelFormat;
279 51
        return $this;
280
    }
281
282
    /**
283
     * Get the time format function.
284
     *
285
     * @return callable Returns the date format.
286
     * @see strftime()
287
     */
288 43
    public function getTimeFormat(): callable {
289 43
        return $this->timeFormatter;
290
    }
291
292
    /**
293
     * Get the log line format.
294
     *
295
     * The log line format determines how lines are outputted. The line consists of fields enclosed in curly braces and
296
     * other raw strings. The fields available for the format are the following:
297
     *
298
     * - `{level}`: Output the log level.
299
     * - `{time}`: Output the time of the log.
300
     * - `{message}`: Output the message string.
301
     *
302
     * @return string Returns the lineFormat.
303
     */
304 43
    public function getLineFormat(): string {
305 43
        return $this->lineFormat;
306
    }
307
308
    /**
309
     * Set the log line format.
310
     *
311
     * @param string $lineFormat The new line format.
312
     * @return $this
313
     */
314 29
    public function setLineFormat(string $lineFormat) {
315 29
        $this->lineFormat = $lineFormat;
316 29
        return $this;
317
    }
318
319
    /**
320
     * Get the end of line string to use.
321
     *
322
     * @return string Returns the eol string.
323
     */
324 43
    public function getEol(): string {
325 43
        return $this->eol;
326
    }
327
328
    /**
329
     * Set the end of line string.
330
     *
331
     * @param string $eol The end of line string to use.
332
     * @return $this
333
     */
334 2
    public function setEol(string $eol) {
335 2
        if (strpos($eol, "\n") === false) {
336 1
            throw new \InvalidArgumentException('The EOL must include the "\n" character."', 500);
337
        }
338
339 1
        $this->eol = $eol;
340 1
        return $this;
341
    }
342
343
    /**
344
     * Format some text for the console.
345
     *
346
     * @param string $text The text to format.
347
     * @param string[] $wrap The format to wrap in the form ['before', 'after'].
348
     * @return string Returns the string formatted according to {@link Cli::$format}.
349
     */
350 43
    private function formatString(string $text, array $wrap): string {
351 43
        if ($this->colorizeOutput()) {
352 1
            return "{$wrap[0]}$text{$wrap[1]}";
353
        } else {
354 42
            return $text;
355
        }
356
    }
357
358
    /**
359
     * Get the showDurations.
360
     *
361
     * @return boolean Returns the showDurations.
362
     */
363 2
    public function showDurations(): bool {
364 2
        return $this->showDurations;
365
    }
366
367
    /**
368
     * Format a time duration.
369
     *
370
     * @param float $duration The duration in seconds and fractions of a second.
371
     * @return string Returns the duration formatted for humans.
372
     * @see microtime()
373
     */
374 3
    private function formatDuration(float $duration): string {
375 3
        if ($duration < 1.0e-3) {
376 1
            $n = number_format($duration * 1.0e6, 0);
377 1
            $sx = 'μs';
378 3
        } elseif ($duration < 1) {
379 1
            $n = number_format($duration * 1000, 0);
380 1
            $sx = 'ms';
381 3
        } elseif ($duration < 60) {
382 3
            $n = number_format($duration, 1);
383 3
            $sx = 's';
384 1
        } elseif ($duration < 3600) {
385 1
            $n = number_format($duration / 60, 1);
386 1
            $sx = 'm';
387 1
        } elseif ($duration < 86400) {
388 1
            $n = number_format($duration / 3600, 1);
389 1
            $sx = 'h';
390
        } else {
391 1
            $n = number_format($duration / 86400, 1);
392 1
            $sx = 'd';
393
        }
394
395 3
        $result = rtrim($n, '0.').$sx;
396 3
        return $result;
397
    }
398
399
    /**
400
     * Whether or not to format console output.
401
     *
402
     * @return bool Returns the format output setting.
403
     */
404 43
    public function colorizeOutput(): bool {
405 43
        return $this->colorizeOutput;
406
    }
407
408
    /**
409
     * Set the showDurations.
410
     *
411
     * @param bool $showDurations
412
     * @return $this
413
     */
414 51
    public function setShowDurations(bool $showDurations) {
415 51
        $this->showDurations = $showDurations;
416 51
        return $this;
417
    }
418
419
    /**
420
     * Set whether not to buffer the newline for begins.
421
     *
422
     * @param bool $bufferBegins The new value.
423
     * @return $this
424
     */
425 1
    public function setBufferBegins(bool $bufferBegins) {
426 1
        $this->bufferBegins = $bufferBegins;
427 1
        return $this;
428
    }
429
430
    /**
431
     * Set whether or not to format console output.
432
     *
433
     * @param bool $colorizeOutput The new value.
434
     * @return $this
435
     */
436 1
    public function setColorizeOutput(bool $colorizeOutput) {
437 1
        $this->colorizeOutput = $colorizeOutput;
438 1
        return $this;
439
    }
440
}
441