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