Completed
Pull Request — master (#25)
by Todd
04:53 queued 02:33
created

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