1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the webmozart/console package. |
5
|
|
|
* |
6
|
|
|
* (c) Bernhard Schussek <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Webmozart\Console\UI\Component; |
13
|
|
|
|
14
|
|
|
use Webmozart\Console\Api\Formatter\Formatter; |
15
|
|
|
use Webmozart\Console\Util\StringUtil; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Wraps cells to fit a given screen width with a given number of columns. |
19
|
|
|
* |
20
|
|
|
* You can add data cells with {@link addCell()}. Call {@link fit()} to fit |
21
|
|
|
* the cells into a given maximum width and number of columns. |
22
|
|
|
* |
23
|
|
|
* You can access the rows with the wrapped cells with {@link getWrappedRows()}. |
24
|
|
|
* |
25
|
|
|
* @since 1.0 |
26
|
|
|
* |
27
|
|
|
* @author Bernhard Schussek <[email protected]> |
28
|
|
|
*/ |
29
|
|
|
class CellWrapper |
30
|
|
|
{ |
31
|
|
|
/** |
32
|
|
|
* @var string[] |
33
|
|
|
*/ |
34
|
|
|
private $cells = array(); |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var int[][] |
38
|
|
|
*/ |
39
|
|
|
private $cellLengths = array(); |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var string[][] |
43
|
|
|
*/ |
44
|
|
|
private $wrappedRows = array(); |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var int |
48
|
|
|
*/ |
49
|
|
|
private $nbColumns = 0; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var int[] |
53
|
|
|
*/ |
54
|
|
|
private $columnLengths; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @var bool |
58
|
|
|
*/ |
59
|
|
|
private $wordWraps = false; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @var bool |
63
|
|
|
*/ |
64
|
|
|
private $wordCuts = false; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* @var int |
68
|
|
|
*/ |
69
|
|
|
private $maxTotalWidth = 0; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var int |
73
|
|
|
*/ |
74
|
|
|
private $totalWidth = 0; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Adds a cell to the wrapper. |
78
|
|
|
* |
79
|
|
|
* @param string $cell The data cell. |
80
|
|
|
*/ |
81
|
21 |
|
public function addCell($cell) |
82
|
|
|
{ |
83
|
21 |
|
$this->cells[] = rtrim($cell); |
84
|
21 |
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Adds cells to the wrapper. |
88
|
|
|
* |
89
|
|
|
* @param string[] $cells The data cells. |
90
|
|
|
*/ |
91
|
|
|
public function addCells(array $cells) |
92
|
|
|
{ |
93
|
|
|
foreach ($cells as $cell) { |
94
|
|
|
$this->cells[] = rtrim($cell); |
95
|
|
|
} |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* Sets the data cells in the wrapper. |
100
|
|
|
* |
101
|
|
|
* @param string[] $cells The data cells. |
102
|
|
|
*/ |
103
|
|
|
public function setCells(array $cells) |
104
|
|
|
{ |
105
|
|
|
$this->cells = array(); |
106
|
|
|
|
107
|
|
|
$this->addCells($cells); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Returns the data cells in the wrapper. |
112
|
|
|
* |
113
|
|
|
* @return string[] The data cells. |
114
|
|
|
*/ |
115
|
|
|
public function getCells() |
116
|
|
|
{ |
117
|
|
|
return $this->cells; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* Returns the wrapped cells organized by rows and columns. |
122
|
|
|
* |
123
|
|
|
* The method {@link fit()} should be called before accessing this method. |
124
|
|
|
* Otherwise, an empty array is returned. |
125
|
|
|
* |
126
|
|
|
* @return string[][] An array of arrays. The first level represents rows, |
127
|
|
|
* the second level the cells in each row. |
128
|
|
|
*/ |
129
|
21 |
|
public function getWrappedRows() |
130
|
|
|
{ |
131
|
21 |
|
return $this->wrappedRows; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Returns the lengths of the wrapped columns. |
136
|
|
|
* |
137
|
|
|
* The method {@link fit()} should be called before accessing this method. |
138
|
|
|
* Otherwise, an empty array is returned. |
139
|
|
|
* |
140
|
|
|
* @return int[] The lengths of each column. |
141
|
|
|
*/ |
142
|
21 |
|
public function getColumnLengths() |
143
|
|
|
{ |
144
|
21 |
|
return $this->columnLengths; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Returns the number of wrapped columns. |
149
|
|
|
* |
150
|
|
|
* The method {@link fit()} should be called before accessing this method. |
151
|
|
|
* Otherwise this method returns zero. |
152
|
|
|
* |
153
|
|
|
* @return int The number of columns. |
154
|
|
|
*/ |
155
|
|
|
public function getNbColumns() |
156
|
|
|
{ |
157
|
|
|
return $this->nbColumns; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Returns an estimated number of columns for the given maximum width. |
162
|
|
|
* |
163
|
|
|
* @param int $maxTotalWidth The maximum total width of the columns. |
164
|
|
|
* |
165
|
|
|
* @return int The estimated number of columns. |
166
|
|
|
*/ |
167
|
10 |
|
public function getEstimatedNbColumns($maxTotalWidth) |
168
|
|
|
{ |
169
|
10 |
|
$i = 0; |
170
|
10 |
|
$rowWidth = 0; |
171
|
|
|
|
172
|
10 |
|
while (isset($this->cells[$i])) { |
173
|
10 |
|
$rowWidth += StringUtil::getLength($this->cells[$i]); |
174
|
|
|
|
175
|
10 |
|
if ($rowWidth > $maxTotalWidth) { |
176
|
|
|
// Return previous number of columns |
177
|
9 |
|
return $i; |
178
|
|
|
} |
179
|
|
|
|
180
|
10 |
|
++$i; |
181
|
|
|
} |
182
|
|
|
|
183
|
1 |
|
return $i; |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
/** |
187
|
|
|
* Returns the maximum allowed total width of the columns. |
188
|
|
|
* |
189
|
|
|
* The method {@link fit()} should be called before accessing this method. |
190
|
|
|
* Otherwise this method returns zero. |
191
|
|
|
* |
192
|
|
|
* @return int The maximum allowed total width. |
193
|
|
|
*/ |
194
|
|
|
public function getMaxTotalWidth() |
195
|
|
|
{ |
196
|
|
|
return $this->maxTotalWidth; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Returns the actual total column width. |
201
|
|
|
* |
202
|
|
|
* The method {@link fit()} should be called before accessing this method. |
203
|
|
|
* Otherwise this method returns zero. |
204
|
|
|
* |
205
|
|
|
* @return int The actual total column width. |
206
|
|
|
*/ |
207
|
|
|
public function getTotalWidth() |
208
|
|
|
{ |
209
|
|
|
return $this->totalWidth; |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Returns whether any of the cells needed to be wrapped into multiple |
214
|
|
|
* lines. |
215
|
|
|
* |
216
|
|
|
* The method {@link fit()} should be called before accessing this method. |
217
|
|
|
* Otherwise this method returns `false`. |
218
|
|
|
* |
219
|
|
|
* @return bool Returns `true` if a cell was wrapped into multiple lines |
220
|
|
|
* and `false` otherwise. |
221
|
|
|
*/ |
222
|
|
|
public function hasWordWraps() |
223
|
|
|
{ |
224
|
|
|
return $this->wordWraps; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Returns whether any of the cells contains words cut in two. |
229
|
|
|
* |
230
|
|
|
* The method {@link fit()} should be called before accessing this method. |
231
|
|
|
* Otherwise this method returns `false`. |
232
|
|
|
* |
233
|
|
|
* @return bool Returns `true` if a cell contains words cut in two and |
234
|
|
|
* `false` otherwise. |
235
|
|
|
*/ |
236
|
10 |
|
public function hasWordCuts() |
237
|
|
|
{ |
238
|
10 |
|
return $this->wordCuts; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
/** |
242
|
|
|
* Fits the added cells into the given maximum total width with the given |
243
|
|
|
* number of columns. |
244
|
|
|
* |
245
|
|
|
* @param int $maxTotalWidth The maximum total width of the columns. |
246
|
|
|
* @param int $nbColumns The number of columns to use. |
247
|
|
|
* @param Formatter $formatter The formatter used to remove style tags. |
248
|
|
|
*/ |
249
|
21 |
|
public function fit($maxTotalWidth, $nbColumns, Formatter $formatter) |
250
|
|
|
{ |
251
|
21 |
|
$this->resetState($maxTotalWidth, $nbColumns); |
252
|
21 |
|
$this->initRows($formatter); |
253
|
|
|
|
254
|
|
|
// If the cells fit within the max width we're good |
255
|
21 |
|
if ($this->totalWidth <= $maxTotalWidth) { |
256
|
7 |
|
return; |
257
|
|
|
} |
258
|
|
|
|
259
|
14 |
|
$this->wrapColumns($formatter); |
260
|
14 |
|
} |
261
|
|
|
|
262
|
21 |
|
private function resetState($maxTotalWidth, $nbColumns) |
263
|
|
|
{ |
264
|
21 |
|
$this->wrappedRows = array(); |
265
|
21 |
|
$this->nbColumns = $nbColumns; |
266
|
21 |
|
$this->cellLengths = array(); |
267
|
21 |
|
$this->columnLengths = array_fill(0, $nbColumns, 0); |
268
|
21 |
|
$this->wordWraps = false; |
269
|
21 |
|
$this->wordCuts = false; |
270
|
21 |
|
$this->maxTotalWidth = $maxTotalWidth; |
271
|
21 |
|
$this->totalWidth = 0; |
272
|
21 |
|
} |
273
|
|
|
|
274
|
21 |
|
private function initRows(Formatter $formatter) |
275
|
|
|
{ |
276
|
21 |
|
$row = null; |
277
|
21 |
|
$col = 0; |
278
|
|
|
|
279
|
21 |
|
foreach ($this->cells as $i => $cell) { |
280
|
21 |
|
if (0 === $col) { |
281
|
21 |
|
$this->wrappedRows[] = array(); |
282
|
21 |
|
$this->cellLengths[] = array(); |
283
|
|
|
|
284
|
21 |
|
$row = &$this->wrappedRows[count($this->wrappedRows) - 1]; |
285
|
21 |
|
$cellLengths = &$this->cellLengths[count($this->cellLengths) - 1]; |
286
|
|
|
} |
287
|
|
|
|
288
|
21 |
|
$row[$col] = $cell; |
289
|
21 |
|
$cellLengths[$col] = StringUtil::getLength($cell, $formatter); |
|
|
|
|
290
|
21 |
|
$this->columnLengths[$col] = max($this->columnLengths[$col], $cellLengths[$col]); |
291
|
|
|
|
292
|
21 |
|
$col = ($col + 1) % $this->nbColumns; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
// Fill last row up |
296
|
21 |
|
if ($col > 0) { |
297
|
7 |
|
while ($col < $this->nbColumns) { |
298
|
7 |
|
$row[$col] = ''; |
299
|
7 |
|
$cellLengths[$col] = 0; |
300
|
7 |
|
++$col; |
301
|
|
|
} |
302
|
|
|
} |
303
|
|
|
|
304
|
21 |
|
$this->totalWidth = array_sum($this->columnLengths); |
|
|
|
|
305
|
21 |
|
} |
306
|
|
|
|
307
|
14 |
|
private function wrapColumns(Formatter $formatter) |
308
|
|
|
{ |
309
|
14 |
|
$availableWidth = $this->maxTotalWidth; |
310
|
14 |
|
$longColumnLengths = $this->columnLengths; |
311
|
|
|
|
312
|
|
|
// Filter "short" column, i.e. columns that are not wrapped |
313
|
|
|
// We distribute the available screen width by the number of columns |
314
|
|
|
// and decide that all columns that are shorter than their share are |
315
|
|
|
// "short". |
316
|
|
|
// This process is repeated until no more "short" columns are found. |
317
|
|
|
do { |
318
|
14 |
|
$threshold = $availableWidth / count($longColumnLengths); |
319
|
14 |
|
$repeat = false; |
320
|
|
|
|
321
|
14 |
|
foreach ($longColumnLengths as $col => $length) { |
322
|
14 |
|
if ($length <= $threshold) { |
323
|
12 |
|
$availableWidth -= $length; |
324
|
12 |
|
unset($longColumnLengths[$col]); |
325
|
14 |
|
$repeat = true; |
326
|
|
|
} |
327
|
|
|
} |
328
|
14 |
|
} while ($repeat); |
329
|
|
|
|
330
|
|
|
// Calculate actual and available width |
331
|
14 |
|
$actualWidth = 0; |
332
|
14 |
|
$lastAdaptedCol = 0; |
333
|
|
|
|
334
|
|
|
// "Long" columns, i.e. columns that need to be wrapped, are added to |
335
|
|
|
// the actual width |
336
|
14 |
|
foreach ($longColumnLengths as $col => $length) { |
337
|
14 |
|
$actualWidth += $length; |
338
|
14 |
|
$lastAdaptedCol = $col; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
// Fit columns into available width |
342
|
14 |
|
foreach ($longColumnLengths as $col => $length) { |
343
|
|
|
// Keep ratios of column lengths and distribute them among the |
344
|
|
|
// available width |
345
|
14 |
|
$this->columnLengths[$col] = round(($length / $actualWidth) * $availableWidth); |
346
|
|
|
|
347
|
14 |
|
if ($col === $lastAdaptedCol) { |
348
|
|
|
// Fix rounding errors |
349
|
14 |
|
$this->columnLengths[$col] += $this->maxTotalWidth - array_sum($this->columnLengths); |
350
|
|
|
} |
351
|
|
|
|
352
|
14 |
|
$this->wrapColumn($col, $this->columnLengths[$col], $formatter); |
353
|
|
|
|
354
|
|
|
// Recalculate the column length based on the actual wrapped length |
355
|
14 |
|
$this->refreshColumnLength($col); |
356
|
|
|
|
357
|
|
|
// Recalculate the actual width based on the changed length. |
358
|
14 |
|
$actualWidth = $actualWidth - $length + $this->columnLengths[$col]; |
359
|
|
|
} |
360
|
|
|
|
361
|
14 |
|
$this->totalWidth = array_sum($this->columnLengths); |
|
|
|
|
362
|
14 |
|
} |
363
|
|
|
|
364
|
14 |
|
private function wrapColumn($col, $columnLength, Formatter $formatter) |
365
|
|
|
{ |
366
|
14 |
|
foreach ($this->wrappedRows as $i => $row) { |
367
|
14 |
|
$cell = $row[$col]; |
368
|
14 |
|
$cellLength = $this->cellLengths[$i][$col]; |
369
|
|
|
|
370
|
14 |
|
if ($cellLength > $columnLength) { |
371
|
14 |
|
$this->wordWraps = true; |
372
|
|
|
|
373
|
14 |
|
if (!$this->wordCuts) { |
374
|
14 |
|
$minLengthWithoutCut = StringUtil::getMaxWordLength($cell, $formatter); |
375
|
|
|
|
376
|
14 |
|
if ($minLengthWithoutCut > $columnLength) { |
377
|
7 |
|
$this->wordCuts = true; |
378
|
|
|
} |
379
|
|
|
} |
380
|
|
|
|
381
|
|
|
// TODO use format aware wrapper |
382
|
|
|
// true: Words may be cut in two |
383
|
14 |
|
$wrappedCell = wordwrap($cell, $columnLength, "\n", true); |
384
|
|
|
|
385
|
14 |
|
$this->wrappedRows[$i][$col] = $wrappedCell; |
386
|
|
|
|
387
|
|
|
// Refresh cell length |
388
|
14 |
|
$this->cellLengths[$i][$col] = StringUtil::getMaxLineLength($wrappedCell, $formatter); |
389
|
|
|
} |
390
|
|
|
} |
391
|
14 |
|
} |
392
|
|
|
|
393
|
14 |
|
private function refreshColumnLength($col) |
394
|
|
|
{ |
395
|
14 |
|
$this->columnLengths[$col] = 0; |
396
|
|
|
|
397
|
14 |
|
foreach ($this->wrappedRows as $i => $row) { |
398
|
14 |
|
$this->columnLengths[$col] = max($this->columnLengths[$col], $this->cellLengths[$i][$col]); |
399
|
|
|
} |
400
|
14 |
|
} |
401
|
|
|
} |
402
|
|
|
|
If you define a variable conditionally, it can happen that it is not defined for all execution paths.
Let’s take a look at an example:
In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.
Available Fixes
Check for existence of the variable explicitly:
Define a default value for the variable:
Add a value for the missing path: