Completed
Push — 2.1 ( 28b26f...4d9204 )
by Alexander
10:53
created

Table::calculateRowHeight()   B

Complexity

Conditions 4
Paths 1

Size

Total Lines 22
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 22
ccs 13
cts 13
cp 1
rs 8.9197
c 0
b 0
f 0
cc 4
eloc 15
nc 1
nop 1
crap 4
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 6
    public function setHeaders(array $headers)
112
    {
113 6
        $this->_headers = $headers;
114 6
        return $this;
115
    }
116
117
    /**
118
     * Set table rows
119
     *
120
     * @param array $rows table rows
121
     * @return $this
122
     */
123 6
    public function setRows(array $rows)
124
    {
125 6
        $this->_rows = $rows;
126 6
        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 6
    public function setScreenWidth($width)
148
    {
149 6
        $this->_screenWidth = $width;
150 6
        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 6
    public function run()
169
    {
170 6
        $this->calculateRowsSize();
171 6
        $buffer = $this->renderSeparator(
172 6
            $this->_chars[self::CHAR_TOP_LEFT],
173 6
            $this->_chars[self::CHAR_TOP_MID],
174 6
            $this->_chars[self::CHAR_TOP],
175 6
            $this->_chars[self::CHAR_TOP_RIGHT]
176
        );
177
        // Header
178 6
        $buffer .= $this->renderRow($this->_headers,
179 6
            $this->_chars[self::CHAR_LEFT],
180 6
            $this->_chars[self::CHAR_MIDDLE],
181 6
            $this->_chars[self::CHAR_RIGHT]
182
        );
183
184
        // Content
185 6
        foreach ($this->_rows as $row) {
186 6
            $buffer .= $this->renderSeparator(
187 6
                $this->_chars[self::CHAR_LEFT_MID],
188 6
                $this->_chars[self::CHAR_MID_MID],
189 6
                $this->_chars[self::CHAR_MID],
190 6
                $this->_chars[self::CHAR_RIGHT_MID]
191
            );
192 6
            $buffer .= $this->renderRow($row,
193 6
                $this->_chars[self::CHAR_LEFT],
194 6
                $this->_chars[self::CHAR_MIDDLE],
195 6
                $this->_chars[self::CHAR_RIGHT]);
196
        }
197
198 6
        $buffer .= $this->renderSeparator(
199 6
            $this->_chars[self::CHAR_BOTTOM_LEFT],
200 6
            $this->_chars[self::CHAR_BOTTOM_MID],
201 6
            $this->_chars[self::CHAR_BOTTOM],
202 6
            $this->_chars[self::CHAR_BOTTOM_RIGHT]
203
        );
204
205 6
        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 6
    protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
219
    {
220 6
        $size = $this->_columnWidths;
221
222 6
        $buffer = '';
223 6
        $arrayPointer = [];
224 6
        for ($i = 0, $max = $this->calculateRowHeight($row); $i < $max; $i++) {
225 6
            $buffer .= $spanLeft . ' ';
226 6
            foreach ($row as $index => $cell) {
227 6
                $prefix = '';
228 6
                if ($index !== 0) {
229 6
                    $buffer .= $spanMiddle . ' ';
230
                }
231 6
                if (is_array($cell)) {
232 2
                    if (empty($finalChunk[$index])) {
233 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...
234 2
                        $start = 0;
235 2
                        $prefix = $this->_listPrefix;
236 2
                        if (!isset($arrayPointer[$index])) {
237 2
                            $arrayPointer[$index] = 0;
238
                        }
239
                    } else {
240
                        $start = mb_strwidth($finalChunk[$index], Yii::$app->charset);
241
                    }
242 2
                    $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $size[$index] - 4, Yii::$app->charset);
243 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...
244 2
                    if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
245 2
                        $arrayPointer[$index]++;
246 2
                        $finalChunk[$index] = '';
247
                    }
248
                } else {
249 6
                    $chunk = mb_substr($cell, ($size[$index] * $i) - ($i * 2), $size[$index] - 2, Yii::$app->charset);
250
                }
251 6
                $chunk = $prefix . $chunk;
252 6
                $repeat = $size[$index] - mb_strwidth($chunk, Yii::$app->charset) - 1;
253 6
                $buffer .= $chunk;
254 6
                if ($repeat >= 0) {
255 6
                    $buffer .= str_repeat(' ', $repeat);
256
                }
257
            }
258 6
            $buffer .= "$spanRight\n";
259
        }
260
261 6
        return $buffer;
262
    }
263
264
    /**
265
     * Renders separator
266
     *
267
     * @param string $spanLeft character for left border
268
     * @param string $spanMid character for middle border
269
     * @param string $spanMidMid character for middle-middle border
270
     * @param string $spanRight character for right border
271
     * @return string the generated separator row
272
     * @see \yii\console\widgets\Table::render()
273
     */
274 6
    protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
275
    {
276 6
        $separator = $spanLeft;
277 6
        foreach ($this->_columnWidths as $index => $rowSize) {
278 6
            if ($index !== 0) {
279 6
                $separator .= $spanMid;
280
            }
281 6
            $separator .= str_repeat($spanMidMid, $rowSize);
282
        }
283 6
        $separator .= $spanRight . "\n";
284 6
        return $separator;
285
    }
286
287
    /**
288
     * Calculate the size of rows to draw anchor of columns in console
289
     *
290
     * @see \yii\console\widgets\Table::render()
291
     */
292 6
    protected function calculateRowsSize()
293
    {
294 6
        $this->_columnWidths = $columns = [];
295 6
        $totalWidth = 0;
296 6
        $screenWidth = $this->getScreenWidth() - 3;
297
298 6
        for ($i = 0, $count = count($this->_headers); $i < $count; $i++) {
299 6
            $columns[] = ArrayHelper::getColumn($this->_rows, $i);
300 6
            $columns[$i][] = $this->_headers[$i];
301
        }
302
303 6
        foreach ($columns as $column) {
304 6
            $columnWidth = max(array_map(function ($val) {
305 6
                if (is_array($val)) {
306 2
                    $encodings = array_fill(0, count($val), Yii::$app->charset);
307 2
                    return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->_listPrefix, Yii::$app->charset);
308
                }
309 6
                return mb_strwidth($val, Yii::$app->charset);
310 6
            }, $column)) + 2;
311 6
            $this->_columnWidths[] = $columnWidth;
312 6
            $totalWidth += $columnWidth;
313
        }
314
315 6
        $relativeWidth = $screenWidth / $totalWidth;
316
317 6
        if ($totalWidth > $screenWidth) {
318
            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...
319
                $this->_columnWidths[$j] = (int) ($width * $relativeWidth);
320
                if ($j === count($this->_columnWidths)) {
321
                    $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...
322
                }
323
                $totalWidth -= $this->_columnWidths[$j];
324
            }
325
        }
