Total Complexity | 177 |
Total Lines | 893 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like Table often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Table, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
30 | class 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 | private const DISPLAY_ORIENTATION_DEFAULT = 'default'; |
||
39 | private const DISPLAY_ORIENTATION_HORIZONTAL = 'horizontal'; |
||
40 | private const DISPLAY_ORIENTATION_VERTICAL = 'vertical'; |
||
41 | |||
42 | private ?string $headerTitle = null; |
||
43 | private ?string $footerTitle = null; |
||
44 | private array $headers = []; |
||
45 | private array $rows = []; |
||
46 | private array $effectiveColumnWidths = []; |
||
47 | private int $numberOfColumns; |
||
48 | private TableStyle $style; |
||
49 | private array $columnStyles = []; |
||
50 | private array $columnWidths = []; |
||
51 | private array $columnMaxWidths = []; |
||
52 | private bool $rendered = false; |
||
53 | private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT; |
||
54 | |||
55 | private static array $styles; |
||
56 | |||
57 | public function __construct( |
||
58 | private OutputInterface $output, |
||
59 | ) { |
||
60 | self::$styles ??= self::initStyles(); |
||
61 | |||
62 | $this->setStyle('default'); |
||
63 | } |
||
64 | |||
65 | /** |
||
66 | * Sets a style definition. |
||
67 | */ |
||
68 | public static function setStyleDefinition(string $name, TableStyle $style): void |
||
69 | { |
||
70 | self::$styles ??= self::initStyles(); |
||
71 | |||
72 | self::$styles[$name] = $style; |
||
73 | } |
||
74 | |||
75 | /** |
||
76 | * Gets a style definition by name. |
||
77 | */ |
||
78 | public static function getStyleDefinition(string $name): TableStyle |
||
79 | { |
||
80 | self::$styles ??= self::initStyles(); |
||
81 | |||
82 | return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); |
||
83 | } |
||
84 | |||
85 | /** |
||
86 | * Sets table style. |
||
87 | * |
||
88 | * @return $this |
||
89 | */ |
||
90 | public function setStyle(TableStyle|string $name): static |
||
91 | { |
||
92 | $this->style = $this->resolveStyle($name); |
||
93 | |||
94 | return $this; |
||
95 | } |
||
96 | |||
97 | /** |
||
98 | * Gets the current table style. |
||
99 | */ |
||
100 | public function getStyle(): TableStyle |
||
101 | { |
||
102 | return $this->style; |
||
103 | } |
||
104 | |||
105 | /** |
||
106 | * Sets table column style. |
||
107 | * |
||
108 | * @param TableStyle|string $name The style name or a TableStyle instance |
||
109 | * |
||
110 | * @return $this |
||
111 | */ |
||
112 | public function setColumnStyle(int $columnIndex, TableStyle|string $name): static |
||
113 | { |
||
114 | $this->columnStyles[$columnIndex] = $this->resolveStyle($name); |
||
115 | |||
116 | return $this; |
||
117 | } |
||
118 | |||
119 | /** |
||
120 | * Gets the current style for a column. |
||
121 | * |
||
122 | * If style was not set, it returns the global table style. |
||
123 | */ |
||
124 | public function getColumnStyle(int $columnIndex): TableStyle |
||
125 | { |
||
126 | return $this->columnStyles[$columnIndex] ?? $this->getStyle(); |
||
127 | } |
||
128 | |||
129 | /** |
||
130 | * Sets the minimum width of a column. |
||
131 | * |
||
132 | * @return $this |
||
133 | */ |
||
134 | public function setColumnWidth(int $columnIndex, int $width): static |
||
135 | { |
||
136 | $this->columnWidths[$columnIndex] = $width; |
||
137 | |||
138 | return $this; |
||
139 | } |
||
140 | |||
141 | /** |
||
142 | * Sets the minimum width of all columns. |
||
143 | * |
||
144 | * @return $this |
||
145 | */ |
||
146 | public function setColumnWidths(array $widths): static |
||
154 | } |
||
155 | |||
156 | /** |
||
157 | * Sets the maximum width of a column. |
||
158 | * |
||
159 | * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while |
||
160 | * formatted strings are preserved. |
||
161 | * |
||
162 | * @return $this |
||
163 | */ |
||
164 | public function setColumnMaxWidth(int $columnIndex, int $width): static |
||
165 | { |
||
166 | if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) { |
||
167 | 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()))); |
||
168 | } |
||
169 | |||
170 | $this->columnMaxWidths[$columnIndex] = $width; |
||
171 | |||
172 | return $this; |
||
173 | } |
||
174 | |||
175 | /** |
||
176 | * @return $this |
||
177 | */ |
||
178 | public function setHeaders(array $headers): static |
||
179 | { |
||
180 | $headers = array_values($headers); |
||
181 | if ($headers && !\is_array($headers[0])) { |
||
|
|||
182 | $headers = [$headers]; |
||
183 | } |
||
184 | |||
185 | $this->headers = $headers; |
||
186 | |||
187 | return $this; |
||
188 | } |
||
189 | |||
190 | /** |
||
191 | * @return $this |
||
192 | */ |
||
193 | public function setRows(array $rows): static |
||
194 | { |
||
195 | $this->rows = []; |
||
196 | |||
197 | return $this->addRows($rows); |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * @return $this |
||
202 | */ |
||
203 | public function addRows(array $rows): static |
||
204 | { |
||
205 | foreach ($rows as $row) { |
||
206 | $this->addRow($row); |
||
207 | } |
||
208 | |||
209 | return $this; |
||
210 | } |
||
211 | |||
212 | /** |
||
213 | * @return $this |
||
214 | */ |
||
215 | public function addRow(TableSeparator|array $row): static |
||
216 | { |
||
217 | if ($row instanceof TableSeparator) { |
||
218 | $this->rows[] = $row; |
||
219 | |||
220 | return $this; |
||
221 | } |
||
222 | |||
223 | $this->rows[] = array_values($row); |
||
224 | |||
225 | return $this; |
||
226 | } |
||
227 | |||
228 | /** |
||
229 | * Adds a row to the table, and re-renders the table. |
||
230 | * |
||
231 | * @return $this |
||
232 | */ |
||
233 | public function appendRow(TableSeparator|array $row): static |
||
234 | { |
||
235 | if (!$this->output instanceof ConsoleSectionOutput) { |
||
236 | throw new RuntimeException(sprintf('Output should be an instance of "%s" when calling "%s".', ConsoleSectionOutput::class, __METHOD__)); |
||
237 | } |
||
238 | |||
239 | if ($this->rendered) { |
||
240 | $this->output->clear($this->calculateRowCount()); |
||
241 | } |
||
242 | |||
243 | $this->addRow($row); |
||
244 | $this->render(); |
||
245 | |||
246 | return $this; |
||
247 | } |
||
248 | |||
249 | /** |
||
250 | * @return $this |
||
251 | */ |
||
252 | public function setRow(int|string $column, array $row): static |
||
253 | { |
||
254 | $this->rows[$column] = $row; |
||
255 | |||
256 | return $this; |
||
257 | } |
||
258 | |||
259 | /** |
||
260 | * @return $this |
||
261 | */ |
||
262 | public function setHeaderTitle(?string $title): static |
||
263 | { |
||
264 | $this->headerTitle = $title; |
||
265 | |||
266 | return $this; |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * @return $this |
||
271 | */ |
||
272 | public function setFooterTitle(?string $title): static |
||
273 | { |
||
274 | $this->footerTitle = $title; |
||
275 | |||
276 | return $this; |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * @return $this |
||
281 | */ |
||
282 | public function setHorizontal(bool $horizontal = true): static |
||
283 | { |
||
284 | $this->displayOrientation = $horizontal ? self::DISPLAY_ORIENTATION_HORIZONTAL : self::DISPLAY_ORIENTATION_DEFAULT; |
||
285 | |||
286 | return $this; |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * @return $this |
||
291 | */ |
||
292 | public function setVertical(bool $vertical = true): static |
||
293 | { |
||
294 | $this->displayOrientation = $vertical ? self::DISPLAY_ORIENTATION_VERTICAL : self::DISPLAY_ORIENTATION_DEFAULT; |
||
295 | |||
296 | return $this; |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * Renders table to output. |
||
301 | * |
||
302 | * Example: |
||
303 | * |
||
304 | * +---------------+-----------------------+------------------+ |
||
305 | * | ISBN | Title | Author | |
||
306 | * +---------------+-----------------------+------------------+ |
||
307 | * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | |
||
308 | * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | |
||
309 | * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | |
||
310 | * +---------------+-----------------------+------------------+ |
||
311 | */ |
||
312 | public function render(): void |
||
313 | { |
||
314 | $divider = new TableSeparator(); |
||
315 | $isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2; |
||
316 | |||
317 | $horizontal = self::DISPLAY_ORIENTATION_HORIZONTAL === $this->displayOrientation; |
||
318 | $vertical = self::DISPLAY_ORIENTATION_VERTICAL === $this->displayOrientation; |
||
319 | |||
320 | $rows = []; |
||
321 | if ($horizontal) { |
||
322 | foreach ($this->headers[0] ?? [] as $i => $header) { |
||
323 | $rows[$i] = [$header]; |
||
324 | foreach ($this->rows as $row) { |
||
325 | if ($row instanceof TableSeparator) { |
||
326 | continue; |
||
327 | } |
||
328 | if (isset($row[$i])) { |
||
329 | $rows[$i][] = $row[$i]; |
||
330 | } elseif ($isCellWithColspan($rows[$i][0])) { |
||
331 | // Noop, there is a "title" |
||
332 | } else { |
||
333 | $rows[$i][] = null; |
||
334 | } |
||
335 | } |
||
336 | } |
||
337 | } elseif ($vertical) { |
||
338 | $formatter = $this->output->getFormatter(); |
||
339 | $maxHeaderLength = array_reduce($this->headers[0] ?? [], static fn ($max, $header) => max($max, Helper::width(Helper::removeDecoration($formatter, $header))), 0); |
||
340 | |||
341 | foreach ($this->rows as $row) { |
||
342 | if ($row instanceof TableSeparator) { |
||
343 | continue; |
||
344 | } |
||
345 | |||
346 | if ($rows) { |
||
347 | $rows[] = [$divider]; |
||
348 | } |
||
349 | |||
350 | $containsColspan = false; |
||
351 | foreach ($row as $cell) { |
||
352 | if ($containsColspan = $isCellWithColspan($cell)) { |
||
353 | break; |
||
354 | } |
||
355 | } |
||
356 | |||
357 | $headers = $this->headers[0] ?? []; |
||
358 | $maxRows = max(\count($headers), \count($row)); |
||
359 | for ($i = 0; $i < $maxRows; ++$i) { |
||
360 | $cell = (string) ($row[$i] ?? ''); |
||
361 | |||
362 | $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; |
||
363 | $parts = explode($eol, $cell); |
||
364 | foreach ($parts as $idx => $part) { |
||
365 | if ($headers && !$containsColspan) { |
||
366 | if (0 === $idx) { |
||
367 | $rows[] = [sprintf( |
||
368 | '<comment>%s%s</>: %s', |
||
369 | str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))), |
||
370 | $headers[$i] ?? '', |
||
371 | $part |
||
372 | )]; |
||
373 | } else { |
||
374 | $rows[] = [sprintf( |
||
375 | '%s %s', |
||
376 | str_pad('', $maxHeaderLength, ' ', \STR_PAD_LEFT), |
||
377 | $part |
||
378 | )]; |
||
379 | } |
||
380 | } elseif ('' !== $cell) { |
||
381 | $rows[] = [$part]; |
||
382 | } |
||
383 | } |
||
384 | } |
||
385 | } |
||
386 | } else { |
||
387 | $rows = array_merge($this->headers, [$divider], $this->rows); |
||
388 | } |
||
389 | |||
390 | $this->calculateNumberOfColumns($rows); |
||
391 | |||
392 | $rowGroups = $this->buildTableRows($rows); |
||
393 | $this->calculateColumnsWidth($rowGroups); |
||
394 | |||
395 | $isHeader = !$horizontal; |
||
396 | $isFirstRow = $horizontal; |
||
397 | $hasTitle = (bool) $this->headerTitle; |
||
398 | |||
399 | foreach ($rowGroups as $rowGroup) { |
||
400 | $isHeaderSeparatorRendered = false; |
||
401 | |||
402 | foreach ($rowGroup as $row) { |
||
403 | if ($divider === $row) { |
||
404 | $isHeader = false; |
||
405 | $isFirstRow = true; |
||
406 | |||
407 | continue; |
||
408 | } |
||
409 | |||
410 | if ($row instanceof TableSeparator) { |
||
411 | $this->renderRowSeparator(); |
||
412 | |||
413 | continue; |
||
414 | } |
||
415 | |||
416 | if (!$row) { |
||
417 | continue; |
||
418 | } |
||
419 | |||
420 | if ($isHeader && !$isHeaderSeparatorRendered) { |
||
421 | $this->renderRowSeparator( |
||
422 | self::SEPARATOR_TOP, |
||
423 | $hasTitle ? $this->headerTitle : null, |
||
424 | $hasTitle ? $this->style->getHeaderTitleFormat() : null |
||
425 | ); |
||
426 | $hasTitle = false; |
||
427 | $isHeaderSeparatorRendered = true; |
||
428 | } |
||
429 | |||
430 | if ($isFirstRow) { |
||
431 | $this->renderRowSeparator( |
||
432 | $horizontal ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, |
||
433 | $hasTitle ? $this->headerTitle : null, |
||
434 | $hasTitle ? $this->style->getHeaderTitleFormat() : null |
||
435 | ); |
||
436 | $isFirstRow = false; |
||
437 | $hasTitle = false; |
||
438 | } |
||
439 | |||
440 | if ($vertical) { |
||
441 | $isHeader = false; |
||
442 | $isFirstRow = false; |
||
443 | } |
||
444 | |||
445 | if ($horizontal) { |
||
446 | $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat()); |
||
447 | } else { |
||
448 | $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); |
||
449 | } |
||
450 | } |
||
451 | } |
||
452 | $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat()); |
||
453 | |||
454 | $this->cleanup(); |
||
455 | $this->rendered = true; |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * Renders horizontal header separator. |
||
460 | * |
||
461 | * Example: |
||
462 | * |
||
463 | * +-----+-----------+-------+ |
||
464 | */ |
||
465 | private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null): void |
||
466 | { |
||
467 | if (!$count = $this->numberOfColumns) { |
||
468 | return; |
||
469 | } |
||
470 | |||
471 | $borders = $this->style->getBorderChars(); |
||
472 | if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) { |
||
473 | return; |
||
474 | } |
||
475 | |||
476 | $crossings = $this->style->getCrossingChars(); |
||
477 | if (self::SEPARATOR_MID === $type) { |
||
478 | [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]]; |
||
479 | } elseif (self::SEPARATOR_TOP === $type) { |
||
480 | [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]]; |
||
481 | } elseif (self::SEPARATOR_TOP_BOTTOM === $type) { |
||
482 | [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]]; |
||
483 | } else { |
||
484 | [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; |
||
485 | } |
||
486 | |||
487 | $markup = $leftChar; |
||
488 | for ($column = 0; $column < $count; ++$column) { |
||
489 | $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]); |
||
490 | $markup .= $column === $count - 1 ? $rightChar : $midChar; |
||
491 | } |
||
492 | |||
493 | if (null !== $title) { |
||
494 | $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title))); |
||
495 | $markupLength = Helper::width($markup); |
||
496 | if ($titleLength > $limit = $markupLength - 4) { |
||
497 | $titleLength = $limit; |
||
498 | $formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, ''))); |
||
499 | $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...'); |
||
500 | } |
||
501 | |||
502 | $titleStart = intdiv($markupLength - $titleLength, 2); |
||
503 | if (false === mb_detect_encoding($markup, null, true)) { |
||
504 | $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength); |
||
505 | } else { |
||
506 | $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength); |
||
507 | } |
||
508 | } |
||
509 | |||
510 | $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); |
||
511 | } |
||
512 | |||
513 | /** |
||
514 | * Renders vertical column separator. |
||
515 | */ |
||
516 | private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string |
||
517 | { |
||
518 | $borders = $this->style->getBorderChars(); |
||
519 | |||
520 | return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]); |
||
521 | } |
||
522 | |||
523 | /** |
||
524 | * Renders table row. |
||
525 | * |
||
526 | * Example: |
||
527 | * |
||
528 | * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | |
||
529 | */ |
||
530 | private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null): void |
||
531 | { |
||
532 | $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE); |
||
533 | $columns = $this->getRowColumns($row); |
||
534 | $last = \count($columns) - 1; |
||
535 | foreach ($columns as $i => $column) { |
||
536 | if ($firstCellFormat && 0 === $i) { |
||
537 | $rowContent .= $this->renderCell($row, $column, $firstCellFormat); |
||
538 | } else { |
||
539 | $rowContent .= $this->renderCell($row, $column, $cellFormat); |
||
540 | } |
||
541 | $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE); |
||
542 | } |
||
543 | $this->output->writeln($rowContent); |
||
544 | } |
||
545 | |||
546 | /** |
||
547 | * Renders table cell with padding. |
||
548 | */ |
||
549 | private function renderCell(array $row, int $column, string $cellFormat): string |
||
550 | { |
||
551 | $cell = $row[$column] ?? ''; |
||
552 | $width = $this->effectiveColumnWidths[$column]; |
||
553 | if ($cell instanceof TableCell && $cell->getColspan() > 1) { |
||
554 | // add the width of the following columns(numbers of colspan). |
||
555 | foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { |
||
556 | $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; |
||
557 | } |
||
558 | } |
||
559 | |||
560 | // str_pad won't work properly with multi-byte strings, we need to fix the padding |
||
561 | if (false !== $encoding = mb_detect_encoding($cell, null, true)) { |
||
562 | $width += \strlen($cell) - mb_strwidth($cell, $encoding); |
||
563 | } |
||
564 | |||
565 | $style = $this->getColumnStyle($column); |
||
566 | |||
567 | if ($cell instanceof TableSeparator) { |
||
568 | return sprintf($style->getBorderFormat(), str_repeat($style->getBorderChars()[2], $width)); |
||
569 | } |
||
570 | |||
571 | $width += Helper::length($cell) - Helper::length(Helper::removeDecoration($this->output->getFormatter(), $cell)); |
||
572 | $content = sprintf($style->getCellRowContentFormat(), $cell); |
||
573 | |||
574 | $padType = $style->getPadType(); |
||
575 | if ($cell instanceof TableCell && $cell->getStyle() instanceof TableCellStyle) { |
||
576 | $isNotStyledByTag = !preg_match('/^<(\w+|(\w+=[\w,]+;?)*)>.+<\/(\w+|(\w+=\w+;?)*)?>$/', $cell); |
||
577 | if ($isNotStyledByTag) { |
||
578 | $cellFormat = $cell->getStyle()->getCellFormat(); |
||
579 | if (!\is_string($cellFormat)) { |
||
580 | $tag = http_build_query($cell->getStyle()->getTagOptions(), '', ';'); |
||
581 | $cellFormat = '<'.$tag.'>%s</>'; |
||
582 | } |
||
583 | |||
584 | if (str_contains($content, '</>')) { |
||
585 | $content = str_replace('</>', '', $content); |
||
586 | $width -= 3; |
||
587 | } |
||
588 | if (str_contains($content, '<fg=default;bg=default>')) { |
||
589 | $content = str_replace('<fg=default;bg=default>', '', $content); |
||
590 | $width -= \strlen('<fg=default;bg=default>'); |
||
591 | } |
||
592 | } |
||
593 | |||
594 | $padType = $cell->getStyle()->getPadByAlign(); |
||
595 | } |
||
596 | |||
597 | return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType)); |
||
598 | } |
||
599 | |||
600 | /** |
||
601 | * Calculate number of columns for this table. |
||
602 | */ |
||
603 | private function calculateNumberOfColumns(array $rows): void |
||
604 | { |
||
605 | $columns = [0]; |
||
606 | foreach ($rows as $row) { |
||
607 | if ($row instanceof TableSeparator) { |
||
608 | continue; |
||
609 | } |
||
610 | |||
611 | $columns[] = $this->getNumberOfColumns($row); |
||
612 | } |
||
613 | |||
614 | $this->numberOfColumns = max($columns); |
||
615 | } |
||
616 | |||
617 | private function buildTableRows(array $rows): TableRows |
||
665 | } |
||
666 | }); |
||
667 | } |
||
668 | |||
669 | private function calculateRowCount(): int |
||
670 | { |
||
671 | $numberOfRows = \count(iterator_to_array($this->buildTableRows(array_merge($this->headers, [new TableSeparator()], $this->rows)))); |
||
672 | |||
673 | if ($this->headers) { |
||
674 | ++$numberOfRows; // Add row for header separator |
||
675 | } |
||
676 | |||
677 | if ($this->rows) { |
||
678 | ++$numberOfRows; // Add row for footer separator |
||
679 | } |
||
680 | |||
681 | return $numberOfRows; |
||
682 | } |
||
683 | |||
684 | /** |
||
685 | * fill rows that contains rowspan > 1. |
||
686 | * |
||
687 | * @throws InvalidArgumentException |
||
688 | */ |
||
689 | private function fillNextRows(array $rows, int $line): array |
||
690 | { |
||
691 | $unmergedRows = []; |
||
692 | foreach ($rows[$line] as $column => $cell) { |
||
693 | if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) { |
||
694 | throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell))); |
||
695 | } |
||
696 | if ($cell instanceof TableCell && $cell->getRowspan() > 1) { |
||
697 | $nbLines = $cell->getRowspan() - 1; |
||
698 | $lines = [$cell]; |
||
699 | if (str_contains($cell, "\n")) { |
||
700 | $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; |
||
701 | $lines = explode($eol, str_replace($eol, '<fg=default;bg=default>'.$eol.'</>', $cell)); |
||
702 | $nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines; |
||
703 | |||
704 | $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); |
||
705 | unset($lines[0]); |
||
706 | } |
||
707 | |||
708 | // create a two dimensional array (rowspan x colspan) |
||
709 | $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); |
||
710 | foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { |
||
711 | $value = $lines[$unmergedRowKey - $line] ?? ''; |
||
712 | $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); |
||
713 | if ($nbLines === $unmergedRowKey - $line) { |
||
714 | break; |
||
715 | } |
||
716 | } |
||
717 | } |
||
718 | } |
||
719 | |||
720 | foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { |
||
721 | // we need to know if $unmergedRow will be merged or inserted into $rows |
||
722 | if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRow) <= $this->numberOfColumns)) { |
||
723 | foreach ($unmergedRow as $cellKey => $cell) { |
||
724 | // insert cell into row at cellKey position |
||
725 | array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); |
||
726 | } |
||
727 | } else { |
||
728 | $row = $this->copyRow($rows, $unmergedRowKey - 1); |
||
729 | foreach ($unmergedRow as $column => $cell) { |
||
730 | if ($cell) { |
||
731 | $row[$column] = $cell; |
||
732 | } |
||
733 | } |
||
734 | array_splice($rows, $unmergedRowKey, 0, [$row]); |
||
735 | } |
||
736 | } |
||
737 | |||
738 | return $rows; |
||
739 | } |
||
740 | |||
741 | /** |
||
742 | * fill cells for a row that contains colspan > 1. |
||
743 | */ |
||
744 | private function fillCells(iterable $row): iterable |
||
745 | { |
||
746 | $newRow = []; |
||
747 | |||
748 | foreach ($row as $column => $cell) { |
||
749 | $newRow[] = $cell; |
||
750 | if ($cell instanceof TableCell && $cell->getColspan() > 1) { |
||
751 | foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { |
||
752 | // insert empty value at column position |
||
753 | $newRow[] = ''; |
||
754 | } |
||
755 | } |
||
756 | } |
||
757 | |||
758 | return $newRow ?: $row; |
||
759 | } |
||
760 | |||
761 | private function copyRow(array $rows, int $line): array |
||
762 | { |
||
763 | $row = $rows[$line]; |
||
764 | foreach ($row as $cellKey => $cellValue) { |
||
765 | $row[$cellKey] = ''; |
||
766 | if ($cellValue instanceof TableCell) { |
||
767 | $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]); |
||
768 | } |
||
769 | } |
||
770 | |||
771 | return $row; |
||
772 | } |
||
773 | |||
774 | /** |
||
775 | * Gets number of columns by row. |
||
776 | */ |
||
777 | private function getNumberOfColumns(array $row): int |
||
778 | { |
||
779 | $columns = \count($row); |
||
780 | foreach ($row as $column) { |
||
781 | $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; |
||
782 | } |
||
783 | |||
784 | return $columns; |
||
785 | } |
||
786 | |||
787 | /** |
||
788 | * Gets list of columns for the given row. |
||
789 | */ |
||
790 | private function getRowColumns(array $row): array |
||
791 | { |
||
792 | $columns = range(0, $this->numberOfColumns - 1); |
||
793 | foreach ($row as $cellKey => $cell) { |
||
794 | if ($cell instanceof TableCell && $cell->getColspan() > 1) { |
||
795 | // exclude grouped columns. |
||
796 | $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); |
||
797 | } |
||
798 | } |
||
799 | |||
800 | return $columns; |
||
801 | } |
||
802 | |||
803 | /** |
||
804 | * Calculates columns widths. |
||
805 | */ |
||
806 | private function calculateColumnsWidth(iterable $groups): void |
||
807 | { |
||
808 | for ($column = 0; $column < $this->numberOfColumns; ++$column) { |
||
809 | $lengths = []; |
||
810 | foreach ($groups as $group) { |
||
811 | foreach ($group as $row) { |
||
812 | if ($row instanceof TableSeparator) { |
||
813 | continue; |
||
814 | } |
||
815 | |||
816 | foreach ($row as $i => $cell) { |
||
817 | if ($cell instanceof TableCell) { |
||
818 | $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); |
||
819 | $textLength = Helper::width($textContent); |
||
820 | if ($textLength > 0) { |
||
821 | $contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan())); |
||
822 | foreach ($contentColumns as $position => $content) { |
||
823 | $row[$i + $position] = $content; |
||
824 | } |
||
825 | } |
||
826 | } |
||
827 | } |
||
828 | |||
829 | $lengths[] = $this->getCellWidth($row, $column); |
||
830 | } |
||
831 | } |
||
832 | |||
833 | $this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2; |
||
834 | } |
||
835 | } |
||
836 | |||
837 | private function getColumnSeparatorWidth(): int |
||
840 | } |
||
841 | |||
842 | private function getCellWidth(array $row, int $column): int |
||
855 | } |
||
856 | |||
857 | /** |
||
858 | * Called after rendering to cleanup cache data. |
||
859 | */ |
||
860 | private function cleanup(): void |
||
861 | { |
||
862 | $this->effectiveColumnWidths = []; |
||
864 | } |
||
865 | |||
866 | /** |
||
867 | * @return array<string, TableStyle> |
||
868 | */ |
||
869 | private static function initStyles(): array |
||
870 | { |
||
871 | $borderless = new TableStyle(); |
||
872 | $borderless |
||
873 | ->setHorizontalBorderChars('=') |
||
874 | ->setVerticalBorderChars(' ') |
||
875 | ->setDefaultCrossingChar(' ') |
||
913 | ]; |
||
914 | } |
||
915 | |||
916 | private function resolveStyle(TableStyle|string $name): TableStyle |
||
923 | } |
||
924 | } |
||
925 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.