Completed
Push — master ( f2a0bc...5b97c7 )
by Todd
03:27
created

StreamLogger   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 424
Duplicated Lines 0 %

Test Coverage

Coverage 97.2%

Importance

Changes 0
Metric Value
dl 0
loc 424
ccs 139
cts 143
cp 0.972
rs 6.96
c 0
b 0
f 0
wmc 53

21 Methods

Rating   Name   Duplication   Size   Complexity  
C log() 0 41 12
A __destruct() 0 3 2
A setTimeFormat() 0 10 2
A replaceContext() 0 10 2
A bufferBegins() 0 2 1
A __construct() 0 19 5
A colorizeOutput() 0 2 1
B formatDuration() 0 23 6
A showDurations() 0 2 1
A getLineFormat() 0 2 1
A setShowDurations() 0 3 1
A getLevelFormat() 0 2 1
A getTimeFormat() 0 2 1
A setLevelFormat() 0 3 1
A formatString() 0 5 2
A getEol() 0 2 1
A setBufferBegins() 0 3 1
A setLineFormat() 0 3 1
A setEol() 0 7 2
A setColorizeOutput() 0 3 1
B fullMessageStr() 0 38 8

How to fix   Complexity   

Complex Class

Complex classes like StreamLogger 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 StreamLogger, and based on these observations, apply Extract Interface, too.

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 52
    public function __construct($out = 'php://output') {
85 52
        if (is_string($out)) {
86
            try {
87 24
                $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 24
            if (!is_resource($out)) {
92 24
                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 52
        $this->outputHandle = $out;
99 52
        $this->colorizeOutput = Cli::guessFormatOutput($this->outputHandle);
100 52
        $this->setTimeFormat('%F %T');
101 52
        $this->setLevelFormat(function ($l) {
102 43
            return $l;
103 52
        });
104
105 52
    }
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 52
    public function setTimeFormat($format) {
117 52
        if (is_string($format)) {
118 52
            $this->timeFormatter = function ($t) use ($format) {
119 43
                return strftime($format, $t);
120 52
            };
121
        } else {
122 1
            $this->timeFormatter = $format;
123
        }
124
125 52
        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 45
    public function log($level, $message, array $context = array()) {
147 45
        if (!isset($this->wraps[$level])) {
148 1
            throw new InvalidArgumentException("Invalid log level: $level", 400);
149
        }
150
151 44
        $msg = $this->replaceContext($message, $context);
152
153 44
        $eol = true;
154 44
        $fullLine = true;
155 44
        $str = ''; // queue everything in a string to avoid race conditions
156
157 44
        if ($this->bufferBegins()) {
158 43
            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 43
            } 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 39
            } elseif ($this->inBegin) {
172 2
                $str .= $this->eol;
173 2
                $this->inBegin = false;
174
            }
175
        }
176
177 44
        $str .= $this->fullMessageStr($level, $msg, $context, $fullLine);
178
179 44
        if ($eol) {
180 44
            $str .= $this->eol;
181
        }
182
183 44
        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 43
            fwrite($this->outputHandle, $str);
187
        }
188 44
    }
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 44
        $msg = preg_replace_callback('`({[^\s{}]+})`', function ($m) use ($context) {
201 44
            $field = trim($m[1], '{}');
202 44
            if (array_key_exists($field, $context)) {
203 44
                return $context[$field];
204
            } else {
205 2
                return $m[1];
206
            }
207 44
        }, $format);
208 44
        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 44
    public function bufferBegins(): bool {
219 44
        return $this->bufferBegins;
220
    }
221
222 44
    private function fullMessageStr($level, $message, $context, $fullLine = true): string {
223 44
        $levelStr = call_user_func($this->getLevelFormat(), $level);
224
225 44
        $timeStr = call_user_func($this->getTimeFormat(), $context[TaskLogger::FIELD_TIME] ?? microtime(true));
226
227 44
        $indent = $context[TaskLogger::FIELD_INDENT] ?? 0;
228 44
        if ($indent <= 0) {
229 42
            $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 44
        $lines = explode("\n", $message);
236 44
        if ($fullLine) {
237 44
            foreach ($lines as &$line) {
238 44
                $line = rtrim($line);
239 44
                $line = $this->replaceContext($this->getLineFormat(), [
240 44
                    'level' => $levelStr,
241 44
                    'time' => $timeStr,
242 44
                    'message' => $indentStr.$line
243
                ]);
244
            }
245
        }
246 44
        $result = implode($this->getEol(), $lines);
247
248 44
        $wrap = $this->wraps[$level] ?? ['', ''];
249 44
        $result = $this->formatString($result, $wrap);
250
251 44
        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 44
        return $result;
260
    }
261
262
    /**
263
     * Get the level formatting function.
264
     *
265
     * @return callable Returns the levelFormat.
266
     */
267 44
    public function getLevelFormat(): callable {
268 44
        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 52
    public function setLevelFormat(callable $levelFormat) {
278 52
        $this->levelFormat = $levelFormat;
279 52
        return $this;
280
    }
281
282
    /**
283
     * Get the time format function.
284
     *
285
     * @return callable Returns the date format.
286
     * @see strftime()
287
     */
288 44
    public function getTimeFormat(): callable {
289 44
        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 44
    public function getLineFormat(): string {
305 44
        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 44
    public function getEol(): string {
325 44
        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 44
    private function formatString(string $text, array $wrap): string {
351 44
        if ($this->colorizeOutput()) {
352 1
            return "{$wrap[0]}$text{$wrap[1]}";
353
        } else {
354 43
            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 44
    public function colorizeOutput(): bool {
405 44
        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