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