Completed
Push — master ( 91d599...412a7f )
by Lincoln
21s
created

LogFormatter::getFormatOutput()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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