1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Ahc\Cli\Output; |
4
|
|
|
|
5
|
|
|
use Ahc\Cli\Exception\InvalidArgumentException; |
6
|
|
|
use Ahc\Cli\Helper\InflectsString; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Cli Writer. |
10
|
|
|
* |
11
|
|
|
* @author Jitendra Adhikari <[email protected]> |
12
|
|
|
* @license MIT |
13
|
|
|
* |
14
|
|
|
* @link https://github.com/adhocore/cli |
15
|
|
|
* |
16
|
|
|
* @method Writer bgBlack($text, $eol = false) |
17
|
|
|
* @method Writer bgBlue($text, $eol = false) |
18
|
|
|
* @method Writer bgCyan($text, $eol = false) |
19
|
|
|
* @method Writer bgGreen($text, $eol = false) |
20
|
|
|
* @method Writer bgPurple($text, $eol = false) |
21
|
|
|
* @method Writer bgRed($text, $eol = false) |
22
|
|
|
* @method Writer bgWhite($text, $eol = false) |
23
|
|
|
* @method Writer bgYellow($text, $eol = false) |
24
|
|
|
* @method Writer black($text, $eol = false) |
25
|
|
|
* @method Writer blackBgBlue($text, $eol = false) |
26
|
|
|
* @method Writer blackBgCyan($text, $eol = false) |
27
|
|
|
* @method Writer blackBgGreen($text, $eol = false) |
28
|
|
|
* @method Writer blackBgPurple($text, $eol = false) |
29
|
|
|
* @method Writer blackBgRed($text, $eol = false) |
30
|
|
|
* @method Writer blackBgWhite($text, $eol = false) |
31
|
|
|
* @method Writer blackBgYellow($text, $eol = false) |
32
|
|
|
* @method Writer blue($text, $eol = false) |
33
|
|
|
* @method Writer blueBgBlack($text, $eol = false) |
34
|
|
|
* @method Writer blueBgCyan($text, $eol = false) |
35
|
|
|
* @method Writer blueBgGreen($text, $eol = false) |
36
|
|
|
* @method Writer blueBgPurple($text, $eol = false) |
37
|
|
|
* @method Writer blueBgRed($text, $eol = false) |
38
|
|
|
* @method Writer blueBgWhite($text, $eol = false) |
39
|
|
|
* @method Writer blueBgYellow($text, $eol = false) |
40
|
|
|
* @method Writer bold($text, $eol = false) |
41
|
|
|
* @method Writer boldBlack($text, $eol = false) |
42
|
|
|
* @method Writer boldBlackBgBlue($text, $eol = false) |
43
|
|
|
* @method Writer boldBlackBgCyan($text, $eol = false) |
44
|
|
|
* @method Writer boldBlackBgGreen($text, $eol = false) |
45
|
|
|
* @method Writer boldBlackBgPurple($text, $eol = false) |
46
|
|
|
* @method Writer boldBlackBgRed($text, $eol = false) |
47
|
|
|
* @method Writer boldBlackBgWhite($text, $eol = false) |
48
|
|
|
* @method Writer boldBlackBgYellow($text, $eol = false) |
49
|
|
|
* @method Writer boldBlue($text, $eol = false) |
50
|
|
|
* @method Writer boldBlueBgBlack($text, $eol = false) |
51
|
|
|
* @method Writer boldBlueBgCyan($text, $eol = false) |
52
|
|
|
* @method Writer boldBlueBgGreen($text, $eol = false) |
53
|
|
|
* @method Writer boldBlueBgPurple($text, $eol = false) |
54
|
|
|
* @method Writer boldBlueBgRed($text, $eol = false) |
55
|
|
|
* @method Writer boldBlueBgWhite($text, $eol = false) |
56
|
|
|
* @method Writer boldBlueBgYellow($text, $eol = false) |
57
|
|
|
* @method Writer boldCyan($text, $eol = false) |
58
|
|
|
* @method Writer boldCyanBgBlack($text, $eol = false) |
59
|
|
|
* @method Writer boldCyanBgBlue($text, $eol = false) |
60
|
|
|
* @method Writer boldCyanBgGreen($text, $eol = false) |
61
|
|
|
* @method Writer boldCyanBgPurple($text, $eol = false) |
62
|
|
|
* @method Writer boldCyanBgRed($text, $eol = false) |
63
|
|
|
* @method Writer boldCyanBgWhite($text, $eol = false) |
64
|
|
|
* @method Writer boldCyanBgYellow($text, $eol = false) |
65
|
|
|
* @method Writer boldGreen($text, $eol = false) |
66
|
|
|
* @method Writer boldGreenBgBlack($text, $eol = false) |
67
|
|
|
* @method Writer boldGreenBgBlue($text, $eol = false) |
68
|
|
|
* @method Writer boldGreenBgCyan($text, $eol = false) |
69
|
|
|
* @method Writer boldGreenBgPurple($text, $eol = false) |
70
|
|
|
* @method Writer boldGreenBgRed($text, $eol = false) |
71
|
|
|
* @method Writer boldGreenBgWhite($text, $eol = false) |
72
|
|
|
* @method Writer boldGreenBgYellow($text, $eol = false) |
73
|
|
|
* @method Writer boldPurple($text, $eol = false) |
74
|
|
|
* @method Writer boldPurpleBgBlack($text, $eol = false) |
75
|
|
|
* @method Writer boldPurpleBgBlue($text, $eol = false) |
76
|
|
|
* @method Writer boldPurpleBgCyan($text, $eol = false) |
77
|
|
|
* @method Writer boldPurpleBgGreen($text, $eol = false) |
78
|
|
|
* @method Writer boldPurpleBgRed($text, $eol = false) |
79
|
|
|
* @method Writer boldPurpleBgWhite($text, $eol = false) |
80
|
|
|
* @method Writer boldPurpleBgYellow($text, $eol = false) |
81
|
|
|
* @method Writer boldRed($text, $eol = false) |
82
|
|
|
* @method Writer boldRedBgBlack($text, $eol = false) |
83
|
|
|
* @method Writer boldRedBgBlue($text, $eol = false) |
84
|
|
|
* @method Writer boldRedBgCyan($text, $eol = false) |
85
|
|
|
* @method Writer boldRedBgGreen($text, $eol = false) |
86
|
|
|
* @method Writer boldRedBgPurple($text, $eol = false) |
87
|
|
|
* @method Writer boldRedBgWhite($text, $eol = false) |
88
|
|
|
* @method Writer boldRedBgYellow($text, $eol = false) |
89
|
|
|
* @method Writer boldWhite($text, $eol = false) |
90
|
|
|
* @method Writer boldWhiteBgBlack($text, $eol = false) |
91
|
|
|
* @method Writer boldWhiteBgBlue($text, $eol = false) |
92
|
|
|
* @method Writer boldWhiteBgCyan($text, $eol = false) |
93
|
|
|
* @method Writer boldWhiteBgGreen($text, $eol = false) |
94
|
|
|
* @method Writer boldWhiteBgPurple($text, $eol = false) |
95
|
|
|
* @method Writer boldWhiteBgRed($text, $eol = false) |
96
|
|
|
* @method Writer boldWhiteBgYellow($text, $eol = false) |
97
|
|
|
* @method Writer boldYellow($text, $eol = false) |
98
|
|
|
* @method Writer boldYellowBgBlack($text, $eol = false) |
99
|
|
|
* @method Writer boldYellowBgBlue($text, $eol = false) |
100
|
|
|
* @method Writer boldYellowBgCyan($text, $eol = false) |
101
|
|
|
* @method Writer boldYellowBgGreen($text, $eol = false) |
102
|
|
|
* @method Writer boldYellowBgPurple($text, $eol = false) |
103
|
|
|
* @method Writer boldYellowBgRed($text, $eol = false) |
104
|
|
|
* @method Writer boldYellowBgWhite($text, $eol = false) |
105
|
|
|
* @method Writer colors($text) |
106
|
|
|
* @method Writer comment($text, $eol = false) |
107
|
|
|
* @method Writer cyan($text, $eol = false) |
108
|
|
|
* @method Writer cyanBgBlack($text, $eol = false) |
109
|
|
|
* @method Writer cyanBgBlue($text, $eol = false) |
110
|
|
|
* @method Writer cyanBgGreen($text, $eol = false) |
111
|
|
|
* @method Writer cyanBgPurple($text, $eol = false) |
112
|
|
|
* @method Writer cyanBgRed($text, $eol = false) |
113
|
|
|
* @method Writer cyanBgWhite($text, $eol = false) |
114
|
|
|
* @method Writer cyanBgYellow($text, $eol = false) |
115
|
|
|
* @method Writer error($text, $eol = false) |
116
|
|
|
* @method Writer green($text, $eol = false) |
117
|
|
|
* @method Writer greenBgBlack($text, $eol = false) |
118
|
|
|
* @method Writer greenBgBlue($text, $eol = false) |
119
|
|
|
* @method Writer greenBgCyan($text, $eol = false) |
120
|
|
|
* @method Writer greenBgPurple($text, $eol = false) |
121
|
|
|
* @method Writer greenBgRed($text, $eol = false) |
122
|
|
|
* @method Writer greenBgWhite($text, $eol = false) |
123
|
|
|
* @method Writer greenBgYellow($text, $eol = false) |
124
|
|
|
* @method Writer info($text, $eol = false) |
125
|
|
|
* @method Writer ok($text, $eol = false) |
126
|
|
|
* @method Writer purple($text, $eol = false) |
127
|
|
|
* @method Writer purpleBgBlack($text, $eol = false) |
128
|
|
|
* @method Writer purpleBgBlue($text, $eol = false) |
129
|
|
|
* @method Writer purpleBgCyan($text, $eol = false) |
130
|
|
|
* @method Writer purpleBgGreen($text, $eol = false) |
131
|
|
|
* @method Writer purpleBgRed($text, $eol = false) |
132
|
|
|
* @method Writer purpleBgWhite($text, $eol = false) |
133
|
|
|
* @method Writer purpleBgYellow($text, $eol = false) |
134
|
|
|
* @method Writer red($text, $eol = false) |
135
|
|
|
* @method Writer redBgBlack($text, $eol = false) |
136
|
|
|
* @method Writer redBgBlue($text, $eol = false) |
137
|
|
|
* @method Writer redBgCyan($text, $eol = false) |
138
|
|
|
* @method Writer redBgGreen($text, $eol = false) |
139
|
|
|
* @method Writer redBgPurple($text, $eol = false) |
140
|
|
|
* @method Writer redBgWhite($text, $eol = false) |
141
|
|
|
* @method Writer redBgYellow($text, $eol = false) |
142
|
|
|
* @method Writer warn($text, $eol = false) |
143
|
|
|
* @method Writer white($text, $eol = false) |
144
|
|
|
* @method Writer yellow($text, $eol = false) |
145
|
|
|
* @method Writer yellowBgBlack($text, $eol = false) |
146
|
|
|
* @method Writer yellowBgBlue($text, $eol = false) |
147
|
|
|
* @method Writer yellowBgCyan($text, $eol = false) |
148
|
|
|
* @method Writer yellowBgGreen($text, $eol = false) |
149
|
|
|
* @method Writer yellowBgPurple($text, $eol = false) |
150
|
|
|
* @method Writer yellowBgRed($text, $eol = false) |
151
|
|
|
* @method Writer yellowBgWhite($text, $eol = false) |
152
|
|
|
*/ |
153
|
|
|
class Writer |
154
|
|
|
{ |
155
|
|
|
use InflectsString; |
156
|
|
|
|
157
|
|
|
/** @var resource Output file handle */ |
158
|
|
|
protected $stream; |
159
|
|
|
|
160
|
|
|
/** @var resource Error output file handle */ |
161
|
|
|
protected $eStream; |
162
|
|
|
|
163
|
|
|
/** @var string Write method to be relayed to Colorizer */ |
164
|
|
|
protected $method; |
165
|
|
|
|
166
|
|
|
/** @var Color */ |
167
|
|
|
protected $colorizer; |
168
|
|
|
|
169
|
|
|
/** @var Cursor */ |
170
|
|
|
protected $cursor; |
171
|
|
|
|
172
|
|
|
public function __construct(string $path = null, Color $colorizer = null) |
173
|
|
|
{ |
174
|
|
|
if ($path) { |
175
|
|
|
$path = \fopen($path, 'w'); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
$this->stream = $path ?: \STDOUT; |
|
|
|
|
179
|
|
|
$this->eStream = $path ?: \STDERR; |
|
|
|
|
180
|
|
|
|
181
|
|
|
$this->cursor = new Cursor; |
182
|
|
|
$this->colorizer = $colorizer ?? new Color; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Get Colorizer. |
187
|
|
|
* |
188
|
|
|
* @return Color |
189
|
|
|
*/ |
190
|
|
|
public function colorizer(): Color |
191
|
|
|
{ |
192
|
|
|
return $this->colorizer; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* Magically set methods. |
197
|
|
|
* |
198
|
|
|
* @param string $name Like `red`, `bgRed`, 'bold', `error` etc |
199
|
|
|
* |
200
|
|
|
* @return self |
201
|
|
|
*/ |
202
|
|
|
public function __get(string $name): self |
203
|
|
|
{ |
204
|
|
|
if (\strpos($this->method, $name) === false) { |
205
|
|
|
$this->method .= $this->method ? \ucfirst($name) : $name; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
return $this; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Write the formatted text to stdout or stderr. |
213
|
|
|
* |
214
|
|
|
* @param string $text |
215
|
|
|
* @param bool $eol |
216
|
|
|
* |
217
|
|
|
* @return self |
218
|
|
|
*/ |
219
|
|
|
public function write(string $text, bool $eol = false): self |
220
|
|
|
{ |
221
|
|
|
list($method, $this->method) = [$this->method ?: 'line', '']; |
222
|
|
|
|
223
|
|
|
$text = $this->colorizer->{$method}($text, []); |
224
|
|
|
$error = \stripos($method, 'error') !== false; |
225
|
|
|
|
226
|
|
|
if ($eol) { |
227
|
|
|
$text .= \PHP_EOL; |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
return $this->doWrite($text, $error); |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
/** |
234
|
|
|
* Really write to the stream. |
235
|
|
|
* |
236
|
|
|
* @param string $text |
237
|
|
|
* @param bool $error |
238
|
|
|
* |
239
|
|
|
* @return self |
240
|
|
|
*/ |
241
|
|
|
protected function doWrite(string $text, bool $error = false): self |
242
|
|
|
{ |
243
|
|
|
$stream = $error ? $this->eStream : $this->stream; |
244
|
|
|
|
245
|
|
|
\fwrite($stream, $text); |
246
|
|
|
|
247
|
|
|
return $this; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* Write EOL n times. |
252
|
|
|
* |
253
|
|
|
* @param int $n |
254
|
|
|
* |
255
|
|
|
* @return self |
256
|
|
|
*/ |
257
|
|
|
public function eol(int $n = 1): self |
258
|
|
|
{ |
259
|
|
|
return $this->doWrite(\str_repeat(PHP_EOL, \max($n, 1))); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* Write raw text (as it is). |
264
|
|
|
* |
265
|
|
|
* @param string $text |
266
|
|
|
* @param bool $error |
267
|
|
|
* |
268
|
|
|
* @return self |
269
|
|
|
*/ |
270
|
|
|
public function raw($text, bool $error = false): self |
271
|
|
|
{ |
272
|
|
|
return $this->doWrite((string) $text, $error); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Generate table for the console. Keys of first row are taken as header. |
277
|
|
|
* |
278
|
|
|
* @param array[] $rows Array of assoc arrays. |
279
|
|
|
* @param array $styles Eg: ['head' => 'bold', 'odd' => 'comment', 'even' => 'green'] |
280
|
|
|
* |
281
|
|
|
* @return self |
282
|
|
|
*/ |
283
|
|
|
public function table(array $rows, array $styles = []): self |
284
|
|
|
{ |
285
|
|
|
if ([] === $table = $this->normalizeTable($rows)) { |
286
|
|
|
return $this; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
list($head, $rows) = $table; |
290
|
|
|
|
291
|
|
|
$styles = $this->normalizeStyles($styles); |
292
|
|
|
$title = $body = $dash = []; |
293
|
|
|
|
294
|
|
|
list($start, $end) = $styles['head']; |
295
|
|
|
foreach ($head as $col => $size) { |
296
|
|
|
$dash[] = \str_repeat('-', $size + 2); |
297
|
|
|
$title[] = \str_pad($this->toWords($col), $size, ' '); |
298
|
|
|
} |
299
|
|
|
|
300
|
|
|
$title = "|$start " . \implode(" $end|$start ", $title) . " $end|" . \PHP_EOL; |
301
|
|
|
|
302
|
|
|
$odd = true; |
303
|
|
|
foreach ($rows as $row) { |
304
|
|
|
$parts = []; |
305
|
|
|
|
306
|
|
|
list($start, $end) = $styles[['even', 'odd'][(int) $odd]]; |
307
|
|
|
foreach ($head as $col => $size) { |
308
|
|
|
$parts[] = \str_pad($row[$col] ?? '', $size, ' '); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
$odd = !$odd; |
|
|
|
|
312
|
|
|
$body[] = "|$start " . \implode(" $end|$start ", $parts) . " $end|"; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
$dash = '+' . \implode('+', $dash) . '+' . \PHP_EOL; |
316
|
|
|
$body = \implode(\PHP_EOL, $body) . \PHP_EOL; |
317
|
|
|
|
318
|
|
|
return $this->colors("$dash$title$dash$body$dash"); |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
protected function normalizeTable(array $rows): array |
322
|
|
|
{ |
323
|
|
|
$head = \reset($rows); |
324
|
|
|
if (empty($head)) { |
325
|
|
|
return []; |
326
|
|
|
} |
327
|
|
|
|
328
|
|
|
if (!\is_array($head)) { |
329
|
|
|
throw new InvalidArgumentException( |
330
|
|
|
\sprintf('Rows must be array of assoc arrays, %s given', \gettype($head)) |
331
|
|
|
); |
332
|
|
|
} |
333
|
|
|
|
334
|
|
|
$head = \array_fill_keys(\array_keys($head), null); |
335
|
|
|
foreach ($rows as $i => &$row) { |
336
|
|
|
$row = \array_merge($head, $row); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
foreach ($head as $col => &$value) { |
340
|
|
|
$cols = \array_column($rows, $col); |
341
|
|
|
$span = \array_map('strlen', $cols); |
342
|
|
|
$span[] = \strlen($col); |
343
|
|
|
$value = \max($span); |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
return [$head, $rows]; |
347
|
|
|
} |
348
|
|
|
|
349
|
|
|
protected function normalizeStyles(array $styles) |
350
|
|
|
{ |
351
|
|
|
$default = [ |
352
|
|
|
// styleFor => ['styleStartFn', 'end'] |
353
|
|
|
'head' => ['', ''], |
354
|
|
|
'odd' => ['', ''], |
355
|
|
|
'even' => ['', ''], |
356
|
|
|
]; |
357
|
|
|
|
358
|
|
|
foreach ($styles as $for => $style) { |
359
|
|
|
if (isset($default[$for])) { |
360
|
|
|
$default[$for] = ['<' . \trim($style, '<> ') . '>', '</end>']; |
361
|
|
|
} |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
return $default; |
365
|
|
|
} |
366
|
|
|
|
367
|
|
|
/** |
368
|
|
|
* Write to stdout or stderr magically. |
369
|
|
|
* |
370
|
|
|
* @param string $method |
371
|
|
|
* @param array $arguments |
372
|
|
|
* |
373
|
|
|
* @return self |
374
|
|
|
*/ |
375
|
|
|
public function __call(string $method, array $arguments): self |
376
|
|
|
{ |
377
|
|
|
if (\method_exists($this->cursor, $method)) { |
378
|
|
|
return $this->doWrite($this->cursor->{$method}(...$arguments)); |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
$this->method = $method; |
382
|
|
|
|
383
|
|
|
return $this->write(...$arguments); |
|
|
|
|
384
|
|
|
} |
385
|
|
|
} |
386
|
|
|
|
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.