326 6
    }
327
328
    /**
329
     * Calculate the height of a row
330
     *
331
     * @param array $row
332
     * @return int maximum row per cell
333
     * @see \yii\console\widgets\Table::render()
334
     */
335
    protected function calculateRowHeight($row)
336
    {
337 6
        $rowsPerCell = array_map(function ($size, $columnWidth) {
338 6
            if (is_array($columnWidth)) {
339 2
                $rows = 0;
340 2
                foreach ($columnWidth as $width) {
341 2
                    $rows += ceil($width / ($size - 2));
342
                }
343 2
                return $rows;
344
            }
345 6
            return ceil($columnWidth / ($size - 2));
346
        }, $this->_columnWidths, array_map(function ($val) {
347 6
            if (is_array($val)) {
348 2
                $encodings = array_fill(0, count($val), Yii::$app->charset);
349 2
                return array_map('mb_strwidth', $val, $encodings);
350
            }
351 6
            return mb_strwidth($val, Yii::$app->charset);
352 6
        }, $row)
353
        );
354
355 6
        return max($rowsPerCell);
356
    }
357
358
    /**
359
     * Getting screen width
360
     *
361
     * @return int screen width
362
     */
363 6
    protected function getScreenWidth()
364
    {
365 6
        if (!$this->_screenWidth) {
366
            $size = Console::getScreenSize();
367
            if (isset($size[0])) {
368
                $this->_screenWidth = $size[0];
369
            }
370
        }
371 6
        return $this->_screenWidth;
372
    }
373
}
374