c15e5ab23c25ac285d3bf7d838a51aba39da9723
[yaffs-website] / vendor / symfony / console / Helper / Table.php
1 <?php
2
3 /*
4  * This file is part of the Symfony package.
5  *
6  * (c) Fabien Potencier <fabien@symfony.com>
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\Output\OutputInterface;
16
17 /**
18  * Provides helpers to display a table.
19  *
20  * @author Fabien Potencier <fabien@symfony.com>
21  * @author Саша Стаменковић <umpirsky@gmail.com>
22  * @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
23  * @author Max Grigorian <maxakawizard@gmail.com>
24  */
25 class Table
26 {
27     /**
28      * Table headers.
29      */
30     private $headers = array();
31
32     /**
33      * Table rows.
34      */
35     private $rows = array();
36
37     /**
38      * Column widths cache.
39      */
40     private $effectiveColumnWidths = array();
41
42     /**
43      * Number of columns cache.
44      *
45      * @var int
46      */
47     private $numberOfColumns;
48
49     /**
50      * @var OutputInterface
51      */
52     private $output;
53
54     /**
55      * @var TableStyle
56      */
57     private $style;
58
59     /**
60      * @var array
61      */
62     private $columnStyles = array();
63
64     /**
65      * User set column widths.
66      *
67      * @var array
68      */
69     private $columnWidths = array();
70
71     private static $styles;
72
73     public function __construct(OutputInterface $output)
74     {
75         $this->output = $output;
76
77         if (!self::$styles) {
78             self::$styles = self::initStyles();
79         }
80
81         $this->setStyle('default');
82     }
83
84     /**
85      * Sets a style definition.
86      *
87      * @param string     $name  The style name
88      * @param TableStyle $style A TableStyle instance
89      */
90     public static function setStyleDefinition($name, TableStyle $style)
91     {
92         if (!self::$styles) {
93             self::$styles = self::initStyles();
94         }
95
96         self::$styles[$name] = $style;
97     }
98
99     /**
100      * Gets a style definition by name.
101      *
102      * @param string $name The style name
103      *
104      * @return TableStyle
105      */
106     public static function getStyleDefinition($name)
107     {
108         if (!self::$styles) {
109             self::$styles = self::initStyles();
110         }
111
112         if (isset(self::$styles[$name])) {
113             return self::$styles[$name];
114         }
115
116         throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
117     }
118
119     /**
120      * Sets table style.
121      *
122      * @param TableStyle|string $name The style name or a TableStyle instance
123      *
124      * @return $this
125      */
126     public function setStyle($name)
127     {
128         $this->style = $this->resolveStyle($name);
129
130         return $this;
131     }
132
133     /**
134      * Gets the current table style.
135      *
136      * @return TableStyle
137      */
138     public function getStyle()
139     {
140         return $this->style;
141     }
142
143     /**
144      * Sets table column style.
145      *
146      * @param int               $columnIndex Column index
147      * @param TableStyle|string $name        The style name or a TableStyle instance
148      *
149      * @return $this
150      */
151     public function setColumnStyle($columnIndex, $name)
152     {
153         $columnIndex = (int) $columnIndex;
154
155         $this->columnStyles[$columnIndex] = $this->resolveStyle($name);
156
157         return $this;
158     }
159
160     /**
161      * Gets the current style for a column.
162      *
163      * If style was not set, it returns the global table style.
164      *
165      * @param int $columnIndex Column index
166      *
167      * @return TableStyle
168      */
169     public function getColumnStyle($columnIndex)
170     {
171         if (isset($this->columnStyles[$columnIndex])) {
172             return $this->columnStyles[$columnIndex];
173         }
174
175         return $this->getStyle();
176     }
177
178     /**
179      * Sets the minimum width of a column.
180      *
181      * @param int $columnIndex Column index
182      * @param int $width       Minimum column width in characters
183      *
184      * @return $this
185      */
186     public function setColumnWidth($columnIndex, $width)
187     {
188         $this->columnWidths[(int) $columnIndex] = (int) $width;
189
190         return $this;
191     }
192
193     /**
194      * Sets the minimum width of all columns.
195      *
196      * @param array $widths
197      *
198      * @return $this
199      */
200     public function setColumnWidths(array $widths)
201     {
202         $this->columnWidths = array();
203         foreach ($widths as $index => $width) {
204             $this->setColumnWidth($index, $width);
205         }
206
207         return $this;
208     }
209
210     public function setHeaders(array $headers)
211     {
212         $headers = array_values($headers);
213         if (!empty($headers) && !\is_array($headers[0])) {
214             $headers = array($headers);
215         }
216
217         $this->headers = $headers;
218
219         return $this;
220     }
221
222     public function setRows(array $rows)
223     {
224         $this->rows = array();
225
226         return $this->addRows($rows);
227     }
228
229     public function addRows(array $rows)
230     {
231         foreach ($rows as $row) {
232             $this->addRow($row);
233         }
234
235         return $this;
236     }
237
238     public function addRow($row)
239     {
240         if ($row instanceof TableSeparator) {
241             $this->rows[] = $row;
242
243             return $this;
244         }
245
246         if (!\is_array($row)) {
247             throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.');
248         }
249
250         $this->rows[] = array_values($row);
251
252         return $this;
253     }
254
255     public function setRow($column, array $row)
256     {
257         $this->rows[$column] = $row;
258
259         return $this;
260     }
261
262     /**
263      * Renders table to output.
264      *
265      * Example:
266      *
267      *     +---------------+-----------------------+------------------+
268      *     | ISBN          | Title                 | Author           |
269      *     +---------------+-----------------------+------------------+
270      *     | 99921-58-10-7 | Divine Comedy         | Dante Alighieri  |
271      *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
272      *     | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
273      *     +---------------+-----------------------+------------------+
274      */
275     public function render()
276     {
277         $this->calculateNumberOfColumns();
278         $rows = $this->buildTableRows($this->rows);
279         $headers = $this->buildTableRows($this->headers);
280
281         $this->calculateColumnsWidth(array_merge($headers, $rows));
282
283         $this->renderRowSeparator();
284         if (!empty($headers)) {
285             foreach ($headers as $header) {
286                 $this->renderRow($header, $this->style->getCellHeaderFormat());
287                 $this->renderRowSeparator();
288             }
289         }
290         foreach ($rows as $row) {
291             if ($row instanceof TableSeparator) {
292                 $this->renderRowSeparator();
293             } else {
294                 $this->renderRow($row, $this->style->getCellRowFormat());
295             }
296         }
297         if (!empty($rows)) {
298             $this->renderRowSeparator();
299         }
300
301         $this->cleanup();
302     }
303
304     /**
305      * Renders horizontal header separator.
306      *
307      * Example:
308      *
309      *     +-----+-----------+-------+
310      */
311     private function renderRowSeparator()
312     {
313         if (0 === $count = $this->numberOfColumns) {
314             return;
315         }
316
317         if (!$this->style->getHorizontalBorderChar() && !$this->style->getCrossingChar()) {
318             return;
319         }
320
321         $markup = $this->style->getCrossingChar();
322         for ($column = 0; $column < $count; ++$column) {
323             $markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->effectiveColumnWidths[$column]).$this->style->getCrossingChar();
324         }
325
326         $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
327     }
328
329     /**
330      * Renders vertical column separator.
331      */
332     private function renderColumnSeparator()
333     {
334         return sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar());
335     }
336
337     /**
338      * Renders table row.
339      *
340      * Example:
341      *
342      *     | 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |
343      *
344      * @param array  $row
345      * @param string $cellFormat
346      */
347     private function renderRow(array $row, $cellFormat)
348     {
349         if (empty($row)) {
350             return;
351         }
352
353         $rowContent = $this->renderColumnSeparator();
354         foreach ($this->getRowColumns($row) as $column) {
355             $rowContent .= $this->renderCell($row, $column, $cellFormat);
356             $rowContent .= $this->renderColumnSeparator();
357         }
358         $this->output->writeln($rowContent);
359     }
360
361     /**
362      * Renders table cell with padding.
363      *
364      * @param array  $row
365      * @param int    $column
366      * @param string $cellFormat
367      */
368     private function renderCell(array $row, $column, $cellFormat)
369     {
370         $cell = isset($row[$column]) ? $row[$column] : '';
371         $width = $this->effectiveColumnWidths[$column];
372         if ($cell instanceof TableCell && $cell->getColspan() > 1) {
373             // add the width of the following columns(numbers of colspan).
374             foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
375                 $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn];
376             }
377         }
378
379         // str_pad won't work properly with multi-byte strings, we need to fix the padding
380         if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
381             $width += \strlen($cell) - mb_strwidth($cell, $encoding);
382         }
383
384         $style = $this->getColumnStyle($column);
385
386         if ($cell instanceof TableSeparator) {
387             return sprintf($style->getBorderFormat(), str_repeat($style->getHorizontalBorderChar(), $width));
388         }
389
390         $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
391         $content = sprintf($style->getCellRowContentFormat(), $cell);
392
393         return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType()));
394     }
395
396     /**
397      * Calculate number of columns for this table.
398      */
399     private function calculateNumberOfColumns()
400     {
401         if (null !== $this->numberOfColumns) {
402             return;
403         }
404
405         $columns = array(0);
406         foreach (array_merge($this->headers, $this->rows) as $row) {
407             if ($row instanceof TableSeparator) {
408                 continue;
409             }
410
411             $columns[] = $this->getNumberOfColumns($row);
412         }
413
414         $this->numberOfColumns = max($columns);
415     }
416
417     private function buildTableRows($rows)
418     {
419         $unmergedRows = array();
420         for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) {
421             $rows = $this->fillNextRows($rows, $rowKey);
422
423             // Remove any new line breaks and replace it with a new line
424             foreach ($rows[$rowKey] as $column => $cell) {
425                 if (!strstr($cell, "\n")) {
426                     continue;
427                 }
428                 $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
429                 foreach ($lines as $lineKey => $line) {
430                     if ($cell instanceof TableCell) {
431                         $line = new TableCell($line, array('colspan' => $cell->getColspan()));
432                     }
433                     if (0 === $lineKey) {
434                         $rows[$rowKey][$column] = $line;
435                     } else {
436                         $unmergedRows[$rowKey][$lineKey][$column] = $line;
437                     }
438                 }
439             }
440         }
441
442         $tableRows = array();
443         foreach ($rows as $rowKey => $row) {
444             $tableRows[] = $this->fillCells($row);
445             if (isset($unmergedRows[$rowKey])) {
446                 $tableRows = array_merge($tableRows, $unmergedRows[$rowKey]);
447             }
448         }
449
450         return $tableRows;
451     }
452
453     /**
454      * fill rows that contains rowspan > 1.
455      *
456      * @param array $rows
457      * @param int   $line
458      *
459      * @return array
460      *
461      * @throws InvalidArgumentException
462      */
463     private function fillNextRows(array $rows, $line)
464     {
465         $unmergedRows = array();
466         foreach ($rows[$line] as $column => $cell) {
467             if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) {
468                 throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing __toString, %s given.', \gettype($cell)));
469             }
470             if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
471                 $nbLines = $cell->getRowspan() - 1;
472                 $lines = array($cell);
473                 if (strstr($cell, "\n")) {
474                     $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
475                     $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
476
477                     $rows[$line][$column] = new TableCell($lines[0], array('colspan' => $cell->getColspan()));
478                     unset($lines[0]);
479                 }
480
481                 // create a two dimensional array (rowspan x colspan)
482                 $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, array()), $unmergedRows);
483                 foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
484                     $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : '';
485                     $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, array('colspan' => $cell->getColspan()));
486                     if ($nbLines === $unmergedRowKey - $line) {
487                         break;
488                     }
489                 }
490             }
491         }
492
493         foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
494             // we need to know if $unmergedRow will be merged or inserted into $rows
495             if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
496                 foreach ($unmergedRow as $cellKey => $cell) {
497                     // insert cell into row at cellKey position
498                     array_splice($rows[$unmergedRowKey], $cellKey, 0, array($cell));
499                 }
500             } else {
501                 $row = $this->copyRow($rows, $unmergedRowKey - 1);
502                 foreach ($unmergedRow as $column => $cell) {
503                     if (!empty($cell)) {
504                         $row[$column] = $unmergedRow[$column];
505                     }
506                 }
507                 array_splice($rows, $unmergedRowKey, 0, array($row));
508             }
509         }
510
511         return $rows;
512     }
513
514     /**
515      * fill cells for a row that contains colspan > 1.
516      *
517      * @return array
518      */
519     private function fillCells($row)
520     {
521         $newRow = array();
522         foreach ($row as $column => $cell) {
523             $newRow[] = $cell;
524             if ($cell instanceof TableCell && $cell->getColspan() > 1) {
525                 foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
526                     // insert empty value at column position
527                     $newRow[] = '';
528                 }
529             }
530         }
531
532         return $newRow ?: $row;
533     }
534
535     /**
536      * @param array $rows
537      * @param int   $line
538      *
539      * @return array
540      */
541     private function copyRow(array $rows, $line)
542     {
543         $row = $rows[$line];
544         foreach ($row as $cellKey => $cellValue) {
545             $row[$cellKey] = '';
546             if ($cellValue instanceof TableCell) {
547                 $row[$cellKey] = new TableCell('', array('colspan' => $cellValue->getColspan()));
548             }
549         }
550
551         return $row;
552     }
553
554     /**
555      * Gets number of columns by row.
556      *
557      * @return int
558      */
559     private function getNumberOfColumns(array $row)
560     {
561         $columns = \count($row);
562         foreach ($row as $column) {
563             $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0;
564         }
565
566         return $columns;
567     }
568
569     /**
570      * Gets list of columns for the given row.
571      *
572      * @return array
573      */
574     private function getRowColumns(array $row)
575     {
576         $columns = range(0, $this->numberOfColumns - 1);
577         foreach ($row as $cellKey => $cell) {
578             if ($cell instanceof TableCell && $cell->getColspan() > 1) {
579                 // exclude grouped columns.
580                 $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1));
581             }
582         }
583
584         return $columns;
585     }
586
587     /**
588      * Calculates columns widths.
589      */
590     private function calculateColumnsWidth(array $rows)
591     {
592         for ($column = 0; $column < $this->numberOfColumns; ++$column) {
593             $lengths = array();
594             foreach ($rows as $row) {
595                 if ($row instanceof TableSeparator) {
596                     continue;
597                 }
598
599                 foreach ($row as $i => $cell) {
600                     if ($cell instanceof TableCell) {
601                         $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
602                         $textLength = Helper::strlen($textContent);
603                         if ($textLength > 0) {
604                             $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
605                             foreach ($contentColumns as $position => $content) {
606                                 $row[$i + $position] = $content;
607                             }
608                         }
609                     }
610                 }
611
612                 $lengths[] = $this->getCellWidth($row, $column);
613             }
614
615             $this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2;
616         }
617     }
618
619     /**
620      * Gets column width.
621      *
622      * @return int
623      */
624     private function getColumnSeparatorWidth()
625     {
626         return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar()));
627     }
628
629     /**
630      * Gets cell width.
631      *
632      * @param array $row
633      * @param int   $column
634      *
635      * @return int
636      */
637     private function getCellWidth(array $row, $column)
638     {
639         $cellWidth = 0;
640
641         if (isset($row[$column])) {
642             $cell = $row[$column];
643             $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
644         }
645
646         $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
647
648         return max($cellWidth, $columnWidth);
649     }
650
651     /**
652      * Called after rendering to cleanup cache data.
653      */
654     private function cleanup()
655     {
656         $this->effectiveColumnWidths = array();
657         $this->numberOfColumns = null;
658     }
659
660     private static function initStyles()
661     {
662         $borderless = new TableStyle();
663         $borderless
664             ->setHorizontalBorderChar('=')
665             ->setVerticalBorderChar(' ')
666             ->setCrossingChar(' ')
667         ;
668
669         $compact = new TableStyle();
670         $compact
671             ->setHorizontalBorderChar('')
672             ->setVerticalBorderChar(' ')
673             ->setCrossingChar('')
674             ->setCellRowContentFormat('%s')
675         ;
676
677         $styleGuide = new TableStyle();
678         $styleGuide
679             ->setHorizontalBorderChar('-')
680             ->setVerticalBorderChar(' ')
681             ->setCrossingChar(' ')
682             ->setCellHeaderFormat('%s')
683         ;
684
685         return array(
686             'default' => new TableStyle(),
687             'borderless' => $borderless,
688             'compact' => $compact,
689             'symfony-style-guide' => $styleGuide,
690         );
691     }
692
693     private function resolveStyle($name)
694     {
695         if ($name instanceof TableStyle) {
696             return $name;
697         }
698
699         if (isset(self::$styles[$name])) {
700             return self::$styles[$name];
701         }
702
703         throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
704     }
705 }