Test Failed
Push — feature/logger ( f91b2f...78aed7 )
by
unknown
05:40
created

LogFormatter   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Test Coverage

Coverage 85.62%

Importance

Changes 0
Metric Value
dl 0
loc 476
rs 3.6585
c 0
b 0
f 0
ccs 131
cts 153
cp 0.8562
wmc 63

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getMaxLevel() 0 2 1
A endSuccess() 0 2 1
A getShowDurations() 0 2 1
A setDateFormat() 0 3 1
A warn() 0 2 1
B fullMessageStr() 0 23 6
A __construct() 0 3 1
A setEol() 0 3 1
A getFormatOutput() 0 2 1
A endError() 0 2 1
B formatDuration() 0 23 6
A setOutputHandle() 0 6 2
A messageStr() 0 2 1
A success() 0 2 1
A getEol() 0 2 1
A getDateFormat() 0 2 1
B endHttpStatus() 0 12 5
C message() 0 30 7
A currentLevel() 0 2 1
A setFormatOutput() 0 3 1
A setShowDurations() 0 3 1
A write() 0 6 2
A formatString() 0 5 2
A begin() 0 17 3
A error() 0 2 1
C end() 0 40 11
A setMaxLevel() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like LogFormatter 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 LogFormatter, 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 Garden\Cli\Logger\LoggerInterface;
11
use Garden\Cli\Logger\LogLevels;
12
13
/**
14
 * A helper class to format CLI output in a log-style format.
15
 */
