Completed
Push — develop ( f11ef2...d41b65 )
by David
06:01 queued 11s
created

Table::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Console\Helper;
13
14
use Symfony\Component\Console\Exception\InvalidArgumentException;
15
use Symfony\Component\Console\Exception\RuntimeException;
16
use Symfony\Component\Console\Formatter\OutputFormatter;
17
use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface;
18
use Symfony\Component\Console\Output\ConsoleSectionOutput;
19
use Symfony\Component\Console\Output\OutputInterface;
20
21
/**
22
 * Provides helpers to display a table.
23
 *
24
 * @author Fabien Potencier <[email protected]>
25
 * @author Саша Стаменковић <[email protected]>
26
 * @author Abdellatif Ait boudad <[email protected]>
27
 * @author Max Grigorian <[email protected]>
28
 * @author Dany Maillard <[email protected]>
29
 */
30
class Table
31
{
32
    private const SEPARATOR_TOP = 0;
33
    private const SEPARATOR_TOP_BOTTOM = 1;
34
    private const SEPARATOR_MID = 2;
35
    private const SEPARATOR_BOTTOM = 3;
36
    private const BORDER_OUTSIDE = 0;
37
    private const BORDER_INSIDE = 1;
38
39
    private $headerTitle;
40
    private $footerTitle;
41
42
    /**
43
     * Table headers.
44
     */
45
    private $headers = [];
46
47
    /**
48
     * Table rows.
49
     */
50
    private $rows = [];
51
    private $horizontal = false;
52
53
    /**
54
     * Column widths cache.
55
     */
56
    private $effectiveColumnWidths = [];
57
58
    /**
59
     * Number of columns cache.
60
     *
61
     * @var int
62
     */
63
    private $numberOfColumns;
64
65
    /**
66
     * @var OutputInterface
67
     */
68
    private $output;
69
70
    /**
71
     * @var TableStyle
72
     */
73
    private $style;
74
75
    /**
76
     * @var array
77
     */
78
    private $columnStyles = [];
79
80
    /**
81
     * User set column widths.
82
     *
83
     * @var array
84
     */
85
    private $columnWidths = [];
86
    private $columnMaxWidths = [];
87
88
    private static $styles;
89
90
    private $rendered = false;
91
92
    public function __construct(OutputInterface $output)
93
    {
94
        $this->output = $output;
95
96
        if (!self::$styles) {
97
            self::$styles = self::initStyles();
98
        }
99
100
        $this->setStyle('default');
101
    }
102
103
    /**
104
     * Sets a style definition.
105
     */
106
    public static function setStyleDefinition(string $name, TableStyle $style)
107
    {
108
        if (!self::$styles) {
109
            self::$styles = self::initStyles();
110
        }
111
112
        self::$styles[$name] = $style;
113
    }
114
115
    /**
116
     * Gets a style definition by name.
117
     *
118
     * @return TableStyle
119
     */
120
    public static function getStyleDefinition(string $name)
121
    {
122
        if (!self::$styles) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$styles of type array<string,Symfony\Com...sole\Helper\TableStyle> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
123
            self::$styles = self::initStyles();
124
        }
125
126
        if (isset(self::$styles[$name])) {
127
            return self::$styles[$name];
128
        }
129
130
        throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
131
    }
132
133
    /**
134
     * Sets table style.
135
     *
136
     * @param TableStyle|string $name The style name or a TableStyle instance
137
     *
138
     * @return $this
139
     */
140
    public function setStyle($name)
141
    {
142
        $this->style = $this->resolveStyle($name);
143
144
        return $this;
145
    }
146
147
    /**
148
     * Gets the current table style.
149
     *
150
     * @return TableStyle
151
     */
152
    public function getStyle()
153
    {
154
        return $this->style;
155
    }
156
157
    /**
158
     * Sets table column style.
159
     *
160
     * @param TableStyle|string $name The style name or a TableStyle instance
161
     *
162
     * @return $this
163
     */
164
    public function setColumnStyle(int $columnIndex, $name)
165
    {
166
        $this->columnStyles[$columnIndex] = $this->resolveStyle($name);
167
168
        return $this;
169
    }
170
171
    /**
172
     * Gets the current style for a column.
173
     *
174
     * If style was not set, it returns the global table style.
175
     *
176
     * @return TableStyle
177
     */
178
    public function getColumnStyle(int $columnIndex)
179
    {
180
        return $this->columnStyles[$columnIndex] ?? $this->getStyle();
181
    }
182
183
    /**
184
     * Sets the minimum width of a column.
185
     *
186
     * @return $this
187
     */
