Completed
Push — 2.1 ( bf116e...646cd7 )
by Alexander
09:33
created

Table   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 339
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.13%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 4
dl 0
loc 339
ccs 117
cts 127
cp 0.9213
rs 8.8
c 0
b 0
f 0

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 39 2
D renderRow() 0 47 12
A renderSeparator() 0 12 3
A getScreenWidth() 0 10 3
B calculateRowHeight() 0 25 4
C calculateRowsSize() 0 36 7
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 8
    public function setHeaders(array $headers)
119
    {
120 8
        $this->_headers = $headers;
121 8
        return $this;
122
    }
123
124
    /**
125
     * Set table rows.
126
     *
127
     * @param array $rows table rows
128
     * @return $this
129
     */
130 8
    public function setRows(array $rows)
131
    {
132 8
        $this->_rows = $rows;
133 8
        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 8
    public function setScreenWidth($width)
155
    {
156 8
        $this->_screenWidth = $width;
157 8
        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 8
    public function run()
176
    {
177 8
        $this->calculateRowsSize();
178 8
        $buffer = $this->renderSeparator(
179 8
            $this->_chars[self::CHAR_TOP_LEFT],
180 8
            $this->_chars[self::CHAR_TOP_MID],
181 8
            $this->_chars[self::CHAR_TOP],
182 8
            $this->_chars[self::CHAR_TOP_RIGHT]
183
        );
184
        // Header
185 8
        $buffer .= $this->renderRow($this->_headers,
186 8
            $this->_chars[self::CHAR_LEFT],
187 8
            $this->_chars[self::CHAR_MIDDLE],
188 8
            $this->_chars[self::CHAR_RIGHT]
189
        );
190
191
        // Content
192 8
        foreach ($this->_rows as $row) {
193 8
            $buffer .= $this->renderSeparator(
194 8
                $this->_chars[self::CHAR_LEFT_MID],
195 8
                $this->_chars[self::CHAR_MID_MID],
196 8
                $this->_chars[self::CHAR_MID],
197 8
                $this->_chars[self::CHAR_RIGHT_MID]
198
            );
199 8
            $buffer .= $this->renderRow($row,
200 8
                $this->_chars[self::CHAR_LEFT],
201 8
                $this->_chars[self::CHAR_MIDDLE],
202 8
                $this->_chars[self::CHAR_RIGHT]);
203
        }
204
205 8
        $buffer .= $this->renderSeparator(
206 8
            $this->_chars[self::CHAR_BOTTOM_LEFT],
207 8
            $this->_chars[self::CHAR_BOTTOM_MID],
208 8
            $this->_chars[self::CHAR_BOTTOM],
209 8
            $this->_chars[self::CHAR_BOTTOM_RIGHT]
210
        );
211
212 8
        return $buffer;
213
    }
214
215
    /**
216
     * Renders a row of data into a string.
217
     *
218
     * @param array $row row of data
219
     * @param string $spanLeft character for left border
220
     * @param string $spanMiddle character for middle border
221
     * @param string $spanRight character for right border
222
     * @return string
223
     * @see \yii\console\widgets\Table::render()
224
     */
225 8
    protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
226
    {
227 8
        $size = $this->_columnWidths;
228
229 8
        $buffer = '';
230 8
        $arrayPointer = [];
231 8
        $finalChunk = [];
232 8
        for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
233 8
            $buffer .= $spanLeft . ' ';
234 8
            foreach ($size as $index => $cellSize) {
235 8
                $cell = isset($row[$index]) ? $row[$index] : null;
236 8
                $prefix = '';
237 8
                if ($index !== 0) {
238 8
                    $buffer .= $spanMiddle . ' ';
239
                }
240 8
                if (is_array($cell)) {
241 2
                    if (empty($finalChunk[$index])) {
242 2
                        $finalChunk[$index] = '';
243 2
                        $start = 0;
244 2
                        $prefix = $this->_listPrefix;
245 2
                        if (!isset($arrayPointer[$index])) {
246 2
                            $arrayPointer[$index] = 0;
247
                        }
248
                    } else {
249
                        $start = mb_strwidth($finalChunk[$index], Yii::$app->charset);
250
                    }
251 2
                    $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset);
252 2
                    $finalChunk[$index] .= $chunk;
253 2
                    if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
254 2
                        $arrayPointer[$index]++;
255 2
                        $finalChunk[$index] = '';
256
                    }
257
                } else {
258 8
                    $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset);
259
                }
260 8
                $chunk = $prefix . $chunk;
261 8
                $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1;
262 8
                $buffer .= $chunk;
263 8
                if ($repeat >= 0) {
264 8
                    $buffer .= str_repeat(' ', $repeat);
265
                }
266
            }
