Completed
Push — 2.1-master-merge ( 240673 )
by Alexander
13:45
created

Table   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 92.8%

Importance

Changes 0
Metric Value
wmc 36
lcom 1
cbo 4
dl 0
loc 334
ccs 116
cts 125
cp 0.928
rs 8.8
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A renderSeparator() 0 12 3
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 46 12
A getScreenWidth() 0 11 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
 * @author Daniel Gomez Pan <[email protected]>
44
 * @since 2.0.13
45
 */
46
class Table extends Widget
47
{
48
    const CHAR_TOP = 'top';
49
    const CHAR_TOP_MID = 'top-mid';
50
    const CHAR_TOP_LEFT = 'top-left';
51
    const CHAR_TOP_RIGHT = 'top-right';
52
    const CHAR_BOTTOM = 'bottom';
53
    const CHAR_BOTTOM_MID = 'bottom-mid';
54
    const CHAR_BOTTOM_LEFT = 'bottom-left';
55
    const CHAR_BOTTOM_RIGHT = 'bottom-right';
56
    const CHAR_LEFT = 'left';
57
    const CHAR_LEFT_MID = 'left-mid';
58
    const CHAR_MID = 'mid';
59
    const CHAR_MID_MID = 'mid-mid';
60
    const CHAR_RIGHT = 'right';
61
    const CHAR_RIGHT_MID = 'right-mid';
62
    const CHAR_MIDDLE = 'middle';
63
64
    /**
65
     * @var array table headers
66
     */
67
    private $_headers = [];
68
    /**
69
     * @var array table rows
70
     */
71
    private $_rows = [];
72
    /**
73
     * @var array table chars
74
     */
75
    private $_chars = [
76
        self::CHAR_TOP => '═',
77
        self::CHAR_TOP_MID => '╤',
78
        self::CHAR_TOP_LEFT => '╔',
79
        self::CHAR_TOP_RIGHT => '╗',
80
        self::CHAR_BOTTOM => '═',
81
        self::CHAR_BOTTOM_MID => '╧',
82
        self::CHAR_BOTTOM_LEFT => '╚',
83
        self::CHAR_BOTTOM_RIGHT => '╝',
84
        self::CHAR_LEFT => '║',
85
        self::CHAR_LEFT_MID => '╟',
86
        self::CHAR_MID => '─',
87
        self::CHAR_MID_MID => '┼',
88
        self::CHAR_RIGHT => '║',
89
        self::CHAR_RIGHT_MID => '╢',
90
        self::CHAR_MIDDLE => '│',
91
    ];
92
    /**
93
     * @var array table column widths
94
     */
95
    private $_columnWidths = [];
96
    /**
97
     * @var int screen width
98
     */
99
    private $_screenWidth;
100
    /**
101
     * @var string list prefix
102
     */
103
    private $_listPrefix = '• ';
104
105
    /**
106
     * Set table headers.
107
     *
108
     * @param array $headers table headers
109
     * @return $this
110
     */
111 8
    public function setHeaders(array $headers)
112
    {
113 8
        $this->_headers = $headers;
114 8
        return $this;
115
    }
116
117
    /**
118
     * Set table rows.
119
     *
120
     * @param array $rows table rows
121
     * @return $this
122
     */
123 8
    public function setRows(array $rows)
124
    {
125 8
        $this->_rows = $rows;
126 8
        return $this;
127
    }
128
129
    /**
130
     * Set table chars.
131
     *
132
     * @param array $chars table chars
133
     * @return $this
134
     */
135 1
    public function setChars(array $chars)
136
    {
137 1
        $this->_chars = $chars;
138 1
        return $this;
139
    }
140
141
    /**
142
     * Set screen width.
143
     *
144
     * @param int $width screen width
145
     * @return $this
146
     */
147 8
    public function setScreenWidth($width)
148
    {
149 8
        $this->_screenWidth = $width;
150 8
        return $this;
151
    }
152
153
    /**
154
     * Set list prefix.
155
     *
156
     * @param string $listPrefix list prefix
157
     * @return $this
158
     */
159 1
    public function setListPrefix($listPrefix)
160
    {
161 1
        $this->_listPrefix = $listPrefix;
162 1
        return $this;
163
    }
164
165
    /**
166
     * @return string the rendered table
167
     */
168 8
    public function run()
169
    {
170 8
        $this->calculateRowsSize();
171 8
        $buffer = $this->renderSeparator(
172 8
            $this->_chars[self::CHAR_TOP_LEFT],
173 8
            $this->_chars[self::CHAR_TOP_MID],
174 8
            $this->_chars[self::CHAR_TOP],
175 8
            $this->_chars[self::CHAR_TOP_RIGHT]
176
        );
177
        // Header
178 8
        $buffer .= $this->renderRow($this->_headers,
179 8
            $this->_chars[self::CHAR_LEFT],
180 8
            $this->_chars[self::CHAR_MIDDLE],
181 8
            $this->_chars[self::CHAR_RIGHT]
182
        );
183
184
        // Content
185 8
        foreach ($this->_rows as $row) {
186 8
            $buffer .= $this->renderSeparator(
187 8
                $this->_chars[self::CHAR_LEFT_MID],
188 8
                $this->_chars[self::CHAR_MID_MID],
189 8
                $this->_chars[self::CHAR_MID],
190 8
                $this->_chars[self::CHAR_RIGHT_MID]
191
            );
192 8
            $buffer .= $this->renderRow($row,
193 8
                $this->_chars[self::CHAR_LEFT],
194 8
                $this->_chars[self::CHAR_MIDDLE],
195 8
                $this->_chars[self::CHAR_RIGHT]);
196
        }
197
198 8
        $buffer .= $this->renderSeparator(
199 8
            $this->_chars[self::CHAR_BOTTOM_LEFT],
200 8
            $this->_chars[self::CHAR_BOTTOM_MID],
201 8
            $this->_chars[self::CHAR_BOTTOM],
202 8
            $this->_chars[self::CHAR_BOTTOM_RIGHT]
203
        );
204
205 8
        return $buffer;
206
    }
207
208
    /**
209
     * Renders a row of data into a string.
210
     *
211
     * @param array $row row of data
212
     * @param string $spanLeft character for left border
213
     * @param string $spanMiddle character for middle border
214
     * @param string $spanRight character for right border
215
     * @return string
216
     * @see \yii\console\widgets\Table::render()
217
     */
218 8
    protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
219
    {
220 8
        $size = $this->_columnWidths;
221
222 8
        $buffer = '';
223 8
        $arrayPointer = [];
224 8
        for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
225 8
            $buffer .= $spanLeft . ' ';
226 8
            foreach ($size as $index => $cellSize) {
227 8
                $cell = isset($row[$index]) ? $row[$index] : null;
228 8
                $prefix = '';
229 8
                if ($index !== 0) {
230 8
                    $buffer .= $spanMiddle . ' ';
231
                }
232 8
                if (is_array($cell)) {
233 2
                    if (empty($finalChunk[$index])) {
234 2
                        $finalChunk[$index] = '';
0 ignored issues
show
Coding Style Comprehensibility introduced by
$finalChunk was never initialized. Although not strictly required by PHP, it is generally a good practice to add $finalChunk = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
235 2
                        $start = 0;
236 2
                        $prefix = $this->_listPrefix;
237 2
                        if (!isset($arrayPointer[$index])) {
238 2
                            $arrayPointer[$index] = 0;
239
                        }
240
                    } else {
241
                        $start = mb_strwidth($finalChunk[$index], Yii::$app->charset);
242
                    }
243 2
                    $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset);
244 2
                    $finalChunk[$index] .= $chunk;
0 ignored issues
show
Bug introduced by
The variable $finalChunk does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
245 2
                    if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
246 2
                        $arrayPointer[$index]++;
247 2
                        $finalChunk[$index] = '';
248
                    }
249
                } else {
250 8
                    $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset);
251
                }
