Passed
Push — master ( 76d634...1a0e91 )
by Alexander
08:33
created

Table   C

Complexity

Total Complexity 53

Size/Duplication

Total Lines 382
Duplicated Lines 0 %

Test Coverage

Coverage 99.36%

Importance

Changes 0
Metric Value
eloc 190
dl 0
loc 382
ccs 155
cts 156
cp 0.9936
rs 6.96
c 0
b 0
f 0
wmc 53

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setScreenWidth() 0 4 1
A setHeaders() 0 4 1
A setChars() 0 4 1
A setListPrefix() 0 4 1
A run() 0 44 5
A setRows() 0 8 3
A getScreenWidth() 0 9 3
C calculateRowsSize() 0 50 12
D renderRow() 0 61 15
A renderSeparator() 0 11 3
B calculateRowHeight() 0 21 8

How to fix   Complexity   

Complex Class

Complex classes like Table often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Table, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link https://www.yiiframework.com/
4
 * @copyright Copyright (c) 2008 Yii Software LLC
5
 * @license https://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-write string $listPrefix List prefix.
44
 * @property-write int $screenWidth Screen width.
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
     * @since 2.0.19
72
     */
73
    protected $headers = [];
74
    /**
75
     * @var array table rows
76
     * @since 2.0.19
77
     */
78
    protected $rows = [];
79
    /**
80
     * @var array table chars
81
     * @since 2.0.19
82
     */
83
    protected $chars = [
84
        self::CHAR_TOP => '═',
85
        self::CHAR_TOP_MID => '╤',
86
        self::CHAR_TOP_LEFT => '╔',
87
        self::CHAR_TOP_RIGHT => '╗',
88
        self::CHAR_BOTTOM => '═',
89
        self::CHAR_BOTTOM_MID => '╧',
90
        self::CHAR_BOTTOM_LEFT => '╚',
91
        self::CHAR_BOTTOM_RIGHT => '╝',
92
        self::CHAR_LEFT => '║',
93
        self::CHAR_LEFT_MID => '╟',
94
        self::CHAR_MID => '─',
95
        self::CHAR_MID_MID => '┼',
96
        self::CHAR_RIGHT => '║',
97
        self::CHAR_RIGHT_MID => '╢',
98
        self::CHAR_MIDDLE => '│',
99
    ];
100
    /**
101
     * @var array table column widths
102
     * @since 2.0.19
103
     */
104
    protected $columnWidths = [];
105
    /**
106
     * @var int screen width
107
     * @since 2.0.19
108
     */
109
    protected $screenWidth;
110
    /**
111
     * @var string list prefix
112
     * @since 2.0.19
113
     */
114
    protected $listPrefix = '• ';
115
116
117
    /**
118
     * Set table headers.
119
     *
120
     * @param array $headers table headers
121
     * @return $this
122
     */
123 27
    public function setHeaders(array $headers)
124
    {
125 27
        $this->headers = array_values($headers);
126 27
        return $this;
127
    }
128
129
    /**
130
     * Set table rows.
131
     *
132
     * @param array $rows table rows
133
     * @return $this
134
     */
135 28
    public function setRows(array $rows)
136
    {
137
        $this->rows = array_map(function($row) {
138
            return array_map(function($value) {
139 27
                return empty($value) && !is_numeric($value) ? ' ' : $value;
140 27
            }, array_values($row));
141 28
        }, $rows);
142 28
        return $this;
143
    }
144
145
    /**
146
     * Set table chars.
147
     *
148
     * @param array $chars table chars
149
     * @return $this
150
     */
151 1
    public function setChars(array $chars)
152
    {
153 1
        $this->chars = $chars;
154 1
        return $this;
155
    }
156
157
    /**
158
     * Set screen width.
159
     *
160
     * @param int $width screen width
161
     * @return $this
162
     */
163 26
    public function setScreenWidth($width)
164
    {
165 26
        $this->screenWidth = $width;
166 26
        return $this;
167
    }
168
169
    /**
170
     * Set list prefix.
171
     *
172
     * @param string $listPrefix list prefix
173
     * @return $this
174
     */
175 2
    public function setListPrefix($listPrefix)
176
    {
177 2
        $this->listPrefix = $listPrefix;
178 2
        return $this;
179
    }
180
181
    /**
182
     * @return string the rendered table
183
     */
184 28
    public function run()
