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 12namespace Symfony\Component\Console\Helper; 13 14use Symfony\Component\Console\Exception\InvalidArgumentException; 15use Symfony\Component\Console\Exception\RuntimeException; 16use Symfony\Component\Console\Formatter\OutputFormatter; 17use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface; 18use Symfony\Component\Console\Output\ConsoleSectionOutput; 19use Symfony\Component\Console\Output\OutputInterface; 20 21/** 22 * Provides helpers to display a table. 23 * 24 * @author Fabien Potencier <fabien@symfony.com> 25 * @author Саша Стаменковић <umpirsky@gmail.com> 26 * @author Abdellatif Ait boudad <a.aitboudad@gmail.com> 27 * @author Max Grigorian <maxakawizard@gmail.com> 28 * @author Dany Maillard <danymaillard93b@gmail.com> 29 */ 30class 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 * @param string $name The style name 107 */ 108 public static function setStyleDefinition($name, TableStyle $style) 109 { 110 if (!self::$styles) { 111 self::$styles = self::initStyles(); 112 } 113 114 self::$styles[$name] = $style; 115 } 116 117 /** 118 * Gets a style definition by name. 119 * 120 * @param string $name The style name 121 * 122 * @return TableStyle 123 */ 124 public static function getStyleDefinition($name) 125 { 126 if (!self::$styles) { 127 self::$styles = self::initStyles(); 128 } 129 130 if (isset(self::$styles[$name])) { 131 return self::$styles[$name]; 132 } 133 134 throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); 135 } 136 137 /** 138 * Sets table style. 139 * 140 * @param TableStyle|string $name The style name or a TableStyle instance 141 * 142 * @return $this 143 */ 144 public function setStyle($name) 145 { 146 $this->style = $this->resolveStyle($name); 147 148 return $this; 149 } 150 151 /** 152 * Gets the current table style. 153 * 154 * @return TableStyle 155 */ 156 public function getStyle() 157 { 158 return $this->style; 159 } 160 161 /** 162 * Sets table column style. 163 * 164 * @param int $columnIndex Column index 165 * @param TableStyle|string $name The style name or a TableStyle instance 166 * 167 * @return $this 168 */ 169 public function setColumnStyle($columnIndex, $name) 170 { 171 $columnIndex = (int) $columnIndex; 172 173 $this->columnStyles[$columnIndex] = $this->resolveStyle($name); 174 175 return $this; 176 } 177 178 /** 179 * Gets the current style for a column. 180 * 181 * If style was not set, it returns the global table style. 182 * 183 * @param int $columnIndex Column index 184 * 185 * @return TableStyle 186 */ 187 public function getColumnStyle($columnIndex) 188 { 189 return $this->columnStyles[$columnIndex] ?? $this->getStyle(); 190 } 191 192 /** 193 * Sets the minimum width of a column. 194 * 195 * @param int $columnIndex Column index 196 * @param int $width Minimum column width in characters 197 * 198 * @return $this 199 */ 200 public function setColumnWidth($columnIndex, $width) 201 { 202 $this->columnWidths[(int) $columnIndex] = (int) $width; 203 204 return $this; 205 } 206 207 /** 208 * Sets the minimum width of all columns. 209 * 210 * @return $this 211 */ 212 public function setColumnWidths(array $widths) 213 { 214 $this->columnWidths = []; 215 foreach ($widths as $index => $width) { 216 $this->setColumnWidth($index, $width); 217 } 218 219 return $this; 220 } 221 222 /** 223 * Sets the maximum width of a column. 224 * 225 * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while 226 * formatted strings are preserved. 227 * 228 * @return $this 229 */ 230 public function setColumnMaxWidth(int $columnIndex, int $width): self 231 { 232 if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) { 233 throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, \get_class($this->output->getFormatter()))); 234 } 235 236 $this->columnMaxWidths[$columnIndex] = $width; 237 238 return $this; 239 } 240 241 public function setHeaders(array $headers) 242 { 243 $headers = array_values($headers); 244 if (!empty($headers) && !\is_array($headers[0])) { 245 $headers = [$headers]; 246 } 247 248 $this->headers = $headers; 249 250 return $this; 251 } 252 253 public function setRows(array $rows) 254 { 255 $this->rows = []; 256 257 return $this->addRows($rows); 258 } 259 260 public function addRows(array $rows) 261 { 262 foreach ($rows as $row) { 263 $this->addRow($row); 264 } 265 266 return $this; 267 } 268 269 public function addRow($row) 270 { 271 if ($row instanceof TableSeparator) { 272 $this->rows[] = $row; 273 274 return $this; 275 } 276 277 if (!\is_array($row)) { 278 throw new InvalidArgumentException('A row must be an array or a TableSeparator instance.'); 279 } 280 281 $this->rows[] = array_values($row); 282 283 return $this; 284 } 285 286 /** 287 * Adds a row to the table, and re-renders the table. 288 */ 289 public function appendRow($row): self 290 { 291 if (!$this->output instanceof ConsoleSectionOutput) { 292 throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); 293 } 294 295 if ($this->rendered) { 296 $this->output->clear($this->calculateRowCount()); 297 } 298 299 $this->addRow($row); 300 $this->render(); 301 302 return $this; 303 } 304 305 public function setRow($column, array $row) 306 { 307 $this->rows[$column] = $row; 308 309 return $this; 310 } 311 312 public function setHeaderTitle(?string $title): self 313 { 314 $this->headerTitle = $title; 315 316 return $this; 317 } 318 319 public function setFooterTitle(?string $title): self 320 { 321 $this->footerTitle = $title; 322 323 return $this; 324 } 325 326 public function setHorizontal(bool $horizontal = true): self 327 { 328 $this->horizontal = $horizontal; 329 330 return $this; 331 } 332 333 /** 334 * Renders table to output. 335 * 336 * Example: 337 * 338 * +---------------+-----------------------+------------------+ 339 * | ISBN | Title | Author | 340 * +---------------+-----------------------+------------------+ 341 * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | 342 * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 343 * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | 344 * +---------------+-----------------------+------------------+ 345 */ 346 public function render() 347 { 348 $divider = new TableSeparator(); 349 if ($this->horizontal) { 350 $rows = []; 351 foreach ($this->headers[0] ?? [] as $i => $header) { 352 $rows[$i] = [$header]; 353 foreach ($this->rows as $row) { 354 if ($row instanceof TableSeparator) { 355 continue; 356 } 357 if (isset($row[$i])) { 358 $rows[$i][] = $row[$i]; 359 } elseif ($rows[$i][0] instanceof TableCell && $rows[$i][0]->getColspan() >= 2) { 360 // Noop, there is a "title" 361 } else { 362 $rows[$i][] = null; 363 } 364 } 365 } 366 } else { 367 $rows = array_merge($this->headers, [$divider], $this->rows); 368 } 369 370 $this->calculateNumberOfColumns($rows); 371 372 $rows = $this->buildTableRows($rows); 373 $this->calculateColumnsWidth($rows); 374 375 $isHeader = !$this->horizontal; 376 $isFirstRow = $this->horizontal; 377 foreach ($rows as $row) { 378 if ($divider === $row) { 379 $isHeader = false; 380 $isFirstRow = true; 381 382 continue; 383 } 384 if ($row instanceof TableSeparator) { 385 $this->renderRowSeparator(); 386 387 continue; 388 } 389 if (!$row) { 390 continue; 391 } 392 393 if ($isHeader || $isFirstRow) { 394 if ($isFirstRow) { 395 $this->renderRowSeparator(self::SEPARATOR_TOP_BOTTOM); 396 $isFirstRow = false; 397 } else { 398 $this->renderRowSeparator(self::SEPARATOR_TOP, $this->headerTitle, $this->style->getHeaderTitleFormat()); 399 } 400 } 401 if ($this->horizontal) { 402 $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat()); 403 } else { 404 $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); 405 } 406 } 407 $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat()); 408 409 $this->cleanup(); 410 $this->rendered = true; 411 } 412 413 /** 414 * Renders horizontal header separator. 415 * 416 * Example: 417 * 418 * +-----+-----------+-------+ 419 */ 420 private function renderRowSeparator(int $type = self::SEPARATOR_MID, string $title = null, string $titleFormat = null) 421 { 422 if (0 === $count = $this->numberOfColumns) { 423 return; 424 } 425 426 $borders = $this->style->getBorderChars(); 427 if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) { 428 return; 429 } 430 431 $crossings = $this->style->getCrossingChars(); 432 if (self::SEPARATOR_MID === $type) { 433 list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[2], $crossings[8], $crossings[0], $crossings[4]]; 434 } elseif (self::SEPARATOR_TOP === $type) { 435 list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[1], $crossings[2], $crossings[3]]; 436 } elseif (self::SEPARATOR_TOP_BOTTOM === $type) { 437 list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[9], $crossings[10], $crossings[11]]; 438 } else { 439 list($horizontal, $leftChar, $midChar, $rightChar) = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; 440 } 441 442 $markup = $leftChar; 443 for ($column = 0; $column < $count; ++$column) { 444 $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]); 445 $markup .= $column === $count - 1 ? $rightChar : $midChar; 446 } 447 448 if (null !== $title) { 449 $titleLength = Helper::strlenWithoutDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title)); 450 $markupLength = Helper::strlen($markup); 451 if ($titleLength > $limit = $markupLength - 4) { 452 $titleLength = $limit; 453 $formatLength = Helper::strlenWithoutDecoration($formatter, sprintf($titleFormat, '')); 454 $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...'); 455 } 456 457 $titleStart = ($markupLength - $titleLength) / 2; 458 if (false === mb_detect_encoding($markup, null, true)) { 459 $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength); 460 } else { 461 $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength); 462 } 463 } 464 465 $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); 466 } 467 468 /** 469 * Renders vertical column separator. 470 */ 471 private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string 472 { 473 $borders = $this->style->getBorderChars(); 474 475 return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]); 476 } 477 478 /** 479 * Renders table row. 480 * 481 * Example: 482 * 483 * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | 484 */ 485 private function renderRow(array $row, string $cellFormat, string $firstCellFormat = null) 486 { 487 $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE); 488 $columns = $this->getRowColumns($row); 489 $last = \count($columns) - 1; 490 foreach ($columns as $i => $column) { 491 if ($firstCellFormat && 0 === $i) { 492 $rowContent .= $this->renderCell($row, $column, $firstCellFormat); 493 } else { 494 $rowContent .= $this->renderCell($row, $column, $cellFormat); 495 } 496 $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE); 497 } 498 $this->output->writeln($rowContent); 499 } 500 501 /** 502 * Renders table cell with padding. 503 */ 504 private function renderCell(array $row, int $column, string $cellFormat): string 505 { 506 $cell = isset($row[$column]) ? $row[$column] : ''; 507 $width = $this->effectiveColumnWidths[$column]; 508 if ($cell instanceof TableCell && $cell->getColspan() > 1) { 509 // add the width of the following columns(numbers of colspan). 510 foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { 511 $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; 512 } 513 } 514 515 // str_pad won't work properly with multi-byte strings, we need to fix the padding 516 if (false !== $encoding = mb_detect_encoding($cell, null, true)) { 517 $width += \strlen($cell) - mb_strwidth($cell, $encoding); 518 } 519 520 $style = $this->getColumnStyle($column); 521 522 if ($cell instanceof TableSeparator) { 523 return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width)); 524 } 525 526 $width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); 527 $content = sprintf($style->getCellRowContentFormat(), $cell); 528 529 return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $style->getPadType())); 530 } 531 532 /** 533 * Calculate number of columns for this table. 534 */ 535 private function calculateNumberOfColumns(array $rows) 536 { 537 $columns = [0]; 538 foreach ($rows as $row) { 539 if ($row instanceof TableSeparator) { 540 continue; 541 } 542 543 $columns[] = $this->getNumberOfColumns($row); 544 } 545 546 $this->numberOfColumns = max($columns); 547 } 548 549 private function buildTableRows(array $rows): TableRows 550 { 551 /** @var WrappableOutputFormatterInterface $formatter */ 552 $formatter = $this->output->getFormatter(); 553 $unmergedRows = []; 554 for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { 555 $rows = $this->fillNextRows($rows, $rowKey); 556 557 // Remove any new line breaks and replace it with a new line 558 foreach ($rows[$rowKey] as $column => $cell) { 559 $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1; 560 561 if (isset($this->columnMaxWidths[$column]) && Helper::strlenWithoutDecoration($formatter, $cell) > $this->columnMaxWidths[$column]) { 562 $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan); 563 } 564 if (!strstr($cell, "\n")) { 565 continue; 566 } 567 $escaped = implode("\n", array_map([OutputFormatter::class, 'escapeTrailingBackslash'], explode("\n", $cell))); 568 $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; 569 $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell)); 570 foreach ($lines as $lineKey => $line) { 571 if ($colspan > 1) { 572 $line = new TableCell($line, ['colspan' => $colspan]); 573 } 574 if (0 === $lineKey) { 575 $rows[$rowKey][$column] = $line; 576 } else { 577 $unmergedRows[$rowKey][$lineKey][$column] = $line; 578 } 579 } 580 } 581 } 582 583 return new TableRows(function () use ($rows, $unmergedRows): \Traversable { 584 foreach ($rows as $rowKey => $row) { 585 yield $this->fillCells($row); 586 587 if (isset($unmergedRows[$rowKey])) { 588 foreach ($unmergedRows[$rowKey] as $row) { 589 yield $row; 590 } 591 } 592 } 593 }); 594 } 595 596 private function calculateRowCount(): int 597 { 598 $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows)))); 599 600 if ($this->headers) { 601 ++$numberOfRows; // Add row for header separator 602 } 603 604 if (\count($this->rows) > 0) { 605 ++$numberOfRows; // Add row for footer separator 606 } 607 608 return $numberOfRows; 609 } 610 611 /** 612 * fill rows that contains rowspan > 1. 613 * 614 * @throws InvalidArgumentException 615 */ 616 private function fillNextRows(array $rows, int $line): array 617 { 618 $unmergedRows = []; 619 foreach ($rows[$line] as $column => $cell) { 620 if (null !== $cell && !$cell instanceof TableCell && !is_scalar($cell) && !(\is_object($cell) && method_exists($cell, '__toString'))) { 621 throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', \gettype($cell))); 622 } 623 if ($cell instanceof TableCell && $cell->getRowspan() > 1) { 624 $nbLines = $cell->getRowspan() - 1; 625 $lines = [$cell]; 626 if (strstr($cell, "\n")) { 627 $lines = explode("\n", str_replace("\n", "<fg=default;bg=default>\n</>", $cell)); 628 $nbLines = \count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines; 629 630 $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan()]); 631 unset($lines[0]); 632 } 633 634 // create a two dimensional array (rowspan x colspan) 635 $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); 636 foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { 637 $value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : ''; 638 $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan()]); 639 if ($nbLines === $unmergedRowKey - $line) { 640 break; 641 } 642 } 643 } 644 } 645 646 foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { 647 // we need to know if $unmergedRow will be merged or inserted into $rows 648 if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { 649 foreach ($unmergedRow as $cellKey => $cell) { 650 // insert cell into row at cellKey position 651 array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); 652 } 653 } else { 654 $row = $this->copyRow($rows, $unmergedRowKey - 1); 655 foreach ($unmergedRow as $column => $cell) { 656 if (!empty($cell)) { 657 $row[$column] = $unmergedRow[$column]; 658 } 659 } 660 array_splice($rows, $unmergedRowKey, 0, [$row]); 661 } 662 } 663 664 return $rows; 665 } 666 667 /** 668 * fill cells for a row that contains colspan > 1. 669 */ 670 private function fillCells($row) 671 { 672 $newRow = []; 673 foreach ($row as $column => $cell) { 674 $newRow[] = $cell; 675 if ($cell instanceof TableCell && $cell->getColspan() > 1) { 676 foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { 677 // insert empty value at column position 678 $newRow[] = ''; 679 } 680 } 681 } 682 683 return $newRow ?: $row; 684 } 685 686 private function copyRow(array $rows, int $line): array 687 { 688 $row = $rows[$line]; 689 foreach ($row as $cellKey => $cellValue) { 690 $row[$cellKey] = ''; 691 if ($cellValue instanceof TableCell) { 692 $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]); 693 } 694 } 695 696 return $row; 697 } 698 699 /** 700 * Gets number of columns by row. 701 */ 702 private function getNumberOfColumns(array $row): int 703 { 704 $columns = \count($row); 705 foreach ($row as $column) { 706 $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; 707 } 708 709 return $columns; 710 } 711 712 /** 713 * Gets list of columns for the given row. 714 */ 715 private function getRowColumns(array $row): array 716 { 717 $columns = range(0, $this->numberOfColumns - 1); 718 foreach ($row as $cellKey => $cell) { 719 if ($cell instanceof TableCell && $cell->getColspan() > 1) { 720 // exclude grouped columns. 721 $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); 722 } 723 } 724 725 return $columns; 726 } 727 728 /** 729 * Calculates columns widths. 730 */ 731 private function calculateColumnsWidth(iterable $rows) 732 { 733 for ($column = 0; $column < $this->numberOfColumns; ++$column) { 734 $lengths = []; 735 foreach ($rows as $row) { 736 if ($row instanceof TableSeparator) { 737 continue; 738 } 739 740 foreach ($row as $i => $cell) { 741 if ($cell instanceof TableCell) { 742 $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); 743 $textLength = Helper::strlen($textContent); 744 if ($textLength > 0) { 745 $contentColumns = str_split($textContent, ceil($textLength / $cell->getColspan())); 746 foreach ($contentColumns as $position => $content) { 747 $row[$i + $position] = $content; 748 } 749 } 750 } 751 } 752 753 $lengths[] = $this->getCellWidth($row, $column); 754 } 755 756 $this->effectiveColumnWidths[$column] = max($lengths) + Helper::strlen($this->style->getCellRowContentFormat()) - 2; 757 } 758 } 759 760 private function getColumnSeparatorWidth(): int 761 { 762 return Helper::strlen(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3])); 763 } 764 765 private function getCellWidth(array $row, int $column): int 766 { 767 $cellWidth = 0; 768 769 if (isset($row[$column])) { 770 $cell = $row[$column]; 771 $cellWidth = Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell); 772 } 773 774 $columnWidth = isset($this->columnWidths[$column]) ? $this->columnWidths[$column] : 0; 775 $cellWidth = max($cellWidth, $columnWidth); 776 777 return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth; 778 } 779 780 /** 781 * Called after rendering to cleanup cache data. 782 */ 783 private function cleanup() 784 { 785 $this->effectiveColumnWidths = []; 786 $this->numberOfColumns = null; 787 } 788 789 private static function initStyles(): array 790 { 791 $borderless = new TableStyle(); 792 $borderless 793 ->setHorizontalBorderChars('=') 794 ->setVerticalBorderChars(' ') 795 ->setDefaultCrossingChar(' ') 796 ; 797 798 $compact = new TableStyle(); 799 $compact 800 ->setHorizontalBorderChars('') 801 ->setVerticalBorderChars(' ') 802 ->setDefaultCrossingChar('') 803 ->setCellRowContentFormat('%s') 804 ; 805 806 $styleGuide = new TableStyle(); 807 $styleGuide 808 ->setHorizontalBorderChars('-') 809 ->setVerticalBorderChars(' ') 810 ->setDefaultCrossingChar(' ') 811 ->setCellHeaderFormat('%s') 812 ; 813 814 $box = (new TableStyle()) 815 ->setHorizontalBorderChars('─') 816 ->setVerticalBorderChars('│') 817 ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├') 818 ; 819 820 $boxDouble = (new TableStyle()) 821 ->setHorizontalBorderChars('═', '─') 822 ->setVerticalBorderChars('║', '│') 823 ->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣') 824 ; 825 826 return [ 827 'default' => new TableStyle(), 828 'borderless' => $borderless, 829 'compact' => $compact, 830 'symfony-style-guide' => $styleGuide, 831 'box' => $box, 832 'box-double' => $boxDouble, 833 ]; 834 } 835 836 private function resolveStyle($name): TableStyle 837 { 838 if ($name instanceof TableStyle) { 839 return $name; 840 } 841 842 if (isset(self::$styles[$name])) { 843 return self::$styles[$name]; 844 } 845 846 throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); 847 } 848} 849