Completed
Push — master ( e11e3e...986483 )
by Carsten
12:50
created

Table::setChars()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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