Completed
Push — master ( b72251...f2a0bc )
by Todd
03:11
created

StreamLogger::getTimeFormat()   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-2018 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Cli;
9
10
use Psr\Log\InvalidArgumentException;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\LoggerTrait;
13
use Psr\Log\LogLevel;
14
15
class StreamLogger implements LoggerInterface {
16
    use LoggerTrait;
17
18
    /**
19
     * @var string The line format.
20
     */
21
    private $lineFormat = '[{time}] {message}';
22
23
    /**
24
     * @var callable The level formatting function.
25
     */
26
    private $levelFormat;
27
28
    /**
29
     * @var callable The time formatting function.
30
     */
31
    private $timeFormatter;
32
33
    /**
34
     * @var string The end of line string to use.
35
     */
36
    private $eol = PHP_EOL;
37
38
    /**
39
     * @var bool Whether or not to format output.
40
     */
41
    private $colorizeOutput;
42
43
    /**
44
     * @var resource The output file handle.
45
     */
46
    private $outputHandle;
47
48
    /**
49
     * @var bool Whether or not the console is on a new line.
50
     */
51
    private $inBegin;
52
53
    /**
54
     * @var bool Whether or not to show durations for tasks.
55
     */
56
    private $showDurations = true;
57
58
    /**
59
     * @var bool Whether or not to buffer begin logs.
60
     */
61
    private $bufferBegins = true;
62
63
    private $wraps = [
64
        LogLevel::DEBUG => ["\033[0;37m", "\033[0m"],
65
        LogLevel::INFO => ['', ''],
66
        LogLevel::NOTICE => ["\033[1m", "\033[0m"],
67
        LogLevel::WARNING => ["\033[1;33m", "\033[0m"],
68
        LogLevel::ERROR => ["\033[1;31m", "\033[0m"],
69
        LogLevel::CRITICAL => ["\033[1;35m", "\033[0m"],
70
        LogLevel::ALERT => ["\033[1;35m", "\033[0m"],
71
        LogLevel::EMERGENCY => ["\033[1;35m", "\033[0m"],
72
    ];
73
74
    /**
75
     * @var resource Whether or not the default stream was opened.
76
     */
77
    private $defaultStream = null;
78
79
    /**
80
     * LogFormatter constructor.
81
     *
82
     * @param mixed $out Either a path or a stream resource for the output.
83
     */
84 51
    public function __construct($out = 'php://output') {
85 51
        if (is_string($out)) {
86
            try {
87 23
                $this->defaultStream = $out = fopen($out, 'a+');
0 ignored issues
show
Documentation Bug introduced by
It seems like $out = fopen($out, 'a+') can also be of type false. However, the property $defaultStream 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...
88 1
            } catch (\Throwable $ex) {
89 1
                throw new \InvalidArgumentException($ex->getMessage(), 500);
90
            }
91 23
            if (!is_resource($out)) {
92 23
                throw new \InvalidArgumentException("The supplied path could not be opened: $out", 500);
93
            }
94 30
        } elseif (!is_resource($out)) {
95 1
            throw new \InvalidArgumentException('The value supplied for $out must be either a stream resource or a path.', 500);
96
        }
97
98 51
        $this->outputHandle = $out;
99 51
        $this->colorizeOutput = Cli::guessFormatOutput($this->outputHandle);
100 51
        $this->setTimeFormat('%F %T');
101 51
        $this->setLevelFormat(function ($l) {
102 42
            return $l;
103 51
        });
104
105 51
    }
106
107
    /**
108
     * Set the time formatter.
109
     *
110
     * This method takes either a format string for **strftime()** or a callable that must format a timestamp.
111
     *
112
     * @param string|callable $format The new format.
113
     * @return $this
114
     * @see strftime()
115
     */
116 51
    public function setTimeFormat($format) {
117 51
        if (is_string($format)) {
118 51
            $this->timeFormatter = function ($t) use ($format) {
119 42
                return strftime($format, $t);
120 51
            };
121
        } else {
122 1
            $this->timeFormatter = $format;
123
        }
124
125 51
        return $this;
126
    }
127
128
    /**
129
     * Clean up the default stream if it was use.
130
     */
131
    public function __destruct() {
132
        if (is_resource($this->defaultStream)) {
133
            fclose($this->defaultStream);
134
        }
135
    }
136
137
    /**
138
     * Logs with an arbitrary level.
139
     *
140
     * @param mixed $level
141
     * @param string $message
142
     * @param array $context
143
     *
144
     * @return void
145
     */
146 44
    public function log($level, $message, array $context = array()) {
147 44
        if (!isset($this->wraps[$level])) {
148 1
            throw new InvalidArgumentException("Invalid log level: $level", 400);
149
        }
150
151 43
        $msg = $this->replaceContext($message, $context);
152
153 43
        $eol = true;
154 43
        $fullLine = true;
155 43
        $str = ''; // queue everything in a string to avoid race conditions
156
157 43
        if ($this->bufferBegins()) {
158 42
            if (!empty($context[TaskLogger::FIELD_BEGIN])) {
159 5
                if ($this->inBegin) {
160 1
                    $str .= $this->eol;
161
                } else {
162 5
                    $this->inBegin = true;
163
                }
164 5
                $eol = false;
165 42
            } elseif (!empty($context[TaskLogger::FIELD_END]) && strpos($msg, "\n") === false) {
166 5
                if ($this->inBegin) {
167 3
                    $msg = ' '.$msg;
168 3
                    $fullLine = false;
169 5
                    $this->inBegin = false;
170
                }
171 39
            } elseif ($this->inBegin) {
172 2
                $str .= $this->eol;
173 2
                $this->inBegin = false;
174
            }
175
        }
176
177 43
        $str .= $this->fullMessageStr($level, $msg, $context, $fullLine);
178
179 43
        if ($eol) {
180 43
            $str .= $this->eol;
181
        }
182
183 43
        if (!is_resource($this->outputHandle) || feof($this->outputHandle)) {
184 1
            trigger_error('The StreamLogger output handle is not valid.', E_USER_WARNING);
185
        } else {
186 42
            fwrite($this->outputHandle, $str);
187
        }
188 43
    }
189
190
    /**
191
     * Replace a message format with context information.
192
     *
193
     * The message format contains fields wrapped in curly braces.
194
     *
195
     * @param string $format The message format to replace.
196
     * @param array $context The context data.
197
     * @return string Returns the formatted message.
198
     */
199
    private function replaceContext(string $format, array $context): string {
200 43
        $msg = preg_replace_callback('`({[^\s{}]+})`', function ($m) use ($context) {
201 43
            $field = trim($m[1], '{}');
202 43
            if (array_key_exists($field, $context)) {
203 43
                return $context[$field];
204
            } else {
205 2
                return $m[1];
206
            }
207 43
        }, $format);
208 43
        return $msg;
209
    }
210
211
    /**
212
     * Whether not to buffer the newline for begins.
213
     *
214
     * When logging a begin this setting will buffer the newline and output the end of the task on the same line if possible.
215
     *
216
     * @return bool Returns the bufferBegins.
217
     */
218 43
    public function bufferBegins(): bool {
219 43
        return $this->bufferBegins;
220
    }
221
222 43
    private function fullMessageStr($level, $message, $context, $fullLine = true): string {
223 43
        $levelStr = call_user_func($this->getLevelFormat(), $level);
224
225 43
        $timeStr = call_user_func($this->getTimeFormat(), $context[TaskLogger::FIELD_TIME] ?? microtime(true));
226
227 43
        $indent = $context[TaskLogger::FIELD_INDENT] ?? 0;
228 43
        if ($indent <= 0) {
229 41
            $indentStr = '';
230
        } else {
231 2
            $indentStr = str_repeat('  ', $indent - 1).'- ';
232
        }
233
234
        // Explode on "\n" because the input string may have a variety of newlines.
235 43
        $lines = explode("\n", $message);
236 43
        $lines = array_map('rtrim', $lines);
237
238 43
        if ($fullLine) {
239 43
            foreach ($lines as &$line) {
240 43
                $line = $this->replaceContext($this->getLineFormat(), [
241 43
                    'level' => $levelStr,
242 43
                    'time' => $timeStr,
243 43
                    'message' => $indentStr.$line
244
                ]);
245
            }
246
        }
247 43
        $result = implode($this->getEol(), $lines);
248
249 43
        $wrap = $this->wraps[$level] ?? ['', ''];
250 43
        $result = $this->formatString($result, $wrap);
251
252 43
        if (isset($context[TaskLogger::FIELD_DURATION]) && $this->showDurations()) {
253 1
            $result = trim($result.' '.$this->formatString($this->formatDuration($context[TaskLogger::FIELD_DURATION]), ["\033[1;34m", "\033[0m"]));
254
        }
255
256 43
        return $result;
257
    }
258
259
    /**
260
     * Get the level formatting function.
261
     *
262
     * @return callable Returns the levelFormat.
263
     */
264 43
    public function getLevelFormat(): callable {
265 43
        return $this->levelFormat;
266
    }
267
268
    /**
269
     * Set the level formatting function.
270
     *
271
     * @param callable $levelFormat The new level format.
272
     * @return $this
273
     */
274 51
    public function setLevelFormat(callable $levelFormat) {
275 51
        $this->levelFormat = $levelFormat;
276 51
        return $this;
277
    }
278
279
    /**
280
     * Get the time format function.
281
     *
282
     * @return callable Returns the date format.
283
     * @see strftime()
284
     */
285 43
    public function getTimeFormat(): callable {
286 43
        return $this->timeFormatter;
287
    }
288
289
    /**
290
     * Get the log line format.
291
     *
292
     * The log line format determines how lines are outputted. The line consists of fields enclosed in curly braces and
293
     * other raw strings. The fields available for the format are the following:
294
     *
295
     * - `{level}`: Output the log level.
296
     * - `{time}`: Output the time of the log.
297
     * - `{message}`: Output the message string.
298
     *
299
     * @return string Returns the lineFormat.
300
     */
301 43
    public function getLineFormat(): string {
302 43
        return $this->lineFormat;
303
    }
304
305
    /**
306
     * Set the log line format.
307
     *
308
     * @param string $lineFormat The new line format.
309
     * @return $this
310
     */
311 29
    public function setLineFormat(string $lineFormat) {
312 29
        $this->lineFormat = $lineFormat;
313 29
        return $this;
314
    }
315
316
    /**
317
     * Get the end of line string to use.
318
     *
319
     * @return string Returns the eol string.
320
     */
321 43
    public function getEol(): string {
322 43
        return $this->eol;
323
    }
324
325
    /**
326
     * Set the end of line string.
327
     *
328
     * @param string $eol The end of line string to use.
329
     * @return $this
330
     */
331 2
    public function setEol(string $eol) {
332 2
        if (strpos($eol, "\n") === false) {
333 1
            throw new \InvalidArgumentException('The EOL must include the "\n" character."', 500);
334
        }
335
336 1
        $this->eol = $eol;
337 1
        return $this;
338
    }
339
340
    /**
341
     * Format some text for the console.
342
     *
343
     * @param string $text The text to format.
344
     * @param string[] $wrap The format to wrap in the form ['before', 'after'].
345
     * @return string Returns the string formatted according to {@link Cli::$format}.
346
     */
347 43
    private function formatString(string $text, array $wrap): string {
348 43
        if ($this->colorizeOutput()) {
349 1
            return "{$wrap[0]}$text{$wrap[1]}";
350
        } else {
351 42
            return $text;
352
        }
353
    }
354
355
    /**
356
     * Get the showDurations.
357
     *
358
     * @return boolean Returns the showDurations.
359
     */
360 1
    public function showDurations(): bool {
361 1
        return $this->showDurations;
362
    }
363
364
    /**
365
     * Format a time duration.
366
     *
367
     * @param float $duration The duration in seconds and fractions of a second.
368
     * @return string Returns the duration formatted for humans.
369
     * @see microtime()
370
     */
371 2
    private function formatDuration(float $duration): string {
372 2
        if ($duration < 1.0e-3) {
373 1
            $n = number_format($duration * 1.0e6, 0);
374 1
            $sx = 'μs';
375 2
        } elseif ($duration < 1) {
376 1
            $n = number_format($duration * 1000, 0);
377 1
            $sx = 'ms';
378 2
        } elseif ($duration < 60) {
379 2
            $n = number_format($duration, 1);
380 2
            $sx = 's';
381 1
        } elseif ($duration < 3600) {
382 1
            $n = number_format($duration / 60, 1);
383 1
            $sx = 'm';
384 1
        } elseif ($duration < 86400) {
385 1
            $n = number_format($duration / 3600, 1);
386 1
            $sx = 'h';
387
        } else {
388 1
            $n = number_format($duration / 86400, 1);
389 1
            $sx = 'd';
390
        }
391
392 2
        $result = rtrim($n, '0.').$sx;
393 2
        return $result;
394
    }
395
396
    /**
397
     * Whether or not to format console output.
398
     *
399
     * @return bool Returns the format output setting.
400
     */
401 43
    public function colorizeOutput(): bool {
402 43
        return $this->colorizeOutput;
403
    }
404
405
    /**
406
     * Set the showDurations.
407
     *
408
     * @param bool $showDurations
409
     * @return $this
410
     */
411 50
    public function setShowDurations(bool $showDurations) {
412 50
        $this->showDurations = $showDurations;
413 50
        return $this;
414
    }
415
416
    /**
417
     * Set whether not to buffer the newline for begins.
418
     *
419
     * @param bool $bufferBegins The new value.
420
     * @return $this
421
     */
422 1
    public function setBufferBegins(bool $bufferBegins) {
423 1
        $this->bufferBegins = $bufferBegins;
424 1
        return $this;
425
    }
426
427
    /**
428
     * Set whether or not to format console output.
429
     *
430
     * @param bool $colorizeOutput The new value.
431
     * @return $this
432
     */
433 1
    public function setColorizeOutput(bool $colorizeOutput) {
434 1
        $this->colorizeOutput = $colorizeOutput;
435 1
        return $this;
436
    }
437
}
438