185
    {
186 28
        $this->calculateRowsSize();
187 28
        $headerCount = count($this->headers);
188
189 28
        $buffer = $this->renderSeparator(
190 28
            $this->chars[self::CHAR_TOP_LEFT],
191 28
            $this->chars[self::CHAR_TOP_MID],
192 28
            $this->chars[self::CHAR_TOP],
193 28
            $this->chars[self::CHAR_TOP_RIGHT]
194
        );
195
        // Header
196 28
        if ($headerCount > 0) {
197 27
            $buffer .= $this->renderRow($this->headers,
198 27
                $this->chars[self::CHAR_LEFT],
199 27
                $this->chars[self::CHAR_MIDDLE],
200 27
                $this->chars[self::CHAR_RIGHT]
201
            );
202
        }
203
204
        // Content
205 28
        foreach ($this->rows as $i => $row) {
206 27
            if ($i > 0 || $headerCount > 0) {
207 27
                $buffer .= $this->renderSeparator(
208 27
                    $this->chars[self::CHAR_LEFT_MID],
209 27
                    $this->chars[self::CHAR_MID_MID],
210 27
                    $this->chars[self::CHAR_MID],
211 27
                    $this->chars[self::CHAR_RIGHT_MID]
212
                );
213
            }
214 27
            $buffer .= $this->renderRow($row,
215 27
                $this->chars[self::CHAR_LEFT],
216 27
                $this->chars[self::CHAR_MIDDLE],
217 27
                $this->chars[self::CHAR_RIGHT]);
218
        }
219
220 28
        $buffer .= $this->renderSeparator(
221 28
            $this->chars[self::CHAR_BOTTOM_LEFT],
222 28
            $this->chars[self::CHAR_BOTTOM_MID],
223 28
            $this->chars[self::CHAR_BOTTOM],
224 28
            $this->chars[self::CHAR_BOTTOM_RIGHT]
225
        );
226
227 28
        return $buffer;
228
    }
229
230
    /**
231
     * Renders a row of data into a string.
232
     *
233
     * @param array $row row of data
234
     * @param string $spanLeft character for left border
235
     * @param string $spanMiddle character for middle border
236
     * @param string $spanRight character for right border
237
     * @return string
238
     * @see \yii\console\widgets\Table::render()
239
     */
240 28
    protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
241
    {
242 28
        $size = $this->columnWidths;
243
244 28
        $buffer = '';
245 28
        $arrayPointer = [];
246 28
        $renderedChunkTexts = [];
247 28
        for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
248 28
            $buffer .= $spanLeft . ' ';
249 28
            foreach ($size as $index => $cellSize) {
250 28
                $cell = isset($row[$index]) ? $row[$index] : null;
251 28
                $prefix = '';
252 28
                if ($index !== 0) {
253 26
                    $buffer .= $spanMiddle . ' ';
254
                }
255
256 28
                $arrayFromMultilineString = false;
257 28
                if (is_string($cell)) {
258 26
                    $cellLines = explode(PHP_EOL, $cell);
259 26
                    if (count($cellLines) > 1) {
260 2
                        $cell = $cellLines;
261 2
                        $arrayFromMultilineString = true;
262
                    }
263
                }
264
265 28
                if (is_array($cell)) {
266 12
                    if (empty($renderedChunkTexts[$index])) {
267 12
                        $renderedChunkTexts[$index] = '';
268 12
                        $start = 0;
269 12
                        $prefix = $arrayFromMultilineString ? '' : $this->listPrefix;
0 ignored issues
show
introduced by
The property listPrefix is declared write-only in yii\console\widgets\Table.
Loading history...
270 12
                        if (!isset($arrayPointer[$index])) {
271 12
                            $arrayPointer[$index] = 0;
272
                        }
273
                    } else {
274 6
                        $start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset);
275
                    }
276 12
                    $chunk = Console::ansiColorizedSubstr(
277 12
                        $cell[$arrayPointer[$index]],
278 12
                        $start,
279 12
                        $cellSize - 2 - Console::ansiStrwidth($prefix)
280
                    );
281 12
                    $renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk);
282 12
                    $fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]);
283 12
                    if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) {
284 11
                        $arrayPointer[$index]++;
285 12
                        $renderedChunkTexts[$index] = '';
286
                    }
287
                } else {
288 28
                    $chunk = Console::ansiColorizedSubstr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2);
289
                }
290 28
                $chunk = $prefix . $chunk;
291 28
                $repeat = $cellSize - Console::ansiStrwidth($chunk) - 1;
292 28
                $buffer .= $chunk;
293 28
                if ($repeat >= 0) {
294 28
                    $buffer .= str_repeat(' ', $repeat);
295
                }
296
            }
297 28
            $buffer .= "$spanRight\n";
298
        }
299
300 28
        return $buffer;
301
    }
302
303
    /**
304
     * Renders separator.
305
     *
306
     * @param string $spanLeft character for left border
307
     * @param string $spanMid character for middle border
308
     * @param string $spanMidMid character for middle-middle border
309
     * @param string $spanRight character for right border
310
     * @return string the generated separator row
311
     * @see \yii\console\widgets\Table::render()
312
     */
313 28
    protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