16
class LogFormatter implements LoggerInterface {
17
    /**
18
     * @var string The date format as passed to {@link strftime()}.
19
     */
20
    protected $dateFormat = '[%F %T]';
21
22
    /**
23
     * @var string The end of line string to use.
24
     */
25
    protected $eol = PHP_EOL;
26
27
    /**
28
     * @var bool Whether or not to format output.
29
     */
30
    protected $formatOutput;
31
32
    /**
33
     * @var resource The output file handle.
34
     */
35
    protected $outputHandle;
36
37
    /**
38
     * @var bool Whether or not the console is on a new line.
39
     */
40
    protected $isNewline = true;
41
42
    /**
43
     * @var int The maximum level deep to output.
44
     */
45
    protected $maxLevel = 2;
46
47
    /**
48
     * @var bool Whether or not to show durations for tasks.
49
     */
50
    protected $showDurations = true;
51
52
    /**
53
     * @var array An array of currently running tasks.
54
     */
55
    protected $taskStack = [];
56
57
    /**
58
     * LogFormatter constructor.
59
     */
60 13
    public function __construct() {
61 13
        $this->formatOutput = Cli::guessFormatOutput();
62 13
        $this->outputHandle = fopen('php://output', 'w');
0 ignored issues
show
Documentation Bug introduced by
It seems like fopen('php://output', 'w') can also be of type false. However, the property $outputHandle 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...
63 13
    }
64
65
    /**
66
     * Output an error message.
67
     *
68
     * When formatting is turned on, error messages are displayed in red. Error messages are always output, even if they
69
     * are past the maximum display level.
70
     *
71
     * @param string $str The message to output.
72
     * @return $this
73
     */
74 3
    public function error(string $str) {
75 3
        return $this->message($this->formatString($str, ["\033[1;31m", "\033[0m"]), true);
76
    }
77
78
    /**
79
     * Output a success message.
80
     *
81
     * When formatting is turned on, success messages are displayed in green.
82
     *
83
     * @param string $str The message to output.
84
     * @return $this
85
     */
86 1
    public function success(string $str) {
87 1
        return $this->message($this->formatString($str, ["\033[1;32m", "\033[0m"]));
88
    }
89
90
    /**
91
     * Output a warning message.
92
     *
93
     * When formatting is turned on, warning messages are displayed in yellow.
94
     *
95
     * @param string $str The message to output.
96
     * @return LogFormatter Returns `$this` for fluent calls.
97
     */
98
    public function warn(string $str) {
99
        return $this->message($this->formatString($str, ["\033[1;33m", "\033[0m"]));
100
    }
101
102
    /**
103
     * Get the current depth of tasks.
104
     *
105
     * @return int Returns the current level.
106
     */
107 11
    protected function currentLevel() {
108 11
        return count($this->taskStack) + 1;
109
    }
110
111
    /**
112
     * Output a message that designates the beginning of a task.
113
     *
114
     * @param string $str The message to output.
115
     * @return $this Returns `$this` for fluent calls.
116
     */
117 8
    public function begin(string $str) {
118 8
        $output = $this->currentLevel() <= $this->getMaxLevel();
119 8
        $task = [$str, microtime(true), $output];
120
121 8
        if ($output) {
122 8
            if (!$this->isNewline) {
123 2
                $this->write($this->getEol());
124 2
                $this->isNewline = true;
125
            }
126
127 8
            $this->write($this->messageStr($str, false));
128 8
            $this->isNewline = false;
129
        }
130
131 8
        array_push($this->taskStack, $task);
132
133 8
        return $this;
134
    }
135
136
    /**
137
     * Output a message that designates a task being completed.
138
     *
139
     * @param string $str The message to output.
140
     * @param bool $force Whether or not to always output the message even if the task is past the max depth.
141
     * @param string $logLevel An unused parameter to keep the interface happy.
142
     *
143
     * @return $this Returns `$this` for fluent calls.
144
     */
145 8
    public function end(string $str = '', bool $force = false, $logLevel = LogLevels::INFO) {
146
        // Find the task we are finishing.
147 8
        $task = array_pop($this->taskStack);
148 8
        if ($task !== null) {
149 8
            list($taskStr, $taskTimestamp, $taskOutput) = $task;
150 8
            $timespan = microtime(true) - $taskTimestamp;
151
        } else {
152 1
            trigger_error('Called LogFormatter::end() without calling LogFormatter::begin()', E_USER_NOTICE);
153
        }
154
155 8
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
156 8
        if ($pastMaxLevel) {
157 3
            if ($force && isset($taskStr) && isset($taskOutput)) {
158 1
                if (!$taskOutput) {
159
                    // Output the full task string if it hasn't already been output.
160 1
                    $str = trim($taskStr.' '.$str);
161
                }
162 1
                if (!$this->isNewline) {
163 1
                    $this->write($this->getEol());
164 1
                    $this->isNewline = true;
165
                }
166
            } else {
167 2
                return $this;
168
            }
169
        }
170
171 8
        if (!empty($timespan) && $this->getShowDurations()) {
172
            $str = trim($str.' '.$this->formatString($this->formatDuration($timespan), ["\033[1;34m", "\033[0m"]));
173
        }
174
175 8
        if ($this->isNewline) {
176
            // Output the end message as a normal message.
177 6
            $this->message($str, $force);
178
        } else {
179
            // Output the end message on the same line.
180 4
            $this->write(' '.$str.$this->getEol());
181 4
            $this->isNewline = true;
182
        }
183
184 8
        return $this;
185
    }
186
187
    /**
188
     * Output a message that represents a task being completed in success.
189
     *
190
     * When formatting is turned on, success messages are output in green.
191
     *
192
     * @param string $str The message to output.
193
     * @param bool $force Whether or not to force a message past the max level to be output.
194
     * @return $this
195
     */
196
    public function endSuccess(string $str, bool $force = false) {
197
        return $this->end($this->formatString($str, ["\033[1;32m", "\033[0m"]), $force);
198
    }
199
200
    /**
201
     * Output a message that represents a task being completed in an error.
202
     *
203
     * When formatting is turned on, error messages are output in red. Error messages are always output even if they are
204
     * past the maximum depth.
205
     *
206
     * @param string $str The message to output.
207
     * @return $this
208
     */
209 1
    public function endError(string $str) {
210 1
        return $this->end($this->formatString($str, ["\033[1;31m", "\033[0m"]), true);
211
    }
212
213
    /**
214
     * Output a message that ends a task with an HTTP status code.
215
     *
216
     * This method is useful if you are making a call to an external API as a task. You can end the task by passing the
217
     * response code to this message.
218
     *
219
     * @param int $httpStatus The HTTP status code that represents the completion of a task.
220
     * @param bool $force Whether or not to force message output.
221
     * @return $this Returns `$this` for fluent calls.
222
     * @see LogFormatter::endSuccess(), LogFormatter::endError().
223
     */
224
    public function endHttpStatus(int $httpStatus, bool $force = false) {
225
        $statusStr = sprintf('%03d', $httpStatus);
226
227
        if ($httpStatus == 0 || $httpStatus >= 400) {
228
            $this->endError($statusStr);
229
        } elseif ($httpStatus >= 200 && $httpStatus < 300) {
230
            $this->endSuccess($statusStr, $force);
231
        } else {
232
            $this->end($statusStr, $force);
233
        }
234
235
        return $this;
236
    }
237
238
    /**
239
     * Format a time duration.
240
     *
241
     * @param float $duration The duration in seconds and fractions of a second.
242
     * @return string Returns the duration formatted for humans.
243
     * @see microtime()
244
     */
245 1
    public function formatDuration(float $duration) {
246 1
        if ($duration < 1.0e-3) {
247 1
            $n = number_format($duration * 1.0e6, 0);
248 1
            $sx = 'μs';
249 1
        } elseif ($duration < 1) {
250 1
            $n = number_format($duration * 1000, 0);
251 1
            $sx = 'ms';
252 1
        } elseif ($duration < 60) {
253 1
            $n = number_format($duration, 1);
254 1
            $sx = 's';
255 1
        } elseif ($duration < 3600) {
256 1
            $n = number_format($duration / 60, 1);
257 1
            $sx = 'm';
258 1
        } elseif ($duration < 86400) {
259 1
            $n = number_format($duration / 3600, 1);
260 1
            $sx = 'h';
261
        } else {
262 1
            $n = number_format($duration / 86400, 1);
263 1
            $sx = 'd';
264
        }
265
266 1
        $result = rtrim($n, '0.').$sx;
267 1
        return $result;
268
    }
269
270
    /**
271
     * Output a message.
272
     *
273
     * @param string $str The message to output.
274
     * @param bool $force Whether or not to force output of the message even if it's past the max depth.
275
     * @return $this Returns `$this` for fluent calls.
276
     */
277 10
    public function message(string $str, bool $force = false) {
278 10
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
279
280 10
        if ($pastMaxLevel) {
281 5
            if ($force) {
282
                // Trace down the task list and output everything that hasn't already been output.
283 3
                foreach ($this->taskStack as $indent => $task) {
284 3
                    list($taskStr, $taskTimestamp, $taskOutput) = $this->taskStack[$indent];
285 3
                    if (!$taskOutput) {
286 1
                        if (!$this->isNewline) {
287 1
                            $this->write($this->eol);
288 1
                            $this->isNewline = true;
289
                        }
290 1
                        $this->write($this->fullMessageStr($taskTimestamp, $taskStr, $indent, true));
291 1
                        $this->taskStack[$indent][2] = true;
292
                    } else {
293 3
                        continue;
294
                    }
295
                }
296
            } else {
297 5
                return $this;
298
            }
299
        }
300
301 9
        if (!$this->isNewline) {
302 2
            $this->write($this->eol);
303 2
            $this->isNewline = true;
304
        }
305 9
        $this->write($this->messageStr($str, true));
306 9
        return $this;
307
    }
308
309
    /**
310
     * Get whether or not output should be formatted.
311
     *
312
     * @return boolean Returns **true** if output should be formatted or **false** otherwise.
313
     */
314
    public function getFormatOutput() {
315
        return $this->formatOutput;
316
    }
317
318
    /**
319
     * Set whether or not output should be formatted.
320
     *
321
     * @param boolean $formatOutput Whether or not to format output.
322
     * @return $this
323
     */
324 11
    public function setFormatOutput(bool $formatOutput) {
325 11
        $this->formatOutput = $formatOutput;
326 11
        return $this;
327
    }
328
329 11
    protected function fullMessageStr($timestamp, $str, $indent = null, $eol = true) {
330 11
        if ($indent === null) {
331 11
            $indent = $this->currentLevel() - 1;
332
        }
333
334 11
        if ($indent <= 0) {
335 11
            $indentStr = '';
336 5
        } elseif ($indent === 1) {
337 5
            $indentStr = '- ';
338
        } else {
339 2
            $indentStr = str_repeat('  ', $indent - 1).'- ';
340
        }
341
342 11
        $result = $indentStr.$str;
343
344 11
        if ($this->getDateFormat()) {
345 9
            $result = strftime($this->getDateFormat(), $timestamp).' '.$result;
346
        }
347
348 11
        if ($eol) {
349 9
            $result .= $this->eol;
350
        }
351 11
        return $result;
352
    }
353
354
    /**
355
     * Create a message string.
356
     *
357
     * @param string $str The message to output.
358
     * @param bool $eol Whether or not to add an EOL.
359
     * @return string Returns the message.
360
     */
361 11
    protected function messageStr($str, $eol = true) {
362 11
        return $this->fullMessageStr(time(), $str, null, $eol);
363
    }
364
365
    /**
366
     * Format some text for the console.
367
     *
368
     * @param string $text The text to format.
369
     * @param string[] $wrap The format to wrap in the form ['before', 'after'].
370
     * @return string Returns the string formatted according to {@link Cli::$format}.
371
     */
372 4
    protected function formatString($text, array $wrap) {
373 4
        if ($this->formatOutput) {
374 1
            return "{$wrap[0]}$text{$wrap[1]}";
375
        } else {
376 3
            return $text;
377
        }
378
    }
379
380
    /**
381
     * Get the maxLevel.
382
     *
383
     * @return int Returns the maxLevel.
384
     */
385 11
    public function getMaxLevel() {
386 11
        return $this->maxLevel;
387
    }
388
389
    /**
390
     * @param int $maxLevel
391
     * @return LogFormatter
392
     */
393 12
    public function setMaxLevel(int $maxLevel) {
394 12
        if ($maxLevel < 0) {
395 1
            throw new \InvalidArgumentException("The max level must be greater than zero.", 416);
396
        }
397
398 11
        $this->maxLevel = $maxLevel;
399 11
        return $this;
400
    }
401
402
    /**
403
     * Get the date format as passed to {@link strftime()}.
404
     *
405
     * @return string Returns the date format.
406
     * @see strftime()
407
     */
408 11
    public function getDateFormat() {
409 11
        return $this->dateFormat;
410
    }
411
412
    /**
413
     * Set the date format as passed to {@link strftime()}.
414
     *
415
     * @param string $dateFormat
416
     * @return $this
417
     * @see strftime()
418
     */
419 11
    public function setDateFormat(string $dateFormat) {
420 11
        $this->dateFormat = $dateFormat;
421 11
        return $this;
422
    }
423
424
    /**
425
     * Get the end of line string to use.
426
     *
427
     * @return string Returns the eol string.
428
     */
429 6
    public function getEol() {
430 6
        return $this->eol;
431
    }
432
433
    /**
434
     * Set the end of line string.
435
     *
436
     * @param string $eol The end of line string to use.
437
     * @return $this
438
     */
439 11
    public function setEol($eol) {
440 11
        $this->eol = $eol;
441 11
        return $this;
442
    }
443
444
    /**
445
     * Get the showDurations.
446
     *
447
     * @return boolean Returns the showDurations.
448
     */
449 8
    public function getShowDurations() {
450 8
        return $this->showDurations;
451
    }
452
453
    /**
454
     * Set the showDurations.
455
     *
456
     * @param boolean $showDurations
457
     * @return $this
458
     */
459 11
    public function setShowDurations(bool $showDurations) {
460 11
        $this->showDurations = $showDurations;
461 11
        return $this;
462
    }
463
464
    /**
465
     * Set the output file handle.
466
     *
467
     * @param resource $handle
468
     * @return $this
469
     */
470
    public function setOutputHandle($handle) {
471
        if (feof($handle)) {
472
            throw new \InvalidArgumentException("The provided file handle must be open.", 416);
473
        }
474
        $this->outputHandle = $handle;
475
        return $this;
476
    }
477
478
    /**
479
     * Write a string to the CLI.
480
     *
481
     * This method is intended to centralize the echoing of output in case the class is subclassed and the behaviour
482
     * needs to change.
483
     *
484
     * @param string $str The string to write.
485
     */
486 11
    public function write($str) {
487 11
        if (feof($this->outputHandle)) {
488
            trigger_error('Called LogFormatter::write() but file handle was closed.', E_USER_WARNING);
489
            return;
490
        }
491 11
        fwrite($this->outputHandle, $str);
492 11
    }
493
}
494