188
    public function setColumnWidth(int $columnIndex, int $width)
189
    {
190
        $this->columnWidths[$columnIndex] = $width;
191
192
        return $this;
193
    }
194
195
    /**
196
     * Sets the minimum width of all columns.
197
     *
198
     * @return $this
199
     */
200
    public function setColumnWidths(array $widths)
201
    {
202
        $this->columnWidths = [];
203
        foreach ($widths as $index => $width) {
204
            $this->setColumnWidth($index, $width);
205
        }
206
207
        return $this;
208
    }
209
210
    /**
211
     * Sets the maximum width of a column.
212
     *
213
     * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while
214
     * formatted strings are preserved.
215
     *
216
     * @return $this
217
     */
218
    public function setColumnMaxWidth(int $columnIndex, int $width): self
219
    {
220
        if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) {
221
            throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter())));
222
        }
223
224
        $this->columnMaxWidths[$columnIndex] = $width;
225
226
        return $this;
227
    }
228
229
    public function setHeaders(array $headers)
230
    {
231
        $headers = array_values($headers);
232
        if (!empty($headers) && !\is_array($headers[0])) {
233
            $headers = [$headers];
234
        }
235
236
        $this->headers = $headers;
237
238
        return $this;
239
    }
240
241
    public function setRows(array $rows)
242
    {
243
        $this->rows = [];
244
245
        return $this->addRows($rows);
246
    }
247
248
    public function addRows(array $rows)
249
    {
250
        foreach ($rows as $row) {
251
            $this->addRow($row);
252
        }
253
254
        return $this;
255
    }
256
257
    public function addRow($row)
258
    {
259
        if ($row instanceof TableSeparator) {
260
            $this->rows[] = $row;
261
262
            return $this;
263
        }
264
265
        if (!\is_array($row)) {
266
            throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.');
267
        }
268
269
        $this->rows[] = array_values($row);
270
271
        return $this;
272
    }
273
274
    /**
275
     * Adds a row to the table, and re-renders the table.
276
     */
277
    public function appendRow($row): self
278
    {
279
        if (!$this->output instanceof ConsoleSectionOutput) {
280
            throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__));
281
        }
282
283
        if ($this->rendered) {
284
            $this->output->clear($this->calculateRowCount());
285
        }
286
287
        $this->addRow($row);
288
        $this->render();
289
290
        return $this;
291
    }
292
293
    public function setRow($column, array $row)
294
    {
295
        $this->rows[$column] = $row;
296
297
        return $this;
298
    }
299
300
    public function setHeaderTitle(?string $title): self
301
    {
302
        $this->headerTitle = $title;
303
304
        return $this;
305
    }
306
307
    public function setFooterTitle(?string $title): self
308
    {
309
        $this->footerTitle = $title;
310
311
        return $this;
312
    }
313
314
    public function setHorizontal(bool $horizontal = true): self
315
    {
316
        $this->horizontal = $horizontal;
317
318
        return $this;
319
    }
320
321
    /**
322
     * Renders table to output.
323
     *
324
     * Example:
325
     *
326
     *     +---------------+-----------------------+------------------+
327
     *     | ISBN          | Title                 | Author           |
328
     *     +---------------+-----------------------+------------------+
329
     *     | 99921-58-10-7 | Divine Comedy         | Dante Alighieri  |
330
     *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
331
     *     | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
332
     *     +---------------+-----------------------+------------------+
333
     */
334
    public function render()
335
    {
336
        $divider = new TableSeparator();
337
        if ($this->horizontal) {
338
            $rows = [];
339
            foreach ($this->headers[0] ?? [] as $i => $header) {
340
                $rows[$i] = [$header];
341
                foreach ($this->rows as $row) {
342
                    if ($row instanceof TableSeparator) {
343
                        continue;
344
                    }
345
                    if (isset($row[$i])) {
346
                        $rows[$i][] = $row[$i];
347
                    } elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) {
348
                        // Noop, there is a "title"
349
                    } else {
350
                        $rows[$i][] = null;
351
                    }
352
                }
353
            }
354
        } else {
355
            $rows = array_merge($this->headers, [$divider], $this->rows);
356
        }
357
358
        $this->calculateNumberOfColumns($rows);
359
360
        $rows = $this->buildTableRows($rows);
361
        $this->calculateColumnsWidth($rows);
362
363
        $isHeader = !$this->horizontal;
364
        $isFirstRow = $this->horizontal;
