Completed
Push — master ( e9f349...b72251 )
by Todd
03:59 queued 01:58
created

LogFormatter::getMaxLevel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 0
dl 0
loc 2
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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');
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...
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
     * @param string $str The message to output.
93
     * @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
     * @return int Returns the current level.
103
     */
104 11
    protected function currentLevel() {
105 11
        return count($this->taskStack) + 1;
106
    }
107
108
    /**
109
     * Output a message that designates the beginning of a task.
110
     *
111
     * @param string $str The message to output.
112
     * @return $this Returns `$this` for fluent calls.
113
     */
114 8
    public function begin($str) {
115 8
        $output = $this->currentLevel() <= $this->getMaxLevel();
116 8
        $task = [$str, microtime(true), $output];
117
118 8
        if ($output) {
119 8
            if (!$this->isNewline) {
120 2
                $this->write($this->getEol());
121 2
                $this->isNewline = true;
122
            }
123
124 8
            $this->write($this->messageStr($str, false));
125 8
            $this->isNewline = false;
126
        }
127
128 8
        array_push($this->taskStack, $task);
129
130 8
        return $this;
131
    }
132
133
    /**
134
     * Output a message that designates a task being completed.
135
     *
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
     * @return $this Returns `$this` for fluent calls.
139
     */
140 8
    public function end($str = '', $force = false) {
141
        // Find the task we are finishing.
142 8
        $task = array_pop($this->taskStack);
143 8
        if ($task !== null) {
144 8
            list($taskStr, $taskTimestamp, $taskOutput) = $task;
145 8
            $timespan = microtime(true) - $taskTimestamp;
146
        } else {
147 1
            trigger_error('Called LogFormatter::end() without calling LogFormatter::begin()', E_USER_NOTICE);
148
        }
149
150 8
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
151 8
        if ($pastMaxLevel) {
152 3
            if ($force && isset($taskStr) && isset($taskOutput)) {
153 1
                if (!$taskOutput) {
154
                    // Output the full task string if it hasn't already been output.
155 1
                    $str = trim($taskStr.' '.$str);
156
                }
157 1
                if (!$this->isNewline) {
158 1
                    $this->write($this->getEol());
159 1
                    $this->isNewline = true;
160
                }
161
            } else {
162 2
                return $this;
163
            }
164
        }
165
166 8
        if (!empty($timespan) && $this->getShowDurations()) {
167
            $str = trim($str.' '.$this->formatString($this->formatDuration($timespan), ["\033[1;34m", "\033[0m"]));
168
        }
169
170 8
        if ($this->isNewline) {
171
            // Output the end message as a normal message.
172 6
            $this->message($str, $force);
173
        } else {
174
            // Output the end message on the same line.
175 4
            $this->write(' '.$str.$this->getEol());
176 4
            $this->isNewline = true;
177
        }
178
179 8
        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
        return $this->end($this->formatString($str, ["\033[1;32m", "\033[0m"]), $force);
193
    }
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 1
    public function endError($str) {
205 1
        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
        }
229
230
        return $this;
231
    }
232
233
    /**
234
     * Format a time duration.
235
     *
236
     * @param float $duration The duration in seconds and fractions of a second.
237
     * @return string Returns the duration formatted for humans.
238
     * @see microtime()
239
     */
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 1
        } elseif ($duration < 1) {
245 1
            $n = number_format($duration * 1000, 0);
246 1
            $sx = 'ms';
247 1
        } elseif ($duration < 60) {
248 1
            $n = number_format($duration, 1);
249 1
            $sx = 's';
250 1
        } elseif ($duration < 3600) {
251 1
            $n = number_format($duration / 60, 1);
252 1
            $sx = 'm';
253 1
        } elseif ($duration < 86400) {
254 1
            $n = number_format($duration / 3600, 1);
255 1
            $sx = 'h';
256
        } else {
257 1
            $n = number_format($duration / 86400, 1);
258 1
            $sx = 'd';
259
        }
260
261 1
        $result = rtrim($n, '0.').$sx;
262 1
        return $result;
263
    }
264
265
    /**
266
     * Output a message.
267
     *
268
     * @param string $str The message to output.
269
     * @param bool $force Whether or not to force output of the message even if it's past the max depth.
270
     * @return $this Returns `$this` for fluent calls.
271
     */
