From 5b25698e972659acbd3bb8cf0ae78776643cbec9 Mon Sep 17 00:00:00 2001
From: rhertogh <rhertogh@users.noreply.github.com>
Date: Fri, 21 Jul 2023 23:39:28 +0200
Subject: [PATCH 1/4] Support multi line cell values in
 `\yii\console\widgets\Table`

---
 framework/console/widgets/Table.php           | 18 ++++++-
 tests/framework/console/widgets/TableTest.php | 49 +++++++++++++++++++
 2 files changed, 66 insertions(+), 1 deletion(-)

diff --git a/framework/console/widgets/Table.php b/framework/console/widgets/Table.php
index 4862e88b6be..7bda8100058 100644
--- a/framework/console/widgets/Table.php
+++ b/framework/console/widgets/Table.php
@@ -252,11 +252,21 @@ protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
                 if ($index !== 0) {
                     $buffer .= $spanMiddle . ' ';
                 }
+
+                $arrayFromMultilineString = false;
+                if (is_string($cell)) {
+                    $cellLines = explode(PHP_EOL, $cell);
+                    if (count($cellLines) > 1) {
+                        $cell = $cellLines;
+                        $arrayFromMultilineString = true;
+                    }
+                }
+
                 if (is_array($cell)) {
                     if (empty($renderedChunkTexts[$index])) {
                         $renderedChunkTexts[$index] = '';
                         $start = 0;
-                        $prefix = $this->listPrefix;
+                        $prefix = $arrayFromMultilineString ? '' : $this->listPrefix;
                         if (!isset($arrayPointer[$index])) {
                             $arrayPointer[$index] = 0;
                         }
@@ -339,6 +349,9 @@ protected function calculateRowsSize()
                 if (is_array($val)) {
                     return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix);
                 }
+                if (is_string($val)) {
+                    return max(array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val)));
+                }
                 return Console::ansiStrwidth($val);
             }, $column)) + 2;
             $this->columnWidths[] = $columnWidth;
@@ -388,6 +401,9 @@ protected function calculateRowHeight($row)
             if (is_array($val)) {
                 return array_map('yii\helpers\Console::ansiStrwidth', $val);
             }
+            if (is_string($val)) {
+                return array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val));
+            }
             return Console::ansiStrwidth($val);
         }, $row));
         return max($rowsPerCell);
diff --git a/tests/framework/console/widgets/TableTest.php b/tests/framework/console/widgets/TableTest.php
index 95e7c60e462..77eae1df5cc 100644
--- a/tests/framework/console/widgets/TableTest.php
+++ b/tests/framework/console/widgets/TableTest.php
@@ -58,6 +58,55 @@ public function testTable($headers, $rows)
 ║ testcontent21 │ testcontent22 │ testcontent23 ║
 ╚═══════════════╧═══════════════╧═══════════════╝
 