252 8
                $chunk = $prefix . $chunk;
253 8
                $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1;
254 8
                $buffer .= $chunk;
255 8
                if ($repeat >= 0) {
256 8
                    $buffer .= str_repeat(' ', $repeat);
257
                }
258
            }
259 8
            $buffer .= "$spanRight\n";
260
        }
261
262 8
        return $buffer;
263
    }
264
265
    /**
266
     * Renders separator.
267
     *
268
     * @param string $spanLeft character for left border
269
     * @param string $spanMid character for middle border
270
     * @param string $spanMidMid character for middle-middle border
271
     * @param string $spanRight character for right border
272
     * @return string the generated separator row
273
     * @see \yii\console\widgets\Table::render()
274
     */
275 8
    protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
276
    {
277 8
        $separator = $spanLeft;
278 8
        foreach ($this->_columnWidths as $index => $rowSize) {
279 8
            if ($index !== 0) {
280 8
                $separator .= $spanMid;
281
            }
282 8
            $separator .= str_repeat($spanMidMid, $rowSize);
283
        }
284 8
        $separator .= $spanRight . "\n";
285 8
        return $separator;
286
    }
287
288
    /**
289
     * Calculate the size of rows to draw anchor of columns in console.
290
     *
291
     * @see \yii\console\widgets\Table::render()
292
     */
