Completed
Pull Request — 2.1 (#15718)
by Alex
17:00
created

Table   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.54%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 4
dl 0
loc 345
rs 8.8
c 0
b 0
f 0
ccs 124
cts 134
cp 0.9254

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setHeaders() 0 5 1
A setRows() 0 5 1
A setChars() 0 5 1
A setScreenWidth() 0 5 1
A setListPrefix() 0 5 1
B run() 0 42 2
D renderRow() 0 47 12
A renderSeparator() 0 12 3
C calculateRowsSize() 0 36 7
B calculateRowHeight() 0 28 4
A getScreenWidth() 0 10 3
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
54
    const CHAR_TOP = 'top';
55
    const CHAR_TOP_MID = 'top-mid';
56
    const CHAR_TOP_LEFT = 'top-left';
57
    const CHAR_TOP_RIGHT = 'top-right';
58
    const CHAR_BOTTOM = 'bottom';
59
    const CHAR_BOTTOM_MID = 'bottom-mid';
60
    const CHAR_BOTTOM_LEFT = 'bottom-left';
61
    const CHAR_BOTTOM_RIGHT = 'bottom-right';
62
    const CHAR_LEFT = 'left';
63
    const CHAR_LEFT_MID = 'left-mid';
64
    const CHAR_MID = 'mid';
65
    const CHAR_MID_MID = 'mid-mid';
66
    const CHAR_RIGHT = 'right';
67
    const CHAR_RIGHT_MID = 'right-mid';
68
    const CHAR_MIDDLE = 'middle';
69
70
    /**
71
     * @var array table headers
72
     */
73
    private $_headers = [];
74
    /**
75
     * @var array table rows
76
     */
77
    private $_rows = [];
78
    /**
79
     * @var array table chars
80
     */
81
    private $_chars = [
82
        self::CHAR_TOP => '═',
83
        self::CHAR_TOP_MID => '╤',
84
        self::CHAR_TOP_LEFT => '╔',
85
        self::CHAR_TOP_RIGHT => '╗',
86
        self::CHAR_BOTTOM => '═',
87
        self::CHAR_BOTTOM_MID => '╧',
88
        self::CHAR_BOTTOM_LEFT => '╚',
89
        self::CHAR_BOTTOM_RIGHT => '╝',
90
        self::CHAR_LEFT => '║',
91
        self::CHAR_LEFT_MID => '╟',
92
        self::CHAR_MID => '─',
93
        self::CHAR_MID_MID => '┼',
94
        self::CHAR_RIGHT => '║',
95
        self::CHAR_RIGHT_MID => '╢',
96
        self::CHAR_MIDDLE => '│',
97
    ];
98
    /**
99
     * @var array table column widths
100
     */
101
    private $_columnWidths = [];
102
    /**
103
     * @var int screen width
104
     */
105
    private $_screenWidth;
106
    /**
107
     * @var string list prefix
108
     */
109
    private $_listPrefix = '• ';
110
111
112
    /**
113
     * Set table headers.
114
     *
115
     * @param array $headers table headers
116
     * @return $this
117
     */
118 9
    public function setHeaders(array $headers)
119
    {
120 9
        $this->_headers = array_values($headers);
121 9
        return $this;
122
    }
123
124
    /**
125
     * Set table rows.
126
     *
127
     * @param array $rows table rows
128
     * @return $this
129
     */
130 9
    public function setRows(array $rows)
131
    {
132 9
        $this->_rows = array_map('array_values', $rows);
133 9
        return $this;
134
    }
135
136
    /**
137
     * Set table chars.
138
     *
139
     * @param array $chars table chars
140
     * @return $this
141
     */
142 1
    public function setChars(array $chars)
143
    {
144 1
        $this->_chars = $chars;
145 1
        return $this;
146
    }
147
148
    /**
149
     * Set screen width.
150
     *
151
     * @param int $width screen width
152
     * @return $this
153
     */
154 9
    public function setScreenWidth($width)
155
    {
156 9
        $this->_screenWidth = $width;
157 9
        return $this;
158
    }
159
160
    /**
161
     * Set list prefix.
162
     *
163
     * @param string $listPrefix list prefix
164
     * @return $this
165
     */
166 1
    public function setListPrefix($listPrefix)
167
    {
168 1
        $this->_listPrefix = $listPrefix;
169 1
        return $this;
170
    }
171
172
    /**
173
     * @return string the rendered table
174
     */
175 9
    public function run()
176
    {
177 9
        $this->calculateRowsSize();
178 9
        $buffer = $this->renderSeparator(
179 9
            $this->_chars[self::CHAR_TOP_LEFT],
180 9
            $this->_chars[self::CHAR_TOP_MID],
181 9
            $this->_chars[self::CHAR_TOP],
182 9
            $this->_chars[self::CHAR_TOP_RIGHT]
183
        );
184
        // Header
185 9
        $buffer .= $this->renderRow(
186 9
            $this->_headers,
187 9
            $this->_chars[self::CHAR_LEFT],
188 9
            $this->_chars[self::CHAR_MIDDLE],
189 9
            $this->_chars[self::CHAR_RIGHT]
190
        );
191
192
        // Content
193 9
        foreach ($this->_rows as $row) {
194 9
            $buffer .= $this->renderSeparator(
195 9
                $this->_chars[self::CHAR_LEFT_MID],
196 9
                $this->_chars[self::CHAR_MID_MID],
197 9
                $this->_chars[self::CHAR_MID],
198 9
                $this->_chars[self::CHAR_RIGHT_MID]
199
            );
200 9
            $buffer .= $this->renderRow(
201 9
                $row,
202 9
                $this->_chars[self::CHAR_LEFT],
203 9
                $this->_chars[self::CHAR_MIDDLE],
204 9
                $this->_chars[self::CHAR_RIGHT]
205
            );
206
        }
207
208 9
        $buffer .= $this->renderSeparator(
209 9
            $this->_chars[self::CHAR_BOTTOM_LEFT],
210 9
            $this->_chars[self::CHAR_BOTTOM_MID],
211 9
            $this->_chars[self::CHAR_BOTTOM],
212 9
            $this->_chars[self::CHAR_BOTTOM_RIGHT]
213
        );
214
215 9
        return $buffer;
216
    }
217
218
    /**
219
     * Renders a row of data into a string.
220
     *
221
     * @param array $row row of data
222
     * @param string $spanLeft character for left border
223
     * @param string $spanMiddle character for middle border
224
     * @param string $spanRight character for right border
225
     * @return string
226
     * @see \yii\console\widgets\Table::render()
227
     */
228 9
    protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
229
    {
230 9
        $size = $this->_columnWidths;
231
232 9
        $buffer = '';
233 9
        $arrayPointer = [];
234 9
        $finalChunk = [];
235 9
        for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
236 9
            $buffer .= $spanLeft . ' ';
237 9
            foreach ($size as $index => $cellSize) {
238 9
                $cell = isset($row[$index]) ? $row[$index] : null;
239 9
                $prefix = '';
240 9
                if ($index !== 0) {
241 9
                    $buffer .= $spanMiddle . ' ';
242
                }
243 9
                if (is_array($cell)) {
244 2
                    if (empty($finalChunk[$index])) {
245 2
                        $finalChunk[$index] = '';
246 2
                        $start = 0;
247 2
                        $prefix = $this->_listPrefix;
248 2
                        if (!isset($arrayPointer[$index])) {
249 2
                            $arrayPointer[$index] = 0;
250
                        }
251
                    } else {
252
                        $start = mb_strwidth($finalChunk[$index], Yii::$app->charset);
253
                    }
254 2
                    $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset);
255 2
                    $finalChunk[$index] .= $chunk;
256 2
                    if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
257 2
                        $arrayPointer[$index]++;
258 2
                        $finalChunk[$index] = '';
259
                    }
260
                } else {
261 9
                    $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset);
262
                }