+EXPECTED;
+
+        $tableContent = $table
+            ->setHeaders($headers)
+            ->setRows($rows)
+            ->setScreenWidth(200)
+            ->run();
+        $this->assertEqualsWithoutLE($expected, $tableContent);
+    }
+
+    public function getMultiLineTableData()
+    {
+        return [
+            [
+                ['test1', 'test2', 'test3' . PHP_EOL . 'multiline'],
+                [
+                    ['test' . PHP_EOL . 'content1', 'testcontent2', 'test' . PHP_EOL . 'content3'],
+                    ['testcontent21', 'test' . PHP_EOL . 'content22', 'testcontent23'],
+                ]
+            ],
+            [
+                ['key1' => 'test1', 'key2' => 'test2', 'key3' => 'test3' . PHP_EOL . 'multiline'],
+                [
+                    ['key1' => 'test' . PHP_EOL . 'content1', 'key2' => 'testcontent2', 'key3' => 'test' . PHP_EOL . 'content3'],
+                    ['key1' => 'testcontent21', 'key2' => 'test' . PHP_EOL . 'content22', 'key3' => 'testcontent23'],
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @dataProvider getMultiLineTableData
+     */
+    public function testMultiLineTable($headers, $rows)
+    {
+        $table = new Table();
+
+        $expected = <<<'EXPECTED'
+╔═══════════════╤══════════════╤═══════════════╗
+║ test1         │ test2        │ test3         ║
+║               │              │ multiline     ║
+╟───────────────┼──────────────┼───────────────╢
+║ test          │ testcontent2 │ test          ║
+║ content1      │              │ content3      ║
+╟───────────────┼──────────────┼───────────────╢
+║ testcontent21 │ test         │ testcontent23 ║
+║               │ content22    │               ║
+╚═══════════════╧══════════════╧═══════════════╝
+
 EXPECTED;
 
         $tableContent = $table

From cdd1866a929d3916ab01437ca57fccc2b7aca8b8 Mon Sep 17 00:00:00 2001
From: rhertogh <rhertogh@users.noreply.github.com>
Date: Fri, 21 Jul 2023 23:52:50 +0200
Subject: [PATCH 2/4] Updated changelog for #19906  (Fixed multiline strings in
 the `\yii\console\widgets\Table` widget)

---
 framework/CHANGELOG.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md
index 894b1ad2a5d..dca3886227c 100644
--- a/framework/CHANGELOG.md
+++ b/framework/CHANGELOG.md
@@ -10,6 +10,7 @@ Yii Framework 2 Change Log
 - Enh #19853: Added support for default value for `\yii\helpers\Console::select()` (rhertogh)
 - Bug #19868: Added whitespace sanitation for tests, due to updates in ICU 72 (schmunk42)
 - Enh #19884: Added support Enums in Query Builder (sk1t0n)
+- Bug #19906: Fixed multiline strings in the `\yii\console\widgets\Table` widget (rhertogh)
 
 
 2.0.48.1 May 24, 2023

From 92d50cfba6d0c747bc3d012cb9614be33bc71945 Mon Sep 17 00:00:00 2001
From: rhertogh <rhertogh@users.noreply.github.com>
Date: Sat, 22 Jul 2023 00:22:11 +0200
Subject: [PATCH 3/4] Added
 `\yiiunit\framework\console\widgets\TableTest::testNumericTable()`

---
 tests/framework/console/widgets/TableTest.php | 46 +++++++++++++++++++
 1 file changed, 46 insertions(+)

diff --git a/tests/framework/console/widgets/TableTest.php b/tests/framework/console/widgets/TableTest.php
index 77eae1df5cc..35e901b2f52 100644
--- a/tests/framework/console/widgets/TableTest.php
+++ b/tests/framework/console/widgets/TableTest.php
@@ -107,6 +107,52 @@ public function testMultiLineTable($headers, $rows)
 ║               │ content22    │               ║
 ╚═══════════════╧══════════════╧═══════════════╝
 
+EXPECTED;
+
+        $tableContent = $table
+            ->setHeaders($headers)
+            ->setRows($rows)
+            ->setScreenWidth(200)
+            ->run();
+        $this->assertEqualsWithoutLE($expected, $tableContent);
+    }
+
+    public function getNumericTableData()
+    {
+        return [
+            [
+                [1, 2, 3],
+                [
+                    [1, 1.2, -1.3],
+                    [-2, 2.2, 2.3],
+                ]
+            ],
+            [
+                ['key1' => 1, 'key2' => 2, 'key3' => 3],
+                [
+                    ['key1' => 1, 'key2' => 1.2, 'key3' => -1.3],
+                    ['key1' => -2, 'key2' => 2.2, 'key3' => 2.3],
+                ]
+            ]
+        ];
+    }
+
+    /**
+     * @dataProvider getNumericTableData
+     */
+    public function testNumericTable($headers, $rows)
+    {
+        $table = new Table();
+
+        $expected = <<<'EXPECTED'
+╔════╤═════╤══════╗
+║ 1  │ 2   │ 3    ║
+╟────┼─────┼──────╢
+║ 1  │ 1.2 │ -1.3 ║
+╟────┼─────┼──────╢
+║ -2 │ 2.2 │ 2.3  ║
+╚════╧═════╧══════╝
+
 EXPECTED;
 
         $tableContent = $table

From 1d8be3149c568cac9cbc0bc244e4c7aaa59e2285 Mon Sep 17 00:00:00 2001
From: rhertogh <rhertogh@users.noreply.github.com>
Date: Sat, 22 Jul 2023 14:30:05 +0200
Subject: [PATCH 4/4] Fixed rendering of long multiline strings and longer
 $listPrefix in `\yii\console\widgets\Table`

---
 framework/console/widgets/Table.php           |  6 +-
 tests/framework/console/widgets/TableTest.php | 77 +++++++++++++++----
 2 files changed, 68 insertions(+), 15 deletions(-)

diff --git a/framework/console/widgets/Table.php b/framework/console/widgets/Table.php
index 7bda8100058..658cf5fd55b 100644
--- a/framework/console/widgets/Table.php
+++ b/framework/console/widgets/Table.php
@@ -273,7 +273,11 @@ protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
                     } else {
                         $start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset);
                     }
-                    $chunk = Console::ansiColorizedSubstr($cell[$arrayPointer[$index]], $start, $cellSize - 4);
+                    $chunk = Console::ansiColorizedSubstr(
+                        $cell[$arrayPointer[$index]],
+                        $start,
+                        $cellSize - 2 - Console::ansiStrwidth($prefix)
+                    );
                     $renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk);
                     $fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]);
                     if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) {
diff --git a/tests/framework/console/widgets/TableTest.php b/tests/framework/console/widgets/TableTest.php
index 35e901b2f52..b5bf6f2c6dc 100644
--- a/tests/framework/console/widgets/TableTest.php
+++ b/tests/framework/console/widgets/TableTest.php
@@ -75,14 +75,32 @@ public function getMultiLineTableData()
                 ['test1', 'test2', 'test3' . PHP_EOL . 'multiline'],
                 [
                     ['test' . PHP_EOL . 'content1', 'testcontent2', 'test' . PHP_EOL . 'content3'],
-                    ['testcontent21', 'test' . PHP_EOL . 'content22', 'testcontent23'],
+                    [
+                        'testcontent21',
+                        'testcontent22' . PHP_EOL
+                        . 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
+                        . 'content',
+                        'testcontent23' . PHP_EOL
+                        . 'loooooooooooooooooooooooooooooooooooong content'
+                    ],
                 ]
             ],
             [
                 ['key1' => 'test1', 'key2' => 'test2', 'key3' => 'test3' . PHP_EOL . 'multiline'],
                 [
-                    ['key1' => 'test' . PHP_EOL . 'content1', 'key2' => 'testcontent2', 'key3' => 'test' . PHP_EOL . 'content3'],
-                    ['key1' => 'testcontent21', 'key2' => 'test' . PHP_EOL . 'content22', 'key3' => 'testcontent23'],
+                    [
+                        'key1' => 'test' . PHP_EOL . 'content1',
+                        'key2' => 'testcontent2',
+                        'key3' => 'test' . PHP_EOL . 'content3'
+                    ],
+                    [
+                        'key1' => 'testcontent21',
+                        'key2' => 'testcontent22' . PHP_EOL
+                            . 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
+                            . 'content',
+                        'key3' => 'testcontent23' . PHP_EOL
+                            . 'loooooooooooooooooooooooooooooooooooong content'
+                    ],
                 ]
             ]
         ];