293 8
    protected function calculateRowsSize()
294
    {
295 8
        $this->_columnWidths = $columns = [];
296 8
        $totalWidth = 0;
297 8
        $screenWidth = $this->getScreenWidth() - 3;
298
299 8
        for ($i = 0, $count = count($this->_headers); $i < $count; $i++) {
300 8
            $columns[] = ArrayHelper::getColumn($this->_rows, $i);
301 8
            $columns[$i][] = $this->_headers[$i];
302
        }
303
304 8
        foreach ($columns as $column) {
305 8
            $columnWidth = max(array_map(function ($val) {
306 8
                if (is_array($val)) {
307 2
                    $encodings = array_fill(0, count($val), Yii::$app->charset);
308 2
                    return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->_listPrefix, Yii::$app->charset);
309
                }
310
311 8
                return mb_strwidth($val, Yii::$app->charset);
312 8
            }, $column)) + 2;
313 8
            $this->_columnWidths[] = $columnWidth;
314 8
            $totalWidth += $columnWidth;
315
        }
316
317 8
        $relativeWidth = $screenWidth / $totalWidth;
318
319 8
        if ($totalWidth > $screenWidth) {
320
            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...
321
                $this->_columnWidths[$j] = (int) ($width * $relativeWidth);
322
                if ($j === count($this->_columnWidths)) {
323
                    $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...
324
                }
325
                $totalWidth -= $this->_columnWidths[$j];
326
            }
327
        }
328 8
    }
329
330
    /**
331
     * Calculate the height of a row.
332
     *
333
     * @param array $row
334
     * @return int maximum row per cell
335
     * @see \yii\console\widgets\Table::render()
336
     */
337
    protected function calculateRowHeight($row)
338
    {
339 8
        $rowsPerCell = array_map(function ($size, $columnWidth) {
340 8
            if (is_array($columnWidth)) {
341 2
                $rows = 0;
342 2
                foreach ($columnWidth as $width) {
343 2
                    $rows += ceil($width / ($size - 2));
344
                }
345
346 2
                return $rows;
347
            }
348
349 8
            return ceil($columnWidth / ($size - 2));
350
        }, $this->_columnWidths, array_map(function ($val) {
351 8
            if (is_array($val)) {
352 2
                $encodings = array_fill(0, count($val), Yii::$app->charset);
353 2
                return array_map('mb_strwidth', $val, $encodings);
354
            }
355
356 8
            return mb_strwidth($val, Yii::$app->charset);
357 8
        }, $row)
358
        );
359
360 8
        return max($rowsPerCell);
361
    }
362
363
    /**
364
     * Getting screen width.
365
     *
366
     * @return int screen width
367
     */
368 8
    protected function getScreenWidth()
369
    {
370 8
        if (!$this->_screenWidth) {
371
            $size = Console::getScreenSize();
372
            if (isset($size[0])) {
373
                $this->_screenWidth = $size[0];
374
            }
375
        }
376
377 8
        return $this->_screenWidth;
378
    }
379
}
380