81d0e427f473df57b4854abc44ab9ce90573e004
[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\Output\OutputInterface;
15 use Symfony\Component\Console\Exception\InvalidArgumentException;
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      * <code>
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      * </code>
275      */
276     public function render()
277     {
278         $this->calculateNumberOfColumns();
279         $rows = $this->buildTableRows($this->rows);
280         $headers = $this->buildTableRows($this->headers);
281
282         $this->calculateColumnsWidth(array_merge($headers, $rows));
283
284         $this->renderRowSeparator();
285         if (!empty($headers)) {
286             foreach ($headers as $header) {
287                 $this->renderRow($header, $this->style->getCellHeaderFormat());
288                 $this->renderRowSeparator();
289             }
290         }
291         foreach ($rows as $row) {
292             if ($row instanceof TableSeparator) {
293                 $this->renderRowSeparator();
294             } else {
295                 $this->renderRow($row, $this->style->getCellRowFormat());
296             }
297         }
298         if (!empty($rows)) {
299             $this->renderRowSeparator();
300         }
301
302         $this->cleanup();
303     }
304
305     /**
306      * Renders horizontal header separator.
307      *
308      * Example: <code>+-----+-----------+-------+</code>
309      */
310     private function renderRowSeparator()
311     {
312         if (0 === $count = $this->numberOfColumns) {
313             return;
314         }
315
316         if (!$this->style->getHorizontalBorderChar() && !$this->style->getCrossingChar()) {
317             return;
318         }
319
320         $markup = $this->style->getCrossingChar();
321         for ($column = 0; $column < $count; ++$column) {
322             $markup .= str_repeat($this->style->getHorizontalBorderChar(), $this->effectiveColumnWidths[$column]).$this->style->getCrossingChar();
323         }
324
325         $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup));
326     }
327
328     /**
329      * Renders vertical column separator.
330      */
331     private function renderColumnSeparator()
332     {
333         return sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar());
334     }
335
336     /**
337      * Renders table row.
338      *
339      * Example: <code>| 9971-5-0210-0 | A Tale of Two Cities  | Charles Dickens  |</code>
340      *
341      * @param array  $row
342      * @param string $cellFormat
343      */
344     private function renderRow(array $row, $cellFormat)
345     {
346         if (empty($row)) {
347             return;
348         }
349
350         $rowContent = $this->renderColumnSeparator();
351         foreach ($this->getRowColumns($row) as $column) {
352             $rowContent .= $this->renderCell($row, $column, $cellFormat);
353             $rowContent .= $this->renderColumnSeparator();
354         }
355         $this->output->writeln($rowContent);
356     }
357
358     /**
359      * Renders table cell with padding.
360      *
361      * @param array  $row
362      * @param int    $column
363      * @param string $cellFormat
364      */
365     private function renderCell(array $row, $column, $cellFormat)
366     {
367         $cell = isset($row[$column]) ? $row[$column] : '';
368         $width = $this->effectiveColumnWidths[$column];
369         if ($cell instanceof TableCell && $cell->getColspan() > 1) {
370             // add the width of the following columns(numbers of colspan).
371             foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
372                 $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn];
373             }
374         }
375
376         // str_pad won't work properly with multi-byte strings, we need to fix the padding
377         if (false !== $encoding = mb_detect_encoding($cell, null, true)) {
378             $width += strlen($cell) - mb_strwidth($cell, $encoding);
379         }
380
381         $style = $this->getColumnStyle($column);
382
383         if ($cell instanceof TableSeparator) {
384             return sprintf($style->getBorderFormat(), str_repeat($style->getHorizontalBorderChar(), $width));
385         }
386
387         $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
388         $content = sprintf($style->getCellRowContentFormat(), $cell);
389
390         return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType()));
391     }
392
393     /**
394      * Calculate number of columns for this table.
395      */
396     private function calculateNumberOfColumns()
397     {
398         if (null !== $this->numberOfColumns) {
399             return;
400         }
401
402         $columns = array(0);
403         foreach (array_merge($this->headers, $this->rows) as $row) {
404             if ($row instanceof TableSeparator) {
405                 continue;
406             }
407
408             $columns[] = $this->getNumberOfColumns($row);
409         }
410
411         $this->numberOfColumns = max($columns);
412     }
413
414     private function buildTableRows($rows)
415     {
416         $unmergedRows = array();
417         for ($rowKey = 0; $rowKey < count($rows); ++$rowKey) {
418             $rows = $this->fillNextRows($rows, $rowKey);
419
420             // Remove any new line breaks and replace it with a new line
421             foreach ($rows[$rowKey] as $column => $cell) {
422                 if (!strstr($cell, "\n")) {
423                     continue;
424                 }
425                 $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
426                 foreach ($lines as $lineKey => $line) {
427                     if ($cell instanceof TableCell) {
428                         $line = new TableCell($line, array('colspan' => $cell->getColspan()));
429                     }
430                     if (0 === $lineKey) {
431                         $rows[$rowKey][$column] = $line;
432                     } else {
433                         $unmergedRows[$rowKey][$lineKey][$column] = $line;
434                     }
435                 }
436             }
437         }
438
439         $tableRows = array();
440         foreach ($rows as $rowKey => $row) {
441             $tableRows[] = $this->fillCells($row);
442             if (isset($unmergedRows[$rowKey])) {
443                 $tableRows = array_merge($tableRows, $unmergedRows[$rowKey]);
444             }
445         }
446
447         return $tableRows;
448     }
449
450     /**
451      * fill rows that contains rowspan > 1.
452      *
453      * @param array $rows
454      * @param int   $line
455      *
456      * @return array
457      *
458      * @throws InvalidArgumentException
459      */
460     private function fillNextRows(array $rows, $line)
461     {
462         $unmergedRows = array();
463         foreach ($rows[$line] as $column => $cell) {
464             if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(is_object($cell) && method_exists($cell, '__toString'))) {
465                 throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing __toString, %s given.', gettype($cell)));
466             }
467             if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
468                 $nbLines = $cell->getRowspan() - 1;
469                 $lines = array($cell);
470                 if (strstr($cell, "\n")) {
471                     $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell));
472                     $nbLines = count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
473
474                     $rows[$line][$column] = new TableCell($lines[0], array('colspan' => $cell->getColspan()));
475                     unset($lines[0]);
476                 }
477
478                 // create a two dimensional array (rowspan x colspan)
479                 $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, array()), $unmergedRows);
480                 foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
481                     $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : '';
482                     $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, array('colspan' => $cell->getColspan()));
483                     if ($nbLines === $unmergedRowKey - $line) {
484                         break;
485                     }
486                 }
487             }
488         }
489
490         foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
491             // we need to know if $unmergedRow will be merged or inserted into $rows
492             if (isset($rows[$unmergedRowKey]) && is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
493                 foreach ($unmergedRow as $cellKey => $cell) {
494                     // insert cell into row at cellKey position
495                     array_splice($rows[$unmergedRowKey], $cellKey, 0, array($cell));
496                 }
497             } else {
498                 $row = $this->copyRow($rows, $unmergedRowKey - 1);
499                 foreach ($unmergedRow as $column => $cell) {
500                     if (!empty($cell)) {
501                         $row[$column] = $unmergedRow[$column];
502                     }
503                 }
504                 array_splice($rows, $unmergedRowKey, 0, array($row));
505             }
506         }
507
508         return $rows;
509     }
510
511     /**
512      * fill cells for a row that contains colspan > 1.
513      *
514      * @return array
515      */
516     private function fillCells($row)
517     {
518         $newRow = array();
519         foreach ($row as $column => $cell) {
520             $newRow[] = $cell;
521             if ($cell instanceof TableCell && $cell->getColspan() > 1) {
522                 foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
523                     // insert empty value at column position
524                     $newRow[] = '';
525                 }
526             }
527         }
528
529         return $newRow ?: $row;
530     }
531
532     /**
533      * @param array $rows
534      * @param int   $line
535      *
536      * @return array
537      */
538     private function copyRow(array $rows, $line)
539     {
540         $row = $rows[$line];
541         foreach ($row as $cellKey => $cellValue) {
542             $row[$cellKey] = '';
543             if ($cellValue instanceof TableCell) {
544                 $row[$cellKey] = new TableCell('', array('colspan' => $cellValue->getColspan()));
545             }
546         }
547
548         return $row;
549     }
550
551     /**
552      * Gets number of columns by row.
553      *
554      * @return int
555      */
556     private function getNumberOfColumns(array $row)
557     {
558         $columns = count($row);
559         foreach ($row as $column) {
560             $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0;
561         }
562
563         return $columns;
564     }
565
566     /**
567      * Gets list of columns for the given row.
568      *
569      * @return array
570      */
571     private function getRowColumns(array $row)
572     {
573         $columns = range(0, $this->numberOfColumns - 1);
574         foreach ($row as $cellKey => $cell) {
575             if ($cell instanceof TableCell && $cell->getColspan() > 1) {
576                 // exclude grouped columns.
577                 $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1));
578             }
579         }
580
581         return $columns;
582     }
583
584     /**
585      * Calculates columns widths.
586      */
587     private function calculateColumnsWidth(array $rows)
588     {
589         for ($column = 0; $column < $this->numberOfColumns; ++$column) {
590             $lengths = array();
591             foreach ($rows as $row) {
592                 if ($row instanceof TableSeparator) {
593                     continue;
594                 }
595
596                 foreach ($row as $i => $cell) {
597                     if ($cell instanceof TableCell) {
598                         $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell);
599                         $textLength = Helper::strlen($textContent);
600                         if ($textLength > 0) {
601                             $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan()));
602                             foreach ($contentColumns as $position => $content) {
603                                 $row[$i + $position] = $content;
604                             }
605                         }
606                     }
607                 }
608
609                 $lengths[] = $this->getCellWidth($row, $column);
610             }
611
612             $this->effectiveColumnWidths[$column] = max($lengths) + strlen($this->style->getCellRowContentFormat()) - 2;
613         }
614     }
615
616     /**
617      * Gets column width.
618      *
619      * @return int
620      */
621     private function getColumnSeparatorWidth()
622     {
623         return strlen(sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar()));
624     }
625
626     /**
627      * Gets cell width.
628      *
629      * @param array $row
630      * @param int   $column
631      *
632      * @return int
633      */
634     private function getCellWidth(array $row, $column)
635     {
636         $cellWidth = 0;
637
638         if (isset($row[$column])) {
639             $cell = $row[$column];
640             $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
641         }
642
643         $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0;
644
645         return max($cellWidth, $columnWidth);
646     }
647
648     /**
649      * Called after rendering to cleanup cache data.
650      */
651     private function cleanup()
652     {
653         $this->effectiveColumnWidths = array();
654         $this->numberOfColumns = null;
655     }
656
657     private static function initStyles()
658     {
659         $borderless = new TableStyle();
660         $borderless
661             ->setHorizontalBorderChar('=')
662             ->setVerticalBorderChar(' ')
663             ->setCrossingChar(' ')
664         ;
665
666         $compact = new TableStyle();
667         $compact
668             ->setHorizontalBorderChar('')
669             ->setVerticalBorderChar(' ')
670             ->setCrossingChar('')
671             ->setCellRowContentFormat('%s')
672         ;
673
674         $styleGuide = new TableStyle();
675         $styleGuide
676             ->setHorizontalBorderChar('-')
677             ->setVerticalBorderChar(' ')
678             ->setCrossingChar(' ')
679             ->setCellHeaderFormat('%s')
680         ;
681
682         return array(
683             'default' => new TableStyle(),
684             'borderless' => $borderless,
685             'compact' => $compact,
686             'symfony-style-guide' => $styleGuide,
687         );
688     }
689
690     private function resolveStyle($name)
691     {
692         if ($name instanceof TableStyle) {
693             return $name;
694         }
695
696         if (isset(self::$styles[$name])) {
697             return self::$styles[$name];
698         }
699
700         throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name));
701     }
702 }