365
        foreach ($rows as $row) {
366
            if ($divider === $row) {
367
                $isHeader = false;
368
                $isFirstRow = true;
369
370
                continue;
371
            }
372
            if ($row instanceof TableSeparator) {
373
                $this->renderRowSeparator();
374
375
                continue;
376
            }
377
            if (!$row) {
378
                continue;
379
            }
380
381
            if ($isHeader || $isFirstRow) {
382
                if ($isFirstRow) {
383
                    $this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM);
384
                    $isFirstRow = false;
385
                } else {
386
                    $this->renderRowSeparator(self::SEPARATOR_TOP, $this->headerTitle, $this->style->getHeaderTitleFormat());
387
                }
388
            }
389
            if ($this->horizontal) {
390
                $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat());
391
            } else {
392
                $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat());
393
            }
394
        }
395
        $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat());
396
397
        $this->cleanup();
398
        $this->rendered = true;
399
    }
400
401
    /**
402
     * Renders horizontal header separator.
403
     *
404
     * Example:
405
     *
406
     *     +-----+-----------+-------+
407
     */
408
    private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $title = null, string $titleFormat = null)
409
    {
410
        if (0 === $count = $this->numberOfColumns) {
411
            return;
412
        }
413
414
        $borders = $this->style->getBorderChars();
415
        if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) {
416
            return;
417
        }
418
419
        $crossings = $this->style->getCrossingChars();
420
        if (self::SEPARATOR_MID === $type) {
421
            list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[2], $crossings[8], $crossings[0], $crossings[4]];
422
        } elseif (self::SEPARATOR_TOP === $type) {
423
            list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[1], $crossings[2], $crossings[3]];
424
        } elseif (self::SEPARATOR_TOP_BOTTOM === $type) {
425
            list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[9], $crossings[10], $crossings[11]];
426
        } else {
427
            list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[7], $crossings[6], $crossings[5]];
428
        }
429
430
        $markup = $leftChar;
431
        for ($column = 0; $column < $count; ++$column) {
432
            $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]);
433
            $markup .= $column === $count - 1 ? $rightChar : $midChar;
434
        }
435
436
        if (null !== $title) {
437
            $titleLength = Helper::strlenWithoutDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title));
438
            $markupLength = Helper::strlen($markup);
439
            if ($titleLength > $limit = $markupLength - 4) {
440
                $titleLength = $limit;
441
                $formatLength = Helper::strlenWithoutDecoration($formatter, sprintf($titleFormat, ''));
442
                $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...');
443
            }
444
445
            $titleStart = ($markupLength - $titleLength) / 2;
446
            if (false === mb_detect_encoding($markup, null, true)) {
447
                $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength);
448
            } else {
449
                $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength);
450
            }
451
        }
452
453
        $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
454
    }
455
456
    /**
457
     * Renders vertical column separator.
458
     */
459
    private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string
460
    {
461
        $borders = $this->style->getBorderChars();
462
463
        return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]);
464
    }
465
466
    /**
467
     * Renders table row.
468
     *
469
     * Example:
470
     *
471
     *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
472
     */
473
    private function renderRow(array $row, string $cellFormat, string $firstCellFormat = null)
474
    {
475
        $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE);
476
        $columns = $this->getRowColumns($row);
477
        $last = \count($columns) - 1;
478
        foreach ($columns as $i => $column) {
479
            if ($firstCellFormat && 0 === $i) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $firstCellFormat of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
480
                $rowContent .= $this->renderCell($row, $column, $firstCellFormat);
481
            } else {
482
                $rowContent .= $this->renderCell($row, $column, $cellFormat);
483
            }
484
            $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE);
485
        }
486
        $this->output->writeln($rowContent);
487
    }
488
489
    /**
490
     * Renders table cell with padding.
491
     */
492
    private function renderCell(array $row, int $column, string $cellFormat): string
493
    {
494
        $cell = isset($row[$column]) ? $row[$column] : '';
495
        $width = $this->effectiveColumnWidths[$column];
496
        if ($cell instanceof TableCell && $cell->getColspan() > 1) {
497
            // add the width of the following columns(numbers of colspan).
498
            foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
499
                $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn];
500
            }
501
        }
502
503
        // str_pad won't work properly with multi-byte strings, we need to fix the padding
504
        if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
505
            $width += \strlen($cell) - mb_strwidth($cell, $encoding);
506
        }
507
508
        $style = $this->getColumnStyle($column);
509
510
        if ($cell instanceof TableSeparator) {
511
            return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width));
512
        }
513
514
        $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
515
        $content = sprintf($style->getCellRowContentFormat(), $cell);
