Completed
Push — remove-intl-polyfills ( 129df4...a6e727 )
by Alexander
16:22 queued 12:49
created

Table::setRows()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
/**
3
 * @link http://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license http://www.yiiframework.com/license/
6
 */
7
8
namespace yii\console\widgets;
9
10
use Yii;
11
use yii\base\Widget;
12
use yii\helpers\ArrayHelper;
13
use yii\helpers\Console;
14
15
/**
16
 * Table class displays a table in console.
17
 *
18
 * For example,
19
 *
20
 * ```php
21
 * $table = new Table();
22
 *
23
 * echo $table
24
 *     ->setHeaders(['test1', 'test2', 'test3'])
25
 *     ->setRows([
26
 *         ['col1', 'col2', 'col3'],
27
 *         ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
28
 *     ])
29
 *     ->run();
30
 * ```
31
 *
32
 * or
33
 *
34
 * ```php
35
 * echo Table::widget([
36
 *     'headers' => ['test1', 'test2', 'test3'],
37
 *     'rows' => [
38
 *         ['col1', 'col2', 'col3'],
39
 *         ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
40
 *     ],
41
 * ]);
42
 *
43
 * @property string $listPrefix List prefix. This property is write-only.
44
 * @property int $screenWidth Screen width. This property is write-only.
45
 *
46
 * @author Daniel Gomez Pan <[email protected]>
47
 * @since 2.0.13
48
 */
