Skip to content

Commit 1a0e91e

Browse files
authored
Fix #19906: Fixed multiline strings in the \yii\console\widgets\Table widget
1 parent 76d6345 commit 1a0e91e

File tree

3 files changed

+167
-2
lines changed

3 files changed

+167
-2
lines changed

framework/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Yii Framework 2 Change Log
1010
- Enh #19853: Added support for default value for `\yii\helpers\Console::select()` (rhertogh)
1111
- Bug #19868: Added whitespace sanitation for tests, due to updates in ICU 72 (schmunk42)
1212
- Enh #19884: Added support Enums in Query Builder (sk1t0n)
13+
- Bug #19906: Fixed multiline strings in the `\yii\console\widgets\Table` widget (rhertogh)
1314

1415

1516
2.0.48.1 May 24, 2023

framework/console/widgets/Table.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,18 +252,32 @@ protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
252252
if ($index !== 0) {
253253
$buffer .= $spanMiddle . ' ';
254254
}
255+
256+
$arrayFromMultilineString = false;
257+
if (is_string($cell)) {
258+
$cellLines = explode(PHP_EOL, $cell);
259+
if (count($cellLines) > 1) {
260+
$cell = $cellLines;
261+
$arrayFromMultilineString = true;
262+
}
263+
}
264+
255265
if (is_array($cell)) {
256266
if (empty($renderedChunkTexts[$index])) {
257267
$renderedChunkTexts[$index] = '';
258268
$start = 0;
259-
$prefix = $this->listPrefix;
269+
$prefix = $arrayFromMultilineString ? '' : $this->listPrefix;
260270
if (!isset($arrayPointer[$index])) {
261271
$arrayPointer[$index] = 0;
262272
}
263273
} else {
264274
$start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset);
265275
}
266-
$chunk = Console::ansiColorizedSubstr($cell[$arrayPointer[$index]], $start, $cellSize - 4);
276+
$chunk = Console::ansiColorizedSubstr(
277+
$cell[$arrayPointer[$index]],
278+
$start,
279+
$cellSize - 2 - Console::ansiStrwidth($prefix)
280+
);
267281
$renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk);
268282
$fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]);
269283
if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) {
@@ -339,6 +353,9 @@ protected function calculateRowsSize()
339353
if (is_array($val)) {
340354
return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix);
341355
}
356+
if (is_string($val)) {
357+
return max(array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val)));
358+
}
342359
return Console::ansiStrwidth($val);
343360
}, $column)) + 2;
344361
$this->columnWidths[] = $columnWidth;
@@ -388,6 +405,9 @@ protected function calculateRowHeight($row)
388405
if (is_array($val)) {
389406
return array_map('yii\helpers\Console::ansiStrwidth', $val);
390407
}
408+
if (is_string($val)) {
409+
return array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val));
410+
}
391411
return Console::ansiStrwidth($val);
392412
}, $row));
393413
return max($rowsPerCell);

tests/framework/console/widgets/TableTest.php

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,121 @@ public function testTable($headers, $rows)
5858
║ testcontent21 │ testcontent22 │ testcontent23 ║
5959
╚═══════════════╧═══════════════╧═══════════════╝
6060

61+
EXPECTED;
62+
63+
$tableContent = $table
64+
->setHeaders($headers)
65+
->setRows($rows)
66+
->setScreenWidth(200)
67+
->run();
68+
$this->assertEqualsWithoutLE($expected, $tableContent);
69+
}
70+
71+
public function getMultiLineTableData()
72+
{
73+
return [
74+
[
75+
['test1', 'test2', 'test3' . PHP_EOL . 'multiline'],
76+
[
77+
['test' . PHP_EOL . 'content1', 'testcontent2', 'test' . PHP_EOL . 'content3'],
78+
[
79+
'testcontent21',
80+
'testcontent22' . PHP_EOL
81+
. 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
82+
. 'content',
83+
'testcontent23' . PHP_EOL
84+
. 'loooooooooooooooooooooooooooooooooooong content'
85+
],
86+
]
87+
],
88+
[
89+
['key1' => 'test1', 'key2' => 'test2', 'key3' => 'test3' . PHP_EOL . 'multiline'],
90+
[
91+
[
92+
'key1' => 'test' . PHP_EOL . 'content1',
93+
'key2' => 'testcontent2',
94+
'key3' => 'test' . PHP_EOL . 'content3'
95+
],
96+
[
97+
'key1' => 'testcontent21',
98+
'key2' => 'testcontent22' . PHP_EOL
99+
. 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
100+
. 'content',
101+
'key3' => 'testcontent23' . PHP_EOL
102+
. 'loooooooooooooooooooooooooooooooooooong content'
103+
],
104+
]
105+
]
106+
];
107+
}
108+
109+
/**
110+
* @dataProvider getMultiLineTableData
111+
*/
112+
public function testMultiLineTable($headers, $rows)
113+
{
114+
$table = new Table();
115+
116+
$expected = <<<'EXPECTED'
117+
╔═════════════╤═════════════════════════════════════╤═════════════════════════════════════════════╗
118+
║ test1 │ test2 │ test3 ║
119+
║ │ │ multiline ║
120+
╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
121+
║ test │ testcontent2 │ test ║
122+
║ content1 │ │ content3 ║
123+
╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
124+
║ testcontent │ testcontent22 │ testcontent23 ║
125+
║ 21 │ loooooooooooooooooooooooooooooooooo │ loooooooooooooooooooooooooooooooooooong con ║
126+
║ │ oong │ tent ║
127+
║ │ content │ ║
128+
╚═════════════╧═════════════════════════════════════╧═════════════════════════════════════════════╝
129+
130+
EXPECTED;
131+
132+
$tableContent = $table
133+
->setHeaders($headers)
134+
->setRows($rows)
135+
->setScreenWidth(100)
136+
->run();
137+
$this->assertEqualsWithoutLE($expected, $tableContent);
138+
}
139+
140+
public function getNumericTableData()
141+
{
142+
return [
143+
[
144+
[1, 2, 3],
145+
[
146+
[1, 1.2, -1.3],
147+
[-2, 2.2, 2.3],
148+
]
149+
],
150+
[
151+
['key1' => 1, 'key2' => 2, 'key3' => 3],
152+
[
153+
['key1' => 1, 'key2' => 1.2, 'key3' => -1.3],
154+
['key1' => -2, 'key2' => 2.2, 'key3' => 2.3],
155+
]
156+
]
157+
];
158+
}
159+
160+
/**
161+
* @dataProvider getNumericTableData
162+
*/
163+
public function testNumericTable($headers, $rows)
164+
{
165+
$table = new Table();
166+
167+
$expected = <<<'EXPECTED'
168+
╔════╤═════╤══════╗
169+
║ 1 │ 2 │ 3 ║
170+
╟────┼─────┼──────╢
171+
║ 1 │ 1.2 │ -1.3 ║
172+
╟────┼─────┼──────╢
173+
║ -2 │ 2.2 │ 2.3 ║
174+
╚════╧═════╧══════╝
175+
61176
EXPECTED;
62177

63178
$tableContent = $table
@@ -141,6 +256,35 @@ public function testListPrefix()
141256
);
142257
}
143258

259+
public function testLongerListPrefix()
260+
{
261+
$table = new Table();
262+
263+
$expected = <<<'EXPECTED'
264+
╔═════════════════════════════════╤═════════════════════════════════╤═════════════════════════════╗
265+
║ test1 │ test2 │ test3 ║
266+
╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
267+
║ testcontent1 │ testcontent2 │ testcontent3 ║
268+
╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
269+
║ testcontent21 with looooooooooo │ testcontent22 with looooooooooo │ -- col1 with looooooooooooo ║
270+
║ ooooooooooooong content │ ooooooooooooong content │ ooooooooooong content ║
271+
║ │ │ -- col2 with long content ║
272+
╚═════════════════════════════════╧═════════════════════════════════╧═════════════════════════════╝
273+
274+
EXPECTED;
275+
276+
$this->assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', 'test3'])
277+
->setRows([
278+
['testcontent1', 'testcontent2', 'testcontent3'],
279+
[
280+
'testcontent21 with loooooooooooooooooooooooong content',
281+
'testcontent22 with loooooooooooooooooooooooong content',
282+
['col1 with loooooooooooooooooooooooong content', 'col2 with long content']
283+
],
284+
])->setScreenWidth(100)->setListPrefix('-- ')->run()
285+
);
286+
}
287+
144288
public function testCustomChars()
145289
{
146290
$table = new Table();

0 commit comments

Comments
 (0)