263 9
                $chunk = $prefix . $chunk;
264 9
                $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1;
265 9
                $buffer .= $chunk;
266 9
                if ($repeat >= 0) {
267 9
                    $buffer .= str_repeat(' ', $repeat);
268
                }
269
            }
270 9
            $buffer .= "$spanRight\n";
271
        }
272
273 9
        return $buffer;
274
    }
275
276
    /**
277
     * Renders separator.
278
     *
279
     * @param string $spanLeft character for left border
280
     * @param string $spanMid character for middle border
281
     * @param string $spanMidMid character for middle-middle border
282
     * @param string $spanRight character for right border
283
     * @return string the generated separator row
284
     * @see \yii\console\widgets\Table::render()
285
     */
286 9
    protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
287
    {
288 9
        $separator = $spanLeft;
289 9
        foreach ($this->_columnWidths as $index => $rowSize) {
290 9
            if ($index !== 0) {
291 9
                $separator .= $spanMid;
292
            }
293 9
            $separator .= str_repeat($spanMidMid, $rowSize);
294
        }
295 9
        $separator .= $spanRight . "\n";
296 9
        return $separator;
297
    }
298
299
    /**
300
     * Calculate the size of rows to draw anchor of columns in console.
301
     *
302
     * @see \yii\console\widgets\Table::render()
303
     */