@@ -96,23 +114,25 @@ public function testMultiLineTable($headers, $rows)
         $table = new Table();
 
         $expected = <<<'EXPECTED'
-╔═══════════════╤══════════════╤═══════════════╗
-║ test1         │ test2        │ test3         ║
-║               │              │ multiline     ║
-╟───────────────┼──────────────┼───────────────╢
-║ test          │ testcontent2 │ test          ║
-║ content1      │              │ content3      ║
-╟───────────────┼──────────────┼───────────────╢
-║ testcontent21 │ test         │ testcontent23 ║
-║               │ content22    │               ║
-╚═══════════════╧══════════════╧═══════════════╝
+╔═════════════╤═════════════════════════════════════╤═════════════════════════════════════════════╗
+║ test1       │ test2                               │ test3                                       ║
+║             │                                     │ multiline                                   ║
+╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
+║ test        │ testcontent2                        │ test                                        ║
+║ content1    │                                     │ content3                                    ║
+╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
+║ testcontent │ testcontent22                       │ testcontent23                               ║
+║ 21          │ loooooooooooooooooooooooooooooooooo │ loooooooooooooooooooooooooooooooooooong con ║
+║             │ oong                                │ tent                                        ║
+║             │ content                             │                                             ║
+╚═════════════╧═════════════════════════════════════╧═════════════════════════════════════════════╝
 
 EXPECTED;
 
         $tableContent = $table
             ->setHeaders($headers)
             ->setRows($rows)
-            ->setScreenWidth(200)
+            ->setScreenWidth(100)
             ->run();
         $this->assertEqualsWithoutLE($expected, $tableContent);
     }
@@ -236,6 +256,35 @@ public function testListPrefix()
         );
     }
 
+    public function testLongerListPrefix()
+    {
+        $table = new Table();
+
+        $expected = <<<'EXPECTED'
+╔═════════════════════════════════╤═════════════════════════════════╤═════════════════════════════╗
+║ test1                           │ test2                           │ test3                       ║
+╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
+║ testcontent1                    │ testcontent2                    │ testcontent3                ║
+╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
+║ testcontent21 with looooooooooo │ testcontent22 with looooooooooo │ -- col1 with looooooooooooo ║
+║ ooooooooooooong content         │ ooooooooooooong content         │ ooooooooooong content       ║
+║                                 │                                 │ -- col2 with long content   ║
+╚═════════════════════════════════╧═════════════════════════════════╧═════════════════════════════╝
+
+EXPECTED;
+
+        $this->assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', 'test3'])
+            ->setRows([
+                ['testcontent1', 'testcontent2', 'testcontent3'],
+                [
+                    'testcontent21 with loooooooooooooooooooooooong content',
+                    'testcontent22 with loooooooooooooooooooooooong content',
+                    ['col1 with loooooooooooooooooooooooong content', 'col2 with long content']
+                ],
+            ])->setScreenWidth(100)->setListPrefix('-- ')->run()
+        );
+    }
+
     public function testCustomChars()
     {
         $table = new Table();