Completed
Pull Request — master (#24)
by
unknown
06:24 queued 03:07
created

LogFormatter   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Test Coverage

Coverage 87.25%

Importance

Changes 0
Metric Value
dl 0
loc 476
ccs 89
cts 102
cp 0.8725
rs 3.6585
c 0
b 0
f 0
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 13
    /**
58
     * LogFormatter constructor.
59
     */
60 13
    public function __construct() {
61
        $this->formatOutput = Cli::guessFormatOutput();
62
        $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
    }
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
    public function error(string $str) {
75
        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
    public function success(string $str) {
87
        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 6
    /**
103
     * Get the current depth of tasks.
104
     *
105
     * @return int Returns the current level.
106 3
     */
107 6
    protected function currentLevel() {
108
        return count($this->taskStack) + 1;
109 2
    }
110
111
    /**
112
     * Output a message that designates the beginning of a task.
113 6
     *
114
     * @param string $str The message to output.
115
     * @return $this Returns `$this` for fluent calls.
116
     */
117
    public function begin(string $str) {
118 3
        $output = $this->currentLevel() <= $this->getMaxLevel();
119
        $task = [$str, microtime(true), $output];
120
121
        if ($output) {
122
            if (!$this->isNewline) {
123
                $this->write($this->getEol());
124
                $this->isNewline = true;
125
            }
126
127
            $this->write($this->messageStr($str, false));
128 4
            $this->isNewline = false;
129
        }
130
131 2
        array_push($this->taskStack, $task);
132 3
133
        return $this;
134
    }
135
136 3
    /**
137
     * Output a message that designates a task being completed.
138
     *
139 2
     * @param string $str The message to output.
140 3
     * @param bool $force Whether or not to always output the message even if the task is past the max depth.
141 1
     * @param string $logLevel An unused parameter to keep the interface happy.
142
     *
143
     * @return $this Returns `$this` for fluent calls.
144
     */
145 1
    public function end(string $str = '', bool $force = false, $logLevel = LogLevels::INFO) {
146
        // Find the task we are finishing.
147 1
        $task = array_pop($this->taskStack);
148
        if ($task !== null) {
149
            list($taskStr, $taskTimestamp, $taskOutput) = $task;
150 2
            $timespan = microtime(true) - $taskTimestamp;
151 1
        } else {
152
            trigger_error('Called LogFormatter::end() without calling LogFormatter::begin()', E_USER_NOTICE);
153
        }
154
155
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
156
        if ($pastMaxLevel) {
157
            if ($force && isset($taskStr) && isset($taskOutput)) {
158 4
                if (!$taskOutput) {
159
                    // Output the full task string if it hasn't already been output.
160
                    $str = trim($taskStr.' '.$str);
161
                }
162
                if (!$this->isNewline) {
163
                    $this->write($this->getEol());
164 4
                    $this->isNewline = true;
165 4
                }
166
            } else {
167 4
                return $this;
168 3
            }
169
        }
170
171
        if (!empty($timespan) && $this->getShowDurations()) {
172
            $str = trim($str.' '.$this->formatString($this->formatDuration($timespan), ["\033[1;34m", "\033[0m"]));
173
        }
174
175
        if ($this->isNewline) {
176
            // Output the end message as a normal message.
177
            $this->message($str, $force);
178
        } else {
179
            // Output the end message on the same line.
180
            $this->write(' '.$str.$this->getEol());
181
            $this->isNewline = true;
182
        }
183
184
        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
    public function endError(string $str) {
210
        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 1
            $this->endError($statusStr);
229
        } elseif ($httpStatus >= 200 && $httpStatus < 300) {
230
            $this->endSuccess($statusStr, $force);
231 1
        } else {
232
            $this->end($statusStr, $force);
233
        }
234 1
235
        return $this;
236
    }
237 1
238
    /**
239
     * Format a time duration.
240 1
     *
241
     * @param float $duration The duration in seconds and fractions of a second.
242
     * @return string Returns the duration formatted for humans.
243 1
     * @see microtime()
244
     */
245
    public function formatDuration(float $duration) {
246 1
        if ($duration < 1.0e-3) {
247
            $n = number_format($duration * 1.0e6, 0);
248
            $sx = 'μs';
249
        } elseif ($duration < 1) {
250
            $n = number_format($duration * 1000, 0);
251
            $sx = 'ms';
252
        } elseif ($duration < 60) {
253
            $n = number_format($duration, 1);
254
            $sx = 's';
255
        } elseif ($duration < 3600) {
256
            $n = number_format($duration / 60, 1);
257
            $sx = 'm';
258
        } elseif ($duration < 86400) {
259
            $n = number_format($duration / 3600, 1);
260 8
            $sx = 'h';
261
        } else {
262
            $n = number_format($duration / 86400, 1);
263 4
            $sx = 'd';
264 2
        }
265
266 3
        $result = rtrim($n, '0.').$sx;
267 1
        return $result;
268 1
    }
269 1
270
    /**
271 1
     * Output a message.
272
     *
273
     * @param string $str The message to output.
274 1
     * @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 2
     */
277 1
    public function message(string $str, bool $force = false) {
278
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
279
280 5
        if ($pastMaxLevel) {
281 3
            if ($force) {
282
                // Trace down the task list and output everything that hasn't already been output.
283
                foreach ($this->taskStack as $indent => $task) {
284 4
                    list($taskStr, $taskTimestamp, $taskOutput) = $this->taskStack[$indent];
285
                    if (!$taskOutput) {
286 2
                        if (!$this->isNewline) {
287
                            $this->write($this->eol);
288
                            $this->isNewline = true;
289 4
                        }
290 8
                        $this->write($this->fullMessageStr($taskTimestamp, $taskStr, $indent, true));
291
                        $this->taskStack[$indent][2] = true;
292
                    } else {
293
                        continue;
294
                    }
295
                }
296
            } else {
297
                return $this;
298
            }
299
        }
300
301
        if (!$this->isNewline) {
302
            $this->write($this->eol);
303
            $this->isNewline = true;
304
        }
305
        $this->write($this->messageStr($str, true));
306
        return $this;
307 10
    }
308 10
309 10
    /**
310
     * Get whether or not output should be formatted.
311
     *
312 4
     * @return boolean Returns **true** if output should be formatted or **false** otherwise.
313 4
     */
314
    public function getFormatOutput() {
315
        return $this->formatOutput;
316
    }
317 4
318 4
    /**
319 2
     * Set whether or not output should be formatted.
320 3
     *
321
     * @param boolean $formatOutput Whether or not to format output.
322
     * @return $this
323 4
     */
324
    public function setFormatOutput(bool $formatOutput) {
325 4
        $this->formatOutput = $formatOutput;
326
        return $this;
327
    }
328
329
    protected function fullMessageStr($timestamp, $str, $indent = null, $eol = true) {
330
        if ($indent === null) {
331 4
            $indent = $this->currentLevel() - 1;
332 4
        }
333
334 4
        if ($indent <= 0) {
335
            $indentStr = '';
336
        } elseif ($indent === 1) {
337
            $indentStr = '- ';
338
        } else {
339
            $indentStr = str_repeat('  ', $indent - 1).'- ';
340
        }
341
342
        $result = $indentStr.$str;
343
344
        if ($this->getDateFormat()) {
345
            $result = strftime($this->getDateFormat(), $timestamp).' '.$result;
346
        }
347
348
        if ($eol) {
349
            $result .= $this->eol;
350
        }
351
        return $result;
352
    }
353
354
    /**
355 3
     * Create a message string.
356 3
     *
357
     * @param string $str The message to output.
358
     * @param bool $eol Whether or not to add an EOL.
359 3
     * @return string Returns the message.
360
     */
361 3
    protected function messageStr($str, $eol = true) {
362
        return $this->fullMessageStr(time(), $str, null, $eol);
363
    }
364
365
    /**
366
     * Format some text for the console.
367
     *
368 2
     * @param string $text The text to format.
369 2
     * @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
    protected function formatString($text, array $wrap) {
373
        if ($this->formatOutput) {
374
            return "{$wrap[0]}$text{$wrap[1]}";
375
        } else {
376 12
            return $text;
377 12
        }
378
    }
379
380
    /**
381 11
     * Get the maxLevel.
382 11
     *
383
     * @return int Returns the maxLevel.
384
     */
385
    public function getMaxLevel() {
386
        return $this->maxLevel;
387
    }
388
389
    /**
390
     * @param int $maxLevel
391 1
     * @return LogFormatter
392 1
     */
393
    public function setMaxLevel(int $maxLevel) {
394
        if ($maxLevel < 0) {
395
            throw new \InvalidArgumentException("The max level must be greater than zero.", 416);
396
        }
397
398
        $this->maxLevel = $maxLevel;
399
        return $this;
400
    }
401
402 9
    /**
403 9
     * Get the date format as passed to {@link strftime()}.
404 9
     *
405
     * @return string Returns the date format.
406
     * @see strftime()
407
     */
408
    public function getDateFormat() {
409
        return $this->dateFormat;
410
    }
411
412 5
    /**
413 5
     * Set the date format as passed to {@link strftime()}.
414
     *
415
     * @param string $dateFormat
416
     * @return $this
417
     * @see strftime()
418
     */
419
    public function setDateFormat(string $dateFormat) {
420
        $this->dateFormat = $dateFormat;
421
        return $this;
422 11
    }
423 11
424 11
    /**
425
     * Get the end of line string to use.
426
     *
427
     * @return string Returns the eol string.
428
     */
429
    public function getEol() {
430
        return $this->eol;
431
    }
432 5
433 5
    /**
434
     * Set the end of line string.
435
     *
436
     * @param string $eol The end of line string to use.
437
     * @return $this
438
     */
439
    public function setEol($eol) {
440
        $this->eol = $eol;
441
        return $this;
442 11
    }
443 11
444 11
    /**
445
     * Get the showDurations.
446
     *
447
     * @return boolean Returns the showDurations.
448
     */
449
    public function getShowDurations() {
450
        return $this->showDurations;
451
    }
452
453
    /**
454
     * Set the showDurations.
455
     *
456
     * @param boolean $showDurations
457
     * @return $this
458
     */
459
    public function setShowDurations(bool $showDurations) {
460
        $this->showDurations = $showDurations;
461
        return $this;
462
    }
463
464
    /**
465
     * Set the output file handle.
466
     *
467
     * @param resource $handle
468
     * @return $this
469 2
     */
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 2
        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
    public function write($str) {
487
        if (feof($this->outputHandle)) {
488
            trigger_error('Called LogFormatter::write() but file handle was closed.', E_USER_WARNING);
489
            return;
490
        }
491
        fwrite($this->outputHandle, $str);
492
    }
493
}
494