272 10
    public function message($str, $force = false) {
273 10
        $pastMaxLevel = $this->currentLevel() > $this->getMaxLevel();
274
275 10
        if ($pastMaxLevel) {
276 5
            if ($force) {
277
                // Trace down the task list and output everything that hasn't already been output.
278 3
                foreach ($this->taskStack as $indent => $task) {
279 3
                    list($taskStr, $taskTimestamp, $taskOutput) = $this->taskStack[$indent];
280 3
                    if (!$taskOutput) {
281 1
                        if (!$this->isNewline) {
282 1
                            $this->write($this->eol);
283 1
                            $this->isNewline = true;
284
                        }
285 1
                        $this->write($this->fullMessageStr($taskTimestamp, $taskStr, $indent, true));
286 1
                        $this->taskStack[$indent][2] = true;
287
                    } else {
288 3
                        continue;
289
                    }
290
                }
291
            } else {
292 5
                return $this;
293
            }
294
        }
295
296 9
        if (!$this->isNewline) {
297 2
            $this->write($this->eol);
298 2
            $this->isNewline = true;
299
        }
300 9
        $this->write($this->messageStr($str, true));
301 9
        return $this;
302
    }
303
304
    /**
305
     * Get whether or not output should be formatted.
306
     *
307
     * @return boolean Returns **true** if output should be formatted or **false** otherwise.
308
     */
309
    public function getFormatOutput() {
310
        return $this->formatOutput;
311
    }
312
313
    /**
314
     * Set whether or not output should be formatted.
315
     *
316
     * @param boolean $formatOutput Whether or not to format output.
317
     * @return $this
318
     */
319 11
    public function setFormatOutput($formatOutput) {
320 11
        $this->formatOutput = $formatOutput;
321 11
        return $this;
322
    }
323
324 11
    protected function fullMessageStr($timestamp, $str, $indent = null, $eol = true) {
325 11
        if ($indent === null) {
326 11
            $indent = $this->currentLevel() - 1;
327
        }
328
329 11
        if ($indent <= 0) {
330 11
            $indentStr = '';
331 5
        } elseif ($indent === 1) {
332 5
            $indentStr = '- ';
333
        } else {
334 2
            $indentStr = str_repeat('  ', $indent - 1).'- ';
335
        }
336
337 11
        $result = $indentStr.$str;
338
339 11
        if ($this->getDateFormat()) {
340 9
            $result = strftime($this->getDateFormat(), $timestamp).' '.$result;
341
        }
342
343 11
        if ($eol) {
344 9
            $result .= $this->eol;
345
        }
346 11
        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
     */
356 11
    protected function messageStr($str, $eol = true) {
357 11
        return $this->fullMessageStr(time(), $str, null, $eol);
358
    }
359
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 4
    protected function formatString($text, array $wrap) {
368 4
        if ($this->formatOutput) {
369 1
            return "{$wrap[0]}$text{$wrap[1]}";
370
        } else {
371 3
            return $text;
372
        }
373
    }
374
375
    /**
376
     * Get the maxLevel.
377
     *
378
     * @return int Returns the maxLevel.
379
     */
380 11
    public function getMaxLevel() {
381 11
        return $this->maxLevel;
382
    }
383
384
    /**
385
     * @param int $maxLevel
386
     * @return LogFormatter
387
     */
388 12
    public function setMaxLevel($maxLevel) {
389 12
        if ($maxLevel < 0) {
390 1
            throw new \InvalidArgumentException("The max level must be greater than zero.", 416);
391
        }
392
393 11
        $this->maxLevel = $maxLevel;
394 11
        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
     */
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
     * @see strftime()
413
     */
414 11
    public function setDateFormat($dateFormat) {
415 11
        $this->dateFormat = $dateFormat;
416 11
        return $this;
417
    }
418
419
    /**
420
     * Get the end of line string to use.
421
     *
422
     * @return string Returns the eol string.
423
     */
424 6
    public function getEol() {
425 6
        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
     * @return $this
433
     */
434 11
    public function setEol($eol) {
435 11
        $this->eol = $eol;
436 11
        return $this;
437
    }
438
439
    /**
440
     * Get the showDurations.
441
     *
442
     * @return boolean Returns the showDurations.
443
     */
444 8
    public function getShowDurations() {
445 8
        return $this->showDurations;
446
    }
447
448
    /**
449
     * Set the showDurations.
450
     *
451
     * @param boolean $showDurations
452
     * @return $this
453
     */
454 11
    public function setShowDurations($showDurations) {
455 11
        $this->showDurations = $showDurations;
456 11
        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
        $this->outputHandle = $handle;
470
        return $this;
471
    }
472
473
    /**
474
     * Write a string to the CLI.
475
     *
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 11
    public function write($str) {
482 11
        if (feof($this->outputHandle)) {
483
            trigger_error('Called LogFormatter::write() but file handle was closed.', E_USER_WARNING);
484
            return;
485
        }
486 11
        fwrite($this->outputHandle, $str);
487 11
    }
488
}
489