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