314
    {
315 28
        $separator = $spanLeft;
316 28
        foreach ($this->columnWidths as $index => $rowSize) {
317 28
            if ($index !== 0) {
318 26
                $separator .= $spanMid;
319
            }
320 28
            $separator .= str_repeat($spanMidMid, $rowSize);
321
        }
322 28
        $separator .= $spanRight . "\n";
323 28
        return $separator;
324
    }
325
326
    /**
327
     * Calculate the size of rows to draw anchor of columns in console.
328
     *
329
     * @see \yii\console\widgets\Table::render()
330
     */
331 28
    protected function calculateRowsSize()
332
    {
333 28
        $this->columnWidths = $columns = [];
334 28
        $totalWidth = 0;
335 28
        $screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET;
336
337 28
        $headerCount = count($this->headers);
338 28
        if (empty($this->rows)) {
339 1
            $rowColCount = 0;
340
        } else {
341 27
            $rowColCount = max(array_map('count', $this->rows));
342
        }
343 28
        $count = max($headerCount, $rowColCount);
344 28
        for ($i = 0; $i < $count; $i++) {
345 28
            $columns[] = ArrayHelper::getColumn($this->rows, $i);
346 28
            if ($i < $headerCount) {
347 27
                $columns[$i][] = $this->headers[$i];
348
            }
349
        }
350
351 28
        foreach ($columns as $column) {
352
            $columnWidth = max(array_map(function ($val) {
353 28
                if (is_array($val)) {
354 10
                    return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix);
0 ignored issues
show
introduced by
The property listPrefix is declared write-only in yii\console\widgets\Table.
Loading history...
355
                }
356 28
                if (is_string($val)) {
357 26
                    return max(array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val)));
358
                }
359 4
                return Console::ansiStrwidth($val);
360 28
            }, $column)) + 2;
361 28
            $this->columnWidths[] = $columnWidth;
362 28
            $totalWidth += $columnWidth;
363
        }
364
365 28
        if ($totalWidth > $screenWidth) {
366 12
            $minWidth = 3;
367 12
            $fixWidths = [];
368 12
            $relativeWidth = $screenWidth / $totalWidth;
369 12
            foreach ($this->columnWidths as $j => $width) {
370 12
                $scaledWidth = (int) ($width * $relativeWidth);
371 12
                if ($scaledWidth < $minWidth) {
372 7
                    $fixWidths[$j] = 3;
373
                }
374
            }
375
376 12
            $totalFixWidth = array_sum($fixWidths);
377 12
            $relativeWidth = ($screenWidth - $totalFixWidth) / ($totalWidth - $totalFixWidth);
378 12
            foreach ($this->columnWidths as $j => $width) {
379 12
                if (!array_key_exists($j, $fixWidths)) {
380 10
                    $this->columnWidths[$j] = (int) ($width * $relativeWidth);
381
                }
382
            }
383
        }
384 28
    }
385
386
    /**
387
     * Calculate the height of a row.
388
     *
389
     * @param array $row
390
     * @return int maximum row per cell
391
     * @see \yii\console\widgets\Table::render()
392
     */
393 28
    protected function calculateRowHeight($row)
394
    {
395
        $rowsPerCell = array_map(function ($size, $columnWidth) {
396 28
            if (is_array($columnWidth)) {
397 26
                $rows = 0;
398 26
                foreach ($columnWidth as $width) {
399 26
                    $rows +=  $size == 2 ? 0 : ceil($width / ($size - 2));
400
                }
401 26
                return $rows;
402
            }
403 5
            return $size == 2 || $columnWidth == 0 ? 0 : ceil($columnWidth / ($size - 2));
404 28
        }, $this->columnWidths, array_map(function ($val) {
405 28
            if (is_array($val)) {
406 10
                return array_map('yii\helpers\Console::ansiStrwidth', $val);
407
            }
408 28
            if (is_string($val)) {
409 26
                return array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val));
410
            }
411 2
            return Console::ansiStrwidth($val);
412 28
        }, $row));
413 28
        return max($rowsPerCell);
414
    }
415
416
    /**
417
     * Getting screen width.
418
     * If it is not able to determine screen width, default value `123` will be set.
419
     *
420
     * @return int screen width
421
     */
422 28
    protected function getScreenWidth()
423
    {
424 28
        if (!$this->screenWidth) {
0 ignored issues
show
introduced by
The property screenWidth is declared write-only in yii\console\widgets\Table.
Loading history...
425 2
            $size = Console::getScreenSize();
426 2
            $this->screenWidth = isset($size[0])
427 2
                ? $size[0]
428
                : self::DEFAULT_CONSOLE_SCREEN_WIDTH + self::CONSOLE_SCROLLBAR_OFFSET;
429
        }
430 28
        return $this->screenWidth;
431
    }
432
}
433