267 8
            $buffer .= "$spanRight\n";
268
        }
269
270 8
        return $buffer;
271
    }
272
273
    /**
274
     * Renders separator.
275
     *
276
     * @param string $spanLeft character for left border
277
     * @param string $spanMid character for middle border
278
     * @param string $spanMidMid character for middle-middle border
279
     * @param string $spanRight character for right border
280
     * @return string the generated separator row
281
     * @see \yii\console\widgets\Table::render()
282
     */
283 8
    protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
284
    {
285 8
        $separator = $spanLeft;
286 8
        foreach ($this->_columnWidths as $index => $rowSize) {
287 8
            if ($index !== 0) {
288 8
                $separator .= $spanMid;
289
            }
290 8
            $separator .= str_repeat($spanMidMid, $rowSize);
291
        }
292 8
        $separator .= $spanRight . "\n";
293 8
        return $separator;
294
    }
295
296
    /**
297
     * Calculate the size of rows to draw anchor of columns in console.
298
     *
299
     * @see \yii\console\widgets\Table::render()
300
     */
301 8
    protected function calculateRowsSize()
302
    {
303 8
        $this->_columnWidths = $columns = [];
304 8
        $totalWidth = 0;
305 8
        $screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET;
306
307 8
        for ($i = 0, $count = count($this->_headers); $i < $count; $i++) {
308 8
            $columns[] = ArrayHelper::getColumn($this->_rows, $i);
309 8
            $columns[$i][] = $this->_headers[$i];
310
        }
311
312 8
        foreach ($columns as $column) {
313 8
            $columnWidth = max(array_map(function ($val) {
314 8
                if (is_array($val)) {
315 2
                    $encodings = array_fill(0, count($val), Yii::$app->charset);
316 2
                    return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->_listPrefix, Yii::$app->charset);
317
                }
318
319 8
                return mb_strwidth($val, Yii::$app->charset);
320 8
            }, $column)) + 2;
321 8
            $this->_columnWidths[] = $columnWidth;
322 8
            $totalWidth += $columnWidth;
323
        }
324
325 8
        $relativeWidth = $screenWidth / $totalWidth;
326
327 8
        if ($totalWidth > $screenWidth) {
328
            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...
329
                $this->_columnWidths[$j] = (int) ($width * $relativeWidth);
330
                if ($j === count($this->_columnWidths)) {
331
                    $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...
332
                }
333
                $totalWidth -= $this->_columnWidths[$j];
334
            }
335
        }
336 8
    }
337
338
    /**
339
     * Calculate the height of a row.
340
     *
341
     * @param array $row
342
     * @return int maximum row per cell
343
     * @see \yii\console\widgets\Table::render()
344
     */
345
    protected function calculateRowHeight($row)
346
    {
347 8
        $rowsPerCell = array_map(function ($size, $columnWidth) {
348 8
            if (is_array($columnWidth)) {
349 2
                $rows = 0;
350 2
                foreach ($columnWidth as $width) {
351 2
                    $rows += ceil($width / ($size - 2));
352
                }
353
354 2
                return $rows;
355
            }
356
357 8
            return ceil($columnWidth / ($size - 2));
358
        }, $this->_columnWidths, array_map(function ($val) {
359 8
            if (is_array($val)) {
360 2
                $encodings = array_fill(0, count($val), Yii::$app->charset);
361 2
                return array_map('mb_strwidth', $val, $encodings);
362
            }
363
364 8
            return mb_strwidth($val, Yii::$app->charset);
365 8
        }, $row)
366
        );
367
368 8
        return max($rowsPerCell);
369
    }
370
371
    /**
372
     * Getting screen width.
373
     * If it is not able to determine screen width, default value `123` will be set.
374
     *
375
     * @return int screen width
376
     */
377 8
    protected function getScreenWidth()
378
    {
379 8
        if (!$this->_screenWidth) {
380
            $size = Console::getScreenSize();
381
            $this->_screenWidth = isset($size[0])
382
                ? $size[0]
383
                : self::DEFAULT_CONSOLE_SCREEN_WIDTH + self::CONSOLE_SCROLLBAR_OFFSET;
384
        }
385 8
        return $this->_screenWidth;
386
    }
387
}
388