516
517
        return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType()));
518
    }
519
520
    /**
521
     * Calculate number of columns for this table.
522
     */
523
    private function calculateNumberOfColumns(array $rows)
524
    {
525
        $columns = [0];
526
        foreach ($rows as $row) {
527
            if ($row instanceof TableSeparator) {
528
                continue;
529
            }
530
531
            $columns[] = $this->getNumberOfColumns($row);
532
        }
533
534
        $this->numberOfColumns = max($columns);
535
    }
536
537
    private function buildTableRows(array $rows): TableRows
538
    {
539
        /** @var WrappableOutputFormatterInterface $formatter */
540
        $formatter = $this->output->getFormatter();
541
        $unmergedRows = [];
542
        for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
543
            $rows = $this->fillNextRows($rows, $rowKey);
544
545
            // Remove any new line breaks and replace it with a new line
546
            foreach ($rows[$rowKey] as $column => $cell) {
547
                $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1;
548
549
                if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) {
550
                    $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan);
551
                }
552
                if (!strstr($cell, "\n")) {
553
                    continue;
554
                }
555
                $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell)));
556
                $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped;
557
                $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
558
                foreach ($lines as $lineKey => $line) {
559
                    if ($colspan > 1) {
560
                        $line = new TableCell($line, ['colspan' => $colspan]);
561
                    }
562
                    if (0 === $lineKey) {
563
                        $rows[$rowKey][$column] = $line;
564
                    } else {
565
                        if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) {
566
                            $unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey);
567
                        }
568
                        $unmergedRows[$rowKey][$lineKey][$column] = $line;
569
                    }
570
                }
571
            }
572
        }
573
574
        return new TableRows(function () use ($rows, $unmergedRows): \Traversable {
575
            foreach ($rows as $rowKey => $row) {
576
                yield $this->fillCells($row);
577
578
                if (isset($unmergedRows[$rowKey])) {
579
                    foreach ($unmergedRows[$rowKey] as $unmergedRow) {
580
                        yield $this->fillCells($unmergedRow);
581
                    }
582
                }
583
            }
584
        });
585
    }
586
587
    private function calculateRowCount(): int
588
    {
589
        $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows))));
590
591
        if ($this->headers) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->headers of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
592
            ++$numberOfRows; // Add row for header separator
593
        }
594
595
        if (\count($this->rows) > 0) {
596
            ++$numberOfRows; // Add row for footer separator
597
        }
598
599
        return $numberOfRows;
600
    }
601
602
    /**
603
     * fill rows that contains rowspan > 1.
604
     *
605
     * @throws InvalidArgumentException
606
     */
607
    private function fillNextRows(array $rows, int $line): array
608
    {
609
        $unmergedRows = [];
610
        foreach ($rows[$line] as $column => $cell) {
611
            if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
612
                throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell)));
613
            }
614
            if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
615
                $nbLines = $cell->getRowspan() - 1;
616
                $lines = [$cell];
617
                if (strstr($cell, "\n")) {
618
                    $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
619
                    $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
620
621
                    $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan()]);
622
                    unset($lines[0]);
623
                }
624
625
                // create a two dimensional array (rowspan x colspan)
626
                $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows);
627
                foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
628
                    $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : '';
629
                    $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]);
630
                    if ($nbLines === $unmergedRowKey - $line) {
631
                        break;
632
                    }
633
                }
634
            }
635
        }
636
637
        foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
638
            // we need to know if $unmergedRow will be merged or inserted into $rows
639
            if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
640
                foreach ($unmergedRow as $cellKey => $cell) {
641
                    // insert cell into row at cellKey position
642
                    array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]);
643
                }
644
            } else {
645
                $row = $this->copyRow($rows, $unmergedRowKey - 1);
646
                foreach ($unmergedRow as $column => $cell) {
647
                    if (!empty($cell)) {
648
                        $row[$column] = $unmergedRow[$column];
649
                    }
650
                }
651
                array_splice($rows, $unmergedRowKey, 0, [$row]);
652
            }
653
        }
654
655
        return $rows;
656
    }
657
658
    /**
659
     * fill cells for a row that contains colspan > 1.
660
     */
661
    private function fillCells($row)
662
    {
663
        $newRow = [];
664
665
        foreach ($row as $column => $cell) {
666
            $newRow[] = $cell;
667
            if ($cell instanceof TableCell && $cell->getColspan() > 1) {
668
                foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
669
                    // insert empty value at column position
670
                    $newRow[] = '';
671
                }
672
            }
673
        }