304 9
    protected function calculateRowsSize()
305
    {
306 9
        $this->_columnWidths = $columns = [];
307 9
        $totalWidth = 0;
308 9
        $screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET;
309
310 9
        for ($i = 0, $count = count($this->_headers); $i < $count; $i++) {
311 9
            $columns[] = ArrayHelper::getColumn($this->_rows, $i);
312 9
            $columns[$i][] = $this->_headers[$i];
313
        }
314
315 9
        foreach ($columns as $column) {
316 9
            $columnWidth = max(array_map(function ($val) {
317 9
                if (is_array($val)) {
318 2
                    $encodings = array_fill(0, count($val), Yii::$app->charset);
319 2
                    return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->_listPrefix, Yii::$app->charset);
320
                }
321
322 9
                return mb_strwidth($val, Yii::$app->charset);
323 9
            }, $column)) + 2;
324 9
            $this->_columnWidths[] = $columnWidth;
325 9
            $totalWidth += $columnWidth;
326
        }
327
328 9
        $relativeWidth = $screenWidth / $totalWidth;
329
330 9
        if ($totalWidth > $screenWidth) {
331
            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...
332
                $this->_columnWidths[$j] = (int) ($width * $relativeWidth);
333
                if ($j === count($this->_columnWidths)) {
334
                    $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...
335
                }
336
                $totalWidth -= $this->_columnWidths[$j];
337
            }
338
        }
339 9
    }
340
341
    /**
342
     * Calculate the height of a row.
343
     *
344
     * @param array $row
345
     * @return int maximum row per cell
346
     * @see \yii\console\widgets\Table::render()
347
     */
348 9
    protected function calculateRowHeight($row)
349
    {
350 9
        $rowsPerCell = array_map(
351 9
            function ($size, $columnWidth) {
352 9
                if (is_array($columnWidth)) {
353 2
                    $rows = 0;
354 2
                    foreach ($columnWidth as $width) {
355 2
                        $rows += ceil($width / ($size - 2));
356
                    }
357
358 2
                    return $rows;
359
                }
360
361 9
                return ceil($columnWidth / ($size - 2));
362 9
            },
363 9
            $this->_columnWidths,
364 9
            array_map(function ($val) {
365 9
                if (is_array($val)) {
366 2
                    $encodings = array_fill(0, count($val), Yii::$app->charset);
367 2
                    return array_map('mb_strwidth', $val, $encodings);
368
                }
369
370 9
                return mb_strwidth($val, Yii::$app->charset);
371 9
            }, $row)
372
        );
373
374 9
        return max($rowsPerCell);
375
    }
376
377
    /**
378
     * Getting screen width.
379
     * If it is not able to determine screen width, default value `123` will be set.
380
     *
381
     * @return int screen width
382
     */
383 9
    protected function getScreenWidth()
384
    {
385 9
        if (!$this->_screenWidth) {
386
            $size = Console::getScreenSize();
387
            $this->_screenWidth = isset($size[0])
388
                ? $size[0]
389
                : self::DEFAULT_CONSOLE_SCREEN_WIDTH + self::CONSOLE_SCROLLBAR_OFFSET;
390
        }
391 9
        return $this->_screenWidth;
392
    }
393
}
394