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
|
52 |
|
public function __construct($out = 'php://output') { |
85
|
52 |
|
if (is_string($out)) { |
86
|
|
|
try { |
87
|
24 |
|
$this->defaultStream = $out = fopen($out, 'a+'); |
|
|
|
|
88
|
1 |
|
} catch (\Throwable $ex) { |
89
|
1 |
|
throw new \InvalidArgumentException($ex->getMessage(), 500); |
90
|
|
|
} |
91
|
24 |
|
if (!is_resource($out)) { |
92
|
24 |
|
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
|
52 |
|
$this->outputHandle = $out; |
99
|
52 |
|
$this->colorizeOutput = Cli::guessFormatOutput($this->outputHandle); |
100
|
52 |
|
$this->setTimeFormat('%F %T'); |
101
|
52 |
|
$this->setLevelFormat(function ($l) { |
102
|
43 |
|
return $l; |
103
|
52 |
|
}); |
104
|
|
|
|
105
|
52 |
|
} |
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
|
52 |
|
public function setTimeFormat($format) { |
117
|
52 |
|
if (is_string($format)) { |
118
|
52 |
|
$this->timeFormatter = function ($t) use ($format) { |
119
|
43 |
|
return strftime($format, $t); |
120
|
52 |
|
}; |
121
|
|
|
} else { |
122
|
1 |
|
$this->timeFormatter = $format; |
123
|
|
|
} |
124
|
|
|
|
125
|
52 |
|
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
|
45 |
|
public function log($level, $message, array $context = array()) { |
147
|
45 |
|
if (!isset($this->wraps[$level])) { |
148
|
1 |
|
throw new InvalidArgumentException("Invalid log level: $level", 400); |
149
|
|
|
} |
150
|
|
|
|
151
|
44 |
|
$msg = $this->replaceContext($message, $context); |
152
|
|
|
|
153
|
44 |
|
$eol = true; |
154
|
44 |
|
$fullLine = true; |
155
|
44 |
|
$str = ''; // queue everything in a string to avoid race conditions |
156
|
|
|
|
157
|
44 |
|
if ($this->bufferBegins()) { |
158
|
43 |
|
if (!empty($context[TaskLogger::FIELD_BEGIN])) { |
159
|
6 |
|
if ($this->inBegin) { |
160
|
1 |
|
$str .= $this->eol; |
161
|
|
|
} else { |
162
|
6 |
|
$this->inBegin = true; |
163
|
|
|
} |
164
|
6 |
|
$eol = false; |
165
|
43 |
|
} elseif (!empty($context[TaskLogger::FIELD_END]) && strpos($msg, "\n") === false) { |
166
|
6 |
|
if ($this->inBegin) { |
167
|
4 |
|
$msg = ' '.$msg; |
168
|
4 |
|
$fullLine = false; |
169
|
6 |
|
$this->inBegin = false; |
170
|
|
|
} |
171
|
39 |
|
} elseif ($this->inBegin) { |
172
|
2 |
|
$str .= $this->eol; |
173
|
2 |
|
$this->inBegin = false; |
174
|
|
|
} |
175
|
|
|
} |
176
|
|
|
|
177
|
44 |
|
$str .= $this->fullMessageStr($level, $msg, $context, $fullLine); |
178
|
|
|
|
179
|
44 |
|
if ($eol) { |
180
|
44 |
|
$str .= $this->eol; |
181
|
|
|
} |
182
|
|
|
|
183
|
44 |
|
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
|
43 |
|
fwrite($this->outputHandle, $str); |
187
|
|
|
} |
188
|
44 |
|
} |
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
|
44 |
|
$msg = preg_replace_callback('`({[^\s{}]+})`', function ($m) use ($context) { |
201
|
44 |
|
$field = trim($m[1], '{}'); |
202
|
44 |
|
if (array_key_exists($field, $context)) { |
203
|
44 |
|
return $context[$field]; |
204
|
|
|
} else { |
205
|
2 |
|
return $m[1]; |
206
|
|
|
} |
207
|
44 |
|
}, $format); |
208
|
44 |
|
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
|
44 |
|
public function bufferBegins(): bool { |
219
|
44 |
|
return $this->bufferBegins; |
220
|
|
|
} |
221
|
|
|
|
222
|
44 |
|
private function fullMessageStr($level, $message, $context, $fullLine = true): string { |
223
|
44 |
|
$levelStr = call_user_func($this->getLevelFormat(), $level); |
224
|
|
|
|
225
|
44 |
|
$timeStr = call_user_func($this->getTimeFormat(), $context[TaskLogger::FIELD_TIME] ?? microtime(true)); |
226
|
|
|
|
227
|
44 |
|
$indent = $context[TaskLogger::FIELD_INDENT] ?? 0; |
228
|
44 |
|
if ($indent <= 0) { |
229
|
42 |
|
$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
|
44 |
|
$lines = explode("\n", $message); |
236
|
44 |
|
if ($fullLine) { |
237
|
44 |
|
foreach ($lines as &$line) { |
238
|
44 |
|
$line = rtrim($line); |
239
|
44 |
|
$line = $this->replaceContext($this->getLineFormat(), [ |
240
|
44 |
|
'level' => $levelStr, |
241
|
44 |
|
'time' => $timeStr, |
242
|
44 |
|
'message' => $indentStr.$line |
243
|
|
|
]); |
244
|
|
|
} |
245
|
|
|
} |
246
|
44 |
|
$result = implode($this->getEol(), $lines); |
247
|
|
|
|
248
|
44 |
|
$wrap = $this->wraps[$level] ?? ['', '']; |
249
|
44 |
|
$result = $this->formatString($result, $wrap); |
250
|
|
|
|
251
|
44 |
|
if (isset($context[TaskLogger::FIELD_DURATION]) && $this->showDurations()) { |
252
|
2 |
|
if ($result && !preg_match('`\s$`', $result)) { |
253
|
1 |
|
$result .= ' '; |
254
|
|
|
} |
255
|
|
|
|
256
|
2 |
|
$result .= $this->formatString($this->formatDuration($context[TaskLogger::FIELD_DURATION]), ["\033[1;34m", "\033[0m"]); |
257
|
|
|
} |
258
|
|
|
|
259
|
44 |
|
return $result; |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Get the level formatting function. |
264
|
|
|
* |
265
|
|
|
* @return callable Returns the levelFormat. |
266
|
|
|
*/ |
267
|
44 |
|
public function getLevelFormat(): callable { |
268
|
44 |
|
return $this->levelFormat; |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Set the level formatting function. |
273
|
|
|
* |
274
|
|
|
* @param callable $levelFormat The new level format. |
275
|
|
|
* @return $this |
276
|
|
|
*/ |
277
|
52 |
|
public function setLevelFormat(callable $levelFormat) { |
278
|
52 |
|
$this->levelFormat = $levelFormat; |
279
|
52 |
|
return $this; |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
/** |
283
|
|
|
* Get the time format function. |
284
|
|
|
* |
285
|
|
|
* @return callable Returns the date format. |
286
|
|
|
* @see strftime() |
287
|
|
|
*/ |
288
|
44 |
|
public function getTimeFormat(): callable { |
289
|
44 |
|
return $this->timeFormatter; |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
/** |
293
|
|
|
* Get the log line format. |
294
|
|
|
* |
295
|
|
|
* The log line format determines how lines are outputted. The line consists of fields enclosed in curly braces and |
296
|
|
|
* other raw strings. The fields available for the format are the following: |
297
|
|
|
* |
298
|
|
|
* - `{level}`: Output the log level. |
299
|
|
|
* - `{time}`: Output the time of the log. |
300
|
|
|
* - `{message}`: Output the message string. |
301
|
|
|
* |
302
|
|
|
* @return string Returns the lineFormat. |
303
|
|
|
*/ |
304
|
44 |
|
public function getLineFormat(): string { |
305
|
44 |
|
return $this->lineFormat; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Set the log line format. |
310
|
|
|
* |
311
|
|
|
* @param string $lineFormat The new line format. |
312
|
|
|
* @return $this |
313
|
|
|
*/ |
314
|
29 |
|
public function setLineFormat(string $lineFormat) { |
315
|
29 |
|
$this->lineFormat = $lineFormat; |
316
|
29 |
|
return $this; |
317
|
|
|
} |
318
|
|
|
|
319
|
|
|
/** |
320
|
|
|
* Get the end of line string to use. |
321
|
|
|
* |
322
|
|
|
* @return string Returns the eol string. |
323
|
|
|
*/ |
324
|
44 |
|
public function getEol(): string { |
325
|
44 |
|
return $this->eol; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
/** |
329
|
|
|
* Set the end of line string. |
330
|
|
|
* |
331
|
|
|
* @param string $eol The end of line string to use. |
332
|
|
|
* @return $this |
333
|
|
|
*/ |
334
|
2 |
|
public function setEol(string $eol) { |
335
|
2 |
|
if (strpos($eol, "\n") === false) { |
336
|
1 |
|
throw new \InvalidArgumentException('The EOL must include the "\n" character."', 500); |
337
|
|
|
} |
338
|
|
|
|
339
|
1 |
|
$this->eol = $eol; |
340
|
1 |
|
return $this; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Format some text for the console. |
345
|
|
|
* |
346
|
|
|
* @param string $text The text to format. |
347
|
|
|
* @param string[] $wrap The format to wrap in the form ['before', 'after']. |
348
|
|
|
* @return string Returns the string formatted according to {@link Cli::$format}. |
349
|
|
|
*/ |
350
|
44 |
|
private function formatString(string $text, array $wrap): string { |
351
|
44 |
|
if ($this->colorizeOutput()) { |
352
|
1 |
|
return "{$wrap[0]}$text{$wrap[1]}"; |
353
|
|
|
} else { |
354
|
43 |
|
return $text; |
355
|
|
|
} |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Get the showDurations. |
360
|
|
|
* |
361
|
|
|
* @return boolean Returns the showDurations. |
362
|
|
|
*/ |
363
|
2 |
|
public function showDurations(): bool { |
364
|
2 |
|
return $this->showDurations; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Format a time duration. |
369
|
|
|
* |
370
|
|
|
* @param float $duration The duration in seconds and fractions of a second. |
371
|
|
|
* @return string Returns the duration formatted for humans. |
372
|
|
|
* @see microtime() |
373
|
|
|
*/ |
374
|
3 |
|
private function formatDuration(float $duration): string { |
375
|
3 |
|
if ($duration < 1.0e-3) { |
376
|
1 |
|
$n = number_format($duration * 1.0e6, 0); |
377
|
1 |
|
$sx = 'μs'; |
378
|
3 |
|
} elseif ($duration < 1) { |
379
|
1 |
|
$n = number_format($duration * 1000, 0); |
380
|
1 |
|
$sx = 'ms'; |
381
|
3 |
|
} elseif ($duration < 60) { |
382
|
3 |
|
$n = number_format($duration, 1); |
383
|
3 |
|
$sx = 's'; |
384
|
1 |
|
} elseif ($duration < 3600) { |
385
|
1 |
|
$n = number_format($duration / 60, 1); |
386
|
1 |
|
$sx = 'm'; |
387
|
1 |
|
} elseif ($duration < 86400) { |
388
|
1 |
|
$n = number_format($duration / 3600, 1); |
389
|
1 |
|
$sx = 'h'; |
390
|
|
|
} else { |
391
|
1 |
|
$n = number_format($duration / 86400, 1); |
392
|
1 |
|
$sx = 'd'; |
393
|
|
|
} |
394
|
|
|
|
395
|
3 |
|
$result = rtrim($n, '0.').$sx; |
396
|
3 |
|
return $result; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* Whether or not to format console output. |
401
|
|
|
* |
402
|
|
|
* @return bool Returns the format output setting. |
403
|
|
|
*/ |
404
|
44 |
|
public function colorizeOutput(): bool { |
405
|
44 |
|
return $this->colorizeOutput; |
406
|
|
|
} |
407
|
|
|
|
408
|
|
|
/** |
409
|
|
|
* Set the showDurations. |
410
|
|
|
* |
411
|
|
|
* @param bool $showDurations |
412
|
|
|
* @return $this |
413
|
|
|
*/ |
414
|
51 |
|
public function setShowDurations(bool $showDurations) { |
415
|
51 |
|
$this->showDurations = $showDurations; |
416
|
51 |
|
return $this; |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Set whether not to buffer the newline for begins. |
421
|
|
|
* |
422
|
|
|
* @param bool $bufferBegins The new value. |
423
|
|
|
* @return $this |
424
|
|
|
*/ |
425
|
1 |
|
public function setBufferBegins(bool $bufferBegins) { |
426
|
1 |
|
$this->bufferBegins = $bufferBegins; |
427
|
1 |
|
return $this; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Set whether or not to format console output. |
432
|
|
|
* |
433
|
|
|
* @param bool $colorizeOutput The new value. |
434
|
|
|
* @return $this |
435
|
|
|
*/ |
436
|
1 |
|
public function setColorizeOutput(bool $colorizeOutput) { |
437
|
1 |
|
$this->colorizeOutput = $colorizeOutput; |
438
|
1 |
|
return $this; |
439
|
|
|
} |
440
|
|
|
} |
441
|
|
|
|
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 theid
property of an instance of theAccount
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.