674
675
        return $newRow ?: $row;
676
    }
677
678
    private function copyRow(array $rows, int $line): array
679
    {
680
        $row = $rows[$line];
681
        foreach ($row as $cellKey => $cellValue) {
682
            $row[$cellKey] = '';
683
            if ($cellValue instanceof TableCell) {
684
                $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]);
685
            }
686
        }
687
688
        return $row;
689
    }
690
691
    /**
692
     * Gets number of columns by row.
693
     */
694
    private function getNumberOfColumns(array $row): int
695
    {
696
        $columns = \count($row);
697
        foreach ($row as $column) {
698
            $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0;
699
        }
700
701
        return $columns;
702
    }
703
704
    /**
705
     * Gets list of columns for the given row.
706
     */
707
    private function getRowColumns(array $row): array
708
    {
709
        $columns = range(0, $this->numberOfColumns - 1);
710
        foreach ($row as $cellKey => $cell) {
711
            if ($cell instanceof TableCell && $cell->getColspan() > 1) {
712
                // exclude grouped columns.
713
                $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1));
714
            }
715
        }
716
717
        return $columns;
718
    }
719
720
    /**
721
     * Calculates columns widths.
722
     */
723
    private function calculateColumnsWidth(iterable $rows)
724
    {
725
        for ($column = 0; $column < $this->numberOfColumns; ++$column) {
726
            $lengths = [];
727
            foreach ($rows as $row) {
728
                if ($row instanceof TableSeparator) {
729
                    continue;
730
                }
731
732
                foreach ($row as $i => $cell) {
733
                    if ($cell instanceof TableCell) {
734
                        $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
735
                        $textLength = Helper::strlen($textContent);
736
                        if ($textLength > 0) {
737
                            $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
738
                            foreach ($contentColumns as $position => $content) {
739
                                $row[$i + $position] = $content;
740
                            }
741
                        }
742
                    }
743
                }
744
745
                $lengths[] = $this->getCellWidth($row, $column);
746
            }
747
748
            $this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2;
749
        }
750
    }
751
752
    private function getColumnSeparatorWidth(): int
753
    {
754
        return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3]));
755
    }
756
757
    private function getCellWidth(array $row, int $column): int
758
    {
759
        $cellWidth = 0;
760
761
        if (isset($row[$column])) {
762
            $cell = $row[$column];
763
            $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
764
        }
765
766
        $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
767
        $cellWidth = max($cellWidth, $columnWidth);
768
769
        return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth;
770
    }
771
772
    /**
773
     * Called after rendering to cleanup cache data.
774
     */
775
    private function cleanup()
776
    {
777
        $this->effectiveColumnWidths = [];
778
        $this->numberOfColumns = null;
779
    }
780
781
    private static function initStyles(): array
782
    {
783
        $borderless = new TableStyle();
784
        $borderless
785
            ->setHorizontalBorderChars('=')
786
            ->setVerticalBorderChars(' ')
787
            ->setDefaultCrossingChar(' ')
788
        ;
789
790
        $compact = new TableStyle();
791
        $compact
792
            ->setHorizontalBorderChars('')
793
            ->setVerticalBorderChars(' ')
794
            ->setDefaultCrossingChar('')
795
            ->setCellRowContentFormat('%s')
796
        ;
797
798
        $styleGuide = new TableStyle();
799
        $styleGuide
800
            ->setHorizontalBorderChars('-')
801
            ->setVerticalBorderChars(' ')
802
            ->setDefaultCrossingChar(' ')
803
            ->setCellHeaderFormat('%s')
804
        ;
805
806
        $box = (new TableStyle())
807
            ->setHorizontalBorderChars('─')
808
            ->setVerticalBorderChars('│')
809
            ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├')
810
        ;
811
812
        $boxDouble = (new TableStyle())
813
            ->setHorizontalBorderChars('═', '─')
814
            ->setVerticalBorderChars('║', '│')
815
            ->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣')
816
        ;
817
818
        return [
819
            'default' => new TableStyle(),
820
            'borderless' => $borderless,
821
            'compact' => $compact,
822
            'symfony-style-guide' => $styleGuide,
823
            'box' => $box,
824
            'box-double' => $boxDouble,
825
        ];
826
    }
827
828
    private function resolveStyle($name): TableStyle
829
    {
830
        if ($name instanceof TableStyle) {
831
            return $name;
832
        }
833
834
        if (isset(self::$styles[$name])) {
835
            return self::$styles[$name];
836
        }
837
838
        throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
839
    }
840
}
841