49
class Table extends Widget
50
{
51
    const DEFAULT_CONSOLE_SCREEN_WIDTH = 120;
52
    const CONSOLE_SCROLLBAR_OFFSET = 3;
53
    const CHAR_TOP = 'top';
54
    const CHAR_TOP_MID = 'top-mid';
55
    const CHAR_TOP_LEFT = 'top-left';
56
    const CHAR_TOP_RIGHT = 'top-right';
57
    const CHAR_BOTTOM = 'bottom';
58
    const CHAR_BOTTOM_MID = 'bottom-mid';
59
    const CHAR_BOTTOM_LEFT = 'bottom-left';
60
    const CHAR_BOTTOM_RIGHT = 'bottom-right';
61
    const CHAR_LEFT = 'left';
62
    const CHAR_LEFT_MID = 'left-mid';
63
    const CHAR_MID = 'mid';
64
    const CHAR_MID_MID = 'mid-mid';
65
    const CHAR_RIGHT = 'right';
66
    const CHAR_RIGHT_MID = 'right-mid';
67
    const CHAR_MIDDLE = 'middle';
68
69
    /**
70
     * @var array table headers
71
     */
72
    private $_headers = [];
73
    /**
74
     * @var array table rows
75
     */
76
    private $_rows = [];
77
    /**
78
     * @var array table chars
79
     */
80
    private $_chars = [
81
        self::CHAR_TOP => '═',
82
        self::CHAR_TOP_MID => '╤',
83
        self::CHAR_TOP_LEFT => '╔',
84
        self::CHAR_TOP_RIGHT => '╗',
85
        self::CHAR_BOTTOM => '═',
86
        self::CHAR_BOTTOM_MID => '╧',
87
        self::CHAR_BOTTOM_LEFT => '╚',
88
        self::CHAR_BOTTOM_RIGHT => '╝',
89
        self::CHAR_LEFT => '║',
90
        self::CHAR_LEFT_MID => '╟',
91
        self::CHAR_MID => '─',
92
        self::CHAR_MID_MID => '┼',
93
        self::CHAR_RIGHT => '║',
94
        self::CHAR_RIGHT_MID => '╢',
95
        self::CHAR_MIDDLE => '│',
96
    ];
97
    /**
98
     * @var array table column widths
99
     */
100
    private $_columnWidths = [];
101
    /**
102
     * @var int screen width
103
     */
104
    private $_screenWidth;
105
    /**
106
     * @var string list prefix
107
     */
108
    private $_listPrefix = '• ';
109
110
111
    /**
112
     * Set table headers.
113
     *
114
     * @param array $headers table headers
115
     * @return $this
116
     */
117 9
    public function setHeaders(array $headers)
118
    {
119 9
        $this->_headers = array_values($headers);
120 9
        return $this;
121
    }
122
123
    /**
124
     * Set table rows.
125
     *
126
     * @param array $rows table rows
127
     * @return $this
128
     */
129 9
    public function setRows(array $rows)
130
    {
131 9
        $this->_rows = array_map('array_values', $rows);
132 9
        return $this;
133
    }
134
135
    /**
136
     * Set table chars.
137
     *
138
     * @param array $chars table chars
139
     * @return $this
140
     */
141 1
    public function setChars(array $chars)
142
    {
143 1
        $this->_chars = $chars;
144 1
        return $this;
145
    }
146
147
    /**
148
     * Set screen width.
149
     *
150
     * @param int $width screen width
151
     * @return $this
152
     */
153 9
    public function setScreenWidth($width)
154
    {
155 9
        $this->_screenWidth = $width;
156 9
        return $this;
157
    }
158
159
    /**
160
     * Set list prefix.
161
     *
162
     * @param string $listPrefix list prefix
163
     * @return $this
164
     */
165 1
    public function setListPrefix($listPrefix)
166
    {
167 1
        $this->_listPrefix = $listPrefix;
168 1
        return $this;
169
    }
170
171
    /**
172
     * @return string the rendered table
173
     */
174 9
    public function run()
175
    {
176 9
        $this->calculateRowsSize();
177 9
        $buffer = $this->renderSeparator(
178 9
            $this->_chars[self::CHAR_TOP_LEFT],
179 9
            $this->_chars[self::CHAR_TOP_MID],
180 9
            $this->_chars[self::CHAR_TOP],
181 9
            $this->_chars[self::CHAR_TOP_RIGHT]
182
        );
183
        // Header
184 9
        $buffer .= $this->renderRow($this->_headers,
185 9
            $this->_chars[self::CHAR_LEFT],
186 9
            $this->_chars[self::CHAR_MIDDLE],
187 9
            $this->_chars[self::CHAR_RIGHT]
188
        );
189
190
        // Content
191 9
        foreach ($this->_rows as $row) {
192 9
            $buffer .= $this->renderSeparator(
193 9
                $this->_chars[self::CHAR_LEFT_MID],
194 9
                $this->_chars[self::CHAR_MID_MID],
195 9
                $this->_chars[self::CHAR_MID],
196 9
                $this->_chars[self::CHAR_RIGHT_MID]
197
            );
198 9
            $buffer .= $this->renderRow($row,
199 9
                $this->_chars[self::CHAR_LEFT],
200 9
                $this->_chars[self::CHAR_MIDDLE],
201 9
                $this->_chars[self::CHAR_RIGHT]);
202
        }
203
204 9
        $buffer .= $this->renderSeparator(
205 9
            $this->_chars[self::CHAR_BOTTOM_LEFT],
206 9
            $this->_chars[self::CHAR_BOTTOM_MID],
207 9
            $this->_chars[self::CHAR_BOTTOM],
208 9
            $this->_chars[self::CHAR_BOTTOM_RIGHT]
209
        );
210
211 9
        return $buffer;
212
    }
213
214
    /**
215
     * Renders a row of data into a string.
216
     *
217
     * @param array $row row of data
218
     * @param string $spanLeft character for left border
219
     * @param string $spanMiddle character for middle border
220
     * @param string $spanRight character for right border
221
     * @return string
222
     * @see \yii\console\widgets\Table::render()
223
     */
224 9
    protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
225
    {
226 9
        $size = $this->_columnWidths;
227
228 9
        $buffer = '';
229 9
        $arrayPointer = [];
230 9
        $finalChunk = [];
231 9
        for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
232 9
            $buffer .= $spanLeft . ' ';
233 9
            foreach ($size as $index => $cellSize) {
234 9
                $cell = $row[$index] ?? null;
235 9
                $prefix = '';
236 9
                if ($index !== 0) {
237 9
                    $buffer .= $spanMiddle . ' ';
238
                }
239 9
                if (is_array($cell)) {
240 2
                    if (empty($finalChunk[$index])) {
241 2
                        $finalChunk[$index] = '';
242 2
                        $start = 0;
243 2
                        $prefix = $this->_listPrefix;
244 2
                        if (!isset($arrayPointer[$index])) {
245 2
                            $arrayPointer[$index] = 0;
246
                        }
247
                    } else {
248
                        $start = mb_strwidth($finalChunk[$index], Yii::$app->charset);
249
                    }
250 2
                    $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset);
251 2
                    $finalChunk[$index] .= $chunk;
252 2
                    if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
253 2
                        $arrayPointer[$index]++;
254 2
                        $finalChunk[$index] = '';
255
                    }
256
                } else {
257 9
                    $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset);
258
                }
259 9
                $chunk = $prefix . $chunk;
260 9
                $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1;
261 9
                $buffer .= $chunk;
262 9
                if ($repeat >= 0) {
263 9
                    $buffer .= str_repeat(' ', $repeat);
264
                }
