Completed
Pull Request — master (#20)
by Todd
03:36 queued 01:57
created

LogFormatter   C

Complexity

Total Complexity 63

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 86.84%

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 1
dl 0
loc 476
ccs 132
cts 152
cp 0.8684
rs 5.8893
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A error() 0 3 1
A success() 0 3 1
A currentLevel() 0 3 1
A setFormatOutput() 0 4 1
A messageStr() 0 3 1
A getMaxLevel() 0 3 1
A getDateFormat() 0 3 1
A warn() 0 3 1
A endError() 0 3 1
A getFormatOutput() 0 3 1
A formatString() 0 7 2
A setMaxLevel() 0 8 2
A setDateFormat() 0 4 1
A getEol() 0 3 1
A setEol() 0 4 1
A getShowDurations() 0 3 1
A write() 0 7 2
A begin() 0 18 3
C end() 0 41 11
A endSuccess() 0 3 1
B endHttpStatus() 0 13 5
B formatDuration() 0 24 6
C message() 0 31 7
B fullMessageStr() 0 24 6
A setShowDurations() 0 4 1
A setOutputHandle() 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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-2015 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Cli;
9
10
/**
11
 * A helper class to format CLI output in a log-style format.
12
 */
13
class LogFormatter {
14
    /**
15
     * @var string The date format as passed to {@link strftime()}.
16
     */
17
    protected $dateFormat = '[%F %T]';
18
19
    /**
20
     * @var string The end of line string to use.
21
     */
22
    protected $eol = PHP_EOL;
23
24
    /**
25
     * @var bool Whether or not to format output.
26
     */
27
    protected $formatOutput;
28
29
    /**
30
     * @var resource The output file handle.
31
     */
32
    protected $outputHandle;
33
34
    /**
35
     * @var bool Whether or not the console is on a new line.
36
     */
37
    protected $isNewline = true;
38
39
    /**
40
     * @var int The maximum level deep to output.
41
     */
42
    protected $maxLevel = 2;
43
44
    /**
45
     * @var bool Whether or not to show durations for tasks.
46
     */
47
    protected $showDurations = true;
48
49
    /**
50
     * @var array An array of currently running tasks.
51
     */
52
    protected $taskStack = [];
53
54
    /**
55
     * LogFormatter constructor.
56
     */
57 13
    public function __construct() {
58 13
        $this->formatOutput = Cli::guessFormatOutput();
59 13
        $this->outputHandle = fopen('php://output', 'w');
60 13
    }
61
62
    /**
63
     * Output an error message.
64
     *
65
     * When formatting is turned on, error messages are displayed in red. Error messages are always output, even if they
66
     * are past the maximum display level.
67
     *
68
     * @param string $str The message to output.
69
     * @return $this
70
     */
71 3
    public function error($str) {
72 3
        return $this->message($this->formatString($str, ["\033[1;31m", "\033[0m"]), true);
73
    }
74
75
    /**
76
     * Output a success message.
77
     *
78
     * When formatting is turned on, success messages are displayed in green.
79
     *
80
     * @param string $str The message to output.
81
     * @return $this
82
     */
83 1
    public function success($str) {
84 1
        return $this->message($this->formatString($str, ["\033[1;32m", "\033[0m"]));
85
    }
86
87
    /**
88
     * Output a warning message.
89
     *
90
     * When formatting is turned on, warning messages are displayed in yellow.
91
     *
92 11
     * @param string $str The message to output.
93 11
     * @return LogFormatter Returns `$this` for fluent calls.
94
     */
95
    public function warn($str) {
96
        return $this->message($this->formatString($str, ["\033[1;33m", "\033[0m"]));
97
    }
98
99
    /**
100
     * Get the current depth of tasks.
101
     *
102 8
     * @return int Returns the current level.
103 8
     */
104 8
    protected function currentLevel() {
105
        return count($this->taskStack) + 1;
106 8
    }
107 8
108 2
    /**
109 2
     * Output a message that designates the beginning of a task.
110
     *
111
     * @param string $str The message to output.
112 8
     * @return $this Returns `$this` for fluent calls.
113 8
     */
114
    public function begin($str) {
115
        $output = $this->currentLevel() <= $this->getMaxLevel();
116 8
        $task = [$str, microtime(true), $output];
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 3 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
117
118 8
        if ($output) {
119
            if (!$this->isNewline) {
120
                $this->write($this->getEol());
121
                $this->isNewline = true;
122
            }
123
124
            $this->write($this->messageStr($str, false));
125
            $this->isNewline = false;
126
        }
127
128 8
        array_push($this->taskStack, $task);
129
130 8
        return $this;
131 8
    }
132 8
133 8
    /**
134
     * Output a message that designates a task being completed.
135 1
     *
136
     * @param string $str The message to output.
137
     * @param bool $force Whether or not to always output the message even if the task is past the max depth.
138 8
     * @return $this Returns `$this` for fluent calls.
139 8
     */
140 3
    public function end($str = '', $force = false) {
141 1
        // Find the task we are finishing.
142
        $task = array_pop($this->taskStack);
143 1
        if ($task !== null) {
144
            list($taskStr, $taskTimestamp, $taskOutput) = $task;
145 1
            $timespan = microtime(true) - $taskTimestamp;
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 35 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
146 1
        } else {
147 1
            trigger_error('Called LogFormatter::end() without calling LogFormatter::begin()', E_USER_NOTICE);
148
        }
149
150 2
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
151
        if ($pastMaxLevel) {
152
            if ($force && isset($taskStr) && isset($taskOutput)) {
153
                if (!$taskOutput) {
154 8
                    // Output the full task string if it hasn't already been output.
155
                    $str = trim($taskStr.' '.$str);
156
                }
157
                if (!$this->isNewline) {
158 8
                    $this->write($this->getEol());
159
                    $this->isNewline = true;
160 6
                }
161
            } else {
162
                return $this;
163 4
            }
164 4
        }
165
166
        if (!empty($timespan) && $this->getShowDurations()) {
167 8
            $str = trim($str.' '.$this->formatString($this->formatDuration($timespan), ["\033[1;34m", "\033[0m"]));
168
        }
169
170
        if ($this->isNewline) {
171
            // Output the end message as a normal message.
172
            $this->message($str, $force);
173
        } else {
174
            // Output the end message on the same line.
175
            $this->write(' '.$str.$this->getEol());
176
            $this->isNewline = true;
177
        }
178
179
        return $this;
180
    }
181
182
    /**
183
     * Output a message that represents a task being completed in success.
184
     *
185
     * When formatting is turned on, success messages are output in green.
186
     *
187
     * @param string $str The message to output.
188
     * @param bool $force Whether or not to force a message past the max level to be output.
189
     * @return $this
190
     */
191
    public function endSuccess($str, $force = false) {
192 1
        return $this->end($this->formatString($str, ["\033[1;32m", "\033[0m"]), $force);
193 1
    }
194
195
    /**
196
     * Output a message that represents a task being completed in an error.
197
     *
198
     * When formatting is turned on, error messages are output in red. Error messages are always output even if they are
199
     * past the maximum depth.
200
     *
201
     * @param string $str The message to output.
202
     * @return $this
203
     */
204
    public function endError($str) {
205
        return $this->end($this->formatString($str, ["\033[1;31m", "\033[0m"]), true);
206
    }
207
208
    /**
209
     * Output a message that ends a task with an HTTP status code.
210
     *
211
     * 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
212
     * response code to this message.
213
     *
214
     * @param int $httpStatus The HTTP status code that represents the completion of a task.
215
     * @param bool $force Whether or not to force message output.
216
     * @return $this Returns `$this` for fluent calls.
217
     * @see LogFormatter::endSuccess(), LogFormatter::endError().
218
     */
219
    public function endHttpStatus($httpStatus, $force = false) {
220
        $statusStr = sprintf('%03d', $httpStatus);
221
222
        if ($httpStatus == 0 || $httpStatus >= 400) {
223
            $this->endError($statusStr);
224
        } elseif ($httpStatus >= 200 && $httpStatus < 300) {
225
            $this->endSuccess($statusStr, $force);
226
        } else {
227
            $this->end($statusStr, $force);
228 1
        }
229 1
230 1
        return $this;
231 1
    }
232 1
233 1
    /**
234 1
     * Format a time duration.
235 1
     *
236 1
     * @param float $duration The duration in seconds and fractions of a second.
237 1
     * @return string Returns the duration formatted for humans.
238 1
     * @see microtime()
239 1
     */
240 1
    public function formatDuration($duration) {
241 1
        if ($duration < 1.0e-3) {
242 1
            $n = number_format($duration * 1.0e6, 0);
243 1
            $sx = 'μs';
244
        } elseif ($duration < 1) {
245 1
            $n = number_format($duration * 1000, 0);
246 1
            $sx = 'ms';
247
        } elseif ($duration < 60) {
248
            $n = number_format($duration, 1);
249 1
            $sx = 's';
250 1
        } elseif ($duration < 3600) {
251
            $n = number_format($duration / 60, 1);
252
            $sx = 'm';
253
        } elseif ($duration < 86400) {
254
            $n = number_format($duration / 3600, 1);
255
            $sx = 'h';
256
        } else {
257
            $n = number_format($duration / 86400, 1);
258
            $sx = 'd';
259
        }
260 10
261 10
        $result = rtrim($n, '0.').$sx;
262
        return $result;
263 10
    }
264 5
265
    /**
266 3
     * Output a message.
267 3
     *
268 3
     * @param string $str The message to output.
269 1
     * @param bool $force Whether or not to force output of the message even if it's past the max depth.
270 1
     * @return $this Returns `$this` for fluent calls.
271 1
     */
272
    public function message($str, $force = false) {
273 1
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
274 1
275
        if ($pastMaxLevel) {
276 3
            if ($force) {
277
                // Trace down the task list and output everything that hasn't already been output.
278
                foreach ($this->taskStack as $indent => $task) {
279
                    list($taskStr, $taskTimestamp, $taskOutput) = $this->taskStack[$indent];
280 5
                    if (!$taskOutput) {
281
                        if (!$this->isNewline) {
282
                            $this->write($this->eol);
283
                            $this->isNewline = true;
284 9
                        }
285 2
                        $this->write($this->fullMessageStr($taskTimestamp, $taskStr, $indent, true));
286 2
                        $this->taskStack[$indent][2] = true;
287
                    } else {
288 9
                        continue;
289 9
                    }
290
                }
291
            } else {
292
                return $this;
293
            }
294
        }
295
296
        if (!$this->isNewline) {
297
            $this->write($this->eol);
298
            $this->isNewline = true;
299
        }
300
        $this->write($this->messageStr($str, true));
301
        return $this;
302
    }
303
304
    /**
305
     * Get whether or not output should be formatted.
306
     *
307 11
     * @return boolean Returns **true** if output should be formatted or **false** otherwise.
308 11
     */
309 11
    public function getFormatOutput() {
310
        return $this->formatOutput;
311
    }
312 11
313 11
    /**
314 11
     * Set whether or not output should be formatted.
315
     *
316
     * @param boolean $formatOutput Whether or not to format output.
317 11
     * @return $this
318 11
     */
319 5
    public function setFormatOutput($formatOutput) {
320 5
        $this->formatOutput = $formatOutput;
321
        return $this;
322 2
    }
323
324
    protected function fullMessageStr($timestamp, $str, $indent = null, $eol = true) {
325 11
        if ($indent === null) {
326
            $indent = $this->currentLevel() - 1;
327 11
        }
328 9
329
        if ($indent <= 0) {
330
            $indentStr = '';
331 11
        } elseif ($indent === 1) {
332 9
            $indentStr = '- ';
333
        } else {
334 11
            $indentStr = str_repeat('  ', $indent - 1).'- ';
335
        }
336
337
        $result = $indentStr.$str;
338
339
        if ($this->getDateFormat()) {
340
            $result = strftime($this->getDateFormat(), $timestamp).' '.$result;
341
        }
342
343
        if ($eol) {
344 11
            $result .= $this->eol;
345 11
        }
346
        return $result;
347
    }
348
349
    /**
350
     * Create a message string.
351
     *
352
     * @param string $str The message to output.
353
     * @param bool $eol Whether or not to add an EOL.
354
     * @return string Returns the message.
355 4
     */
356 4
    protected function messageStr($str, $eol = true) {
357 1
        return $this->fullMessageStr(time(), $str, null, $eol);
358
    }
359 3
360
    /**
361
     * Format some text for the console.
362
     *
363
     * @param string $text The text to format.
364
     * @param string[] $wrap The format to wrap in the form ['before', 'after'].
365
     * @return string Returns the string formatted according to {@link Cli::$format}.
366
     */
367
    protected function formatString($text, array $wrap) {
368 11
        if ($this->formatOutput) {
369 11
            return "{$wrap[0]}$text{$wrap[1]}";
370
        } else {
371
            return $text;
372
        }
373
    }
374
375
    /**
376 12
     * Get the maxLevel.
377 12
     *
378 1
     * @return int Returns the maxLevel.
379
     */
380
    public function getMaxLevel() {
381 11
        return $this->maxLevel;
382 11
    }
383
384
    /**
385
     * @param int $maxLevel
386
     * @return LogFormatter
387
     */
388
    public function setMaxLevel($maxLevel) {
389
        if ($maxLevel < 0) {
390
            throw new \InvalidArgumentException("The max level must be greater than zero.", 416);
391 11
        }
392 11
393
        $this->maxLevel = $maxLevel;
394
        return $this;
395
    }
396
397
    /**
398
     * Get the date format as passed to {@link strftime()}.
399
     *
400
     * @return string Returns the date format.
401
     * @see strftime()
402 11
     */
403 11
    public function getDateFormat() {
404 11
        return $this->dateFormat;
405
    }
406
407
    /**
408
     * Set the date format as passed to {@link strftime()}.
409
     *
410
     * @param string $dateFormat
411
     * @return $this
412 6
     * @see strftime()
413 6
     */
414
    public function setDateFormat($dateFormat) {
415
        $this->dateFormat = $dateFormat;
416
        return $this;
417
    }
418
419
    /**
420
     * Get the end of line string to use.
421
     *
422 11
     * @return string Returns the eol string.
423 11
     */
424 11
    public function getEol() {
425
        return $this->eol;
426
    }
427
428
    /**
429
     * Set the end of line string.
430
     *
431
     * @param string $eol The end of line string to use.
432 8
     * @return $this
433 8
     */
434
    public function setEol($eol) {
435
        $this->eol = $eol;
436
        return $this;
437
    }
438
439
    /**
440
     * Get the showDurations.
441
     *
442 11
     * @return boolean Returns the showDurations.
443 11
     */
444 11
    public function getShowDurations() {
445
        return $this->showDurations;
446
    }
447
448
    /**
449
     * Set the showDurations.
450
     *
451
     * @param boolean $showDurations
452
     * @return $this
453
     */
454
    public function setShowDurations($showDurations) {
455
        $this->showDurations = $showDurations;
456
        return $this;
457
    }
458
459
    /**
460
     * Set the output file handle.
461
     *
462
     * @param resource $handle
463
     * @return $this
464
     */
465
    public function setOutputHandle($handle) {
466
        if (feof($handle)) {
467
            throw new \InvalidArgumentException("The provided file handle must be open.", 416);
468
        }
469 11
        $this->outputHandle = $handle;
470 11
        return $this;
471
    }
472
473
    /**
474 11
     * Write a string to the CLI.
475 11
     *
476
     * This method is intended to centralize the echoing of output in case the class is subclassed and the behaviour
477
     * needs to change.
478
     *
479
     * @param string $str The string to write.
480
     */
481
    public function write($str) {
482
        if (feof($this->outputHandle)) {
483
            trigger_error('Called LogFormatter::write() but file handle was closed.', E_USER_WARNING);
484
            return;
485
        }
486
        fwrite($this->outputHandle, $str);
487
    }
488
}
489