265
            }
266 9
            $buffer .= "$spanRight\n";
267
        }
268
269 9
        return $buffer;
270
    }
271
272
    /**
273
     * Renders separator.
274
     *
275
     * @param string $spanLeft character for left border
276
     * @param string $spanMid character for middle border
277
     * @param string $spanMidMid character for middle-middle border
278
     * @param string $spanRight character for right border
279
     * @return string the generated separator row
280
     * @see \yii\console\widgets\Table::render()
281
     */
282 9
    protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
283
    {
284 9
        $separator = $spanLeft;
285 9
        foreach ($this->_columnWidths as $index => $rowSize) {
286 9
            if ($index !== 0) {
287 9
                $separator .= $spanMid;
288
            }
289 9
            $separator .= str_repeat($spanMidMid, $rowSize);
290
        }
291 9
        $separator .= $spanRight . "\n";
292 9
        return $separator;
293
    }
294
295
    /**
296
     * Calculate the size of rows to draw anchor of columns in console.
297
     *
298
     * @see \yii\console\widgets\Table::render()
299
     */
300 9
    protected function calculateRowsSize()
301
    {
302 9
        $this->_columnWidths = $columns = [];
303 9
        $totalWidth = 0;
304 9
        $screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET;
305
306 9
        for ($i = 0, $count = count($this->_headers); $i < $count; $i++) {
307 9
            $columns[] = ArrayHelper::getColumn($this->_rows, $i);
308 9
            $columns[$i][] = $this->_headers[$i];
309
        }
310
311 9
        foreach ($columns as $column) {
312 9
            $columnWidth = max(array_map(function ($val) {
313 9
                if (is_array($val)) {
314 2
                    $encodings = array_fill(0, count($val), Yii::$app->charset);
315 2
                    return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->_listPrefix, Yii::$app->charset);
316
                }
317
318 9
                return mb_strwidth($val, Yii::$app->charset);
319 9
            }, $column)) + 2;
320 9
            $this->_columnWidths[] = $columnWidth;
321 9
            $totalWidth += $columnWidth;
322
        }
323
324 9
        $relativeWidth = $screenWidth / $totalWidth;
325
326 9
        if ($totalWidth > $screenWidth) {
327
            foreach ($this->_columnWidths as $j => $width) {
0 ignored issues
show
Bug introduced by
The expression $this->_columnWidths of type integer|double|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
328
                $this->_columnWidths[$j] = (int) ($width * $relativeWidth);
329
                if ($j === count($this->_columnWidths)) {
330
                    $this->_columnWidths = $totalWidth;
0 ignored issues
show
Documentation Bug introduced by
It seems like $totalWidth of type integer or double is incompatible with the declared type array of property $_columnWidths.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
331
                }
332
                $totalWidth -= $this->_columnWidths[$j];
333
            }
334
        }
335 9
    }
336
337
    /**
338
     * Calculate the height of a row.
339
     *
340
     * @param array $row
341
     * @return int maximum row per cell
342
     * @see \yii\console\widgets\Table::render()
343
     */
344
    protected function calculateRowHeight($row)
345
    {
346 9
        $rowsPerCell = array_map(function ($size, $columnWidth) {
347 9
            if (is_array($columnWidth)) {
348 2
                $rows = 0;
349 2
                foreach ($columnWidth as $width) {
350 2
                    $rows += ceil($width / ($size - 2));
351
                }
352
353 2
                return $rows;
354
            }
355
356 9
            return ceil($columnWidth / ($size - 2));
357
        }, $this->_columnWidths, array_map(function ($val) {
358 9
            if (is_array($val)) {
359 2
                $encodings = array_fill(0, count($val), Yii::$app->charset);
360 2
                return array_map('mb_strwidth', $val, $encodings);
361
            }
362
363 9
            return mb_strwidth($val, Yii::$app->charset);
364 9
        }, $row)
365
        );
366
367 9
        return max($rowsPerCell);
368
    }
369
370
    /**
371
     * Getting screen width.
372
     * If it is not able to determine screen width, default value `123` will be set.
373
     *
374
     * @return int screen width
375
     */
376 9
    protected function getScreenWidth()
377
    {
378 9
        if (!$this->_screenWidth) {
379
            $size = Console::getScreenSize();
380
            $this->_screenWidth = $size[0]
381
                ?? self::DEFAULT_CONSOLE_SCREEN_WIDTH + self::CONSOLE_SCROLLBAR_OFFSET;
382
        }
383 9
        return $this->_screenWidth;
384
    }
385
}
386