8000 [Console][Table] Add support for colspan/rowspan + multiple header lines · symfony/symfony@ed18767 · GitHub
[go: up one dir, main page]

Skip to content

Commit ed18767

Browse files
aitboudadfabpot
authored andcommitted
[Console][Table] Add support for colspan/rowspan + multiple header lines
1 parent cb70899 commit ed18767

File tree

4 files changed

+524
-44
lines changed

4 files changed

+524
-44
lines changed

src/Symfony/Component/Console/Helper/Table.php

Lines changed: 229 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*
1919
* @author Fabien Potencier <fabien@symfony.com>
2020
* @author Саша Стаменковић <umpirsky@gmail.com>
21+
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
2122
*/
2223
class Table
2324
{
@@ -139,7 +140,12 @@ public function getStyle()
139140

140141
public function setHeaders(array $headers)
141142
{
142-
$this->headers = array_values($headers);
143+
$headers = array_values($headers);
144+
if (!empty($headers) && !is_array($headers[0])) {
145+
$headers = array($headers);
146+
}
147+
148+
$this->headers = $headers;
143149

144150
return $this;
145151
}
@@ -174,30 +180,6 @@ public function addRow($row)
174180

175181
$this->rows[] = array_values($row);
176182

177-
end($this->rows);
178-
$rowKey = key($this->rows);
179-
reset($this->rows);
180-
181-
foreach ($row as $key => $cellValue) {
182-
if (!strstr($cellValue, "\n")) {
183-
continue;
184-
}
185-
186-
$lines = explode("\n", $cellValue);
187-
$this->rows[$rowKey][$key] = $lines[0];
188-
unset($lines[0]);
189-
190-
foreach ($lines as $lineKey => $line) {
191-
$nextRowKey = $rowKey + $lineKey + 1;
192-
193-
if (isset($this->rows[$nextRowKey])) {
194-
$this->rows[$nextRowKey][$key] = $line;
195-
} else {
196-
$this->rows[$nextRowKey] = array($key => $line);
197-
}
198-
}
199-
}
200-
201183
return $this;
202184
}
203185

@@ -222,10 +204,16 @@ public function setRow($column, array $row)
222204
*/
223205
public function render()
224206
{
207+
$this->calculateNumberOfColumns();
208+
$this->rows = $this->buildTableRows($this->rows);
209+
$this->headers = $this->buildTableRows($this->headers);
210+
225211
$this->renderRowSeparator();
226-
$this->renderRow($this->headers, $this->style->getCellHeaderFormat());
227212
if (!empty($this->headers)) {
228-
$this->renderRowSeparator();
213+
foreach ($this->headers as $header) {
214+
$this->renderRow($header, $this->style->getCellHeaderFormat());
215+
$this->renderRowSeparator();
216+
}
229217
}
230218
foreach ($this->rows as $row) {
231219
if ($row instanceof TableSeparator) {
@@ -248,7 +236,7 @@ public function render()
248236
*/
249237
private function renderRowSeparator()
250238
{
251-
if (0 === $count = $this->getNumberOfColumns()) {
239+
if (0 === $count = $this->numberOfColumns) {
252240
return;
253241
}
254242

@@ -287,7 +275,7 @@ private function renderRow(array $row, $cellFormat)
287275
}
288276

289277
$this->renderColumnSeparator();
290-
for ($column = 0, $count = $this->getNumberOfColumns(); $column < $count; $column++) {
278+
foreach ($this->getRowColumns($row) as $column) {
291279
$this->renderCell($row, $column, $cellFormat);
292280
$this->renderColumnSeparator();
293281
}
@@ -305,36 +293,213 @@ private function renderCell(array $row, $column, $cellFormat)
305293
{
306294
$cell = isset($row[$column]) ? $row[$column] : '';
307295
$width = $this->getColumnWidth($column);
296+
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
297+
// add the width of the following columns(numbers of colspan).
298+
foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) {
299+
$width += $this->getColumnSeparatorWidth() + $this->getColumnWidth($nextColumn);
300+
}
301+
}
308302

309303
// str_pad won't work properly with multi-byte strings, we need to fix the padding
310304
if (function_exists('mb_strwidth') && false !== $encoding = mb_detect_encoding($cell)) {
311305
$width += strlen($cell) - mb_strwidth($cell, $encoding);
312306
}
313307

314-
$width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
308+
if ($cell instanceof TableSeparator) {
309+
$this->output->write(sprintf($this->style->getBorderFormat(), str_repeat($this->style->getHorizontalBorderChar(), $width)));
310+
} else {
311+
$width += Helper::strlen($cell) - Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
312+
$content = sprintf($this->style->getCellRowContentFormat(), $cell);
313+
$this->output->write(sprintf($cellFormat, str_pad($content, $width, $this->style->getPaddingChar(), $this->style->getPadType())));
314+
}
315+
}
315316

316-
$content = sprintf($this->style->getCellRowContentFormat(), $cell);
317+
/**
318+
* Calculate number of columns for this table.
319+
*/
320+
private function calculateNumberOfColumns()
321+
{
322+
if (null !== $this->numberOfColumns) {
323+
return;
324+
}
317325

318-
$this->output->write(sprintf($cellFormat, str_pad($content, $width, $this->style->getPaddingChar(), $this->style->getPadType())));
326+
$columns = array(0);
327+
foreach (array_merge($this->headers, $this->rows) as $row) {
328+
if ($row instanceof TableSeparator) {
329+
continue;
330+
}
331+
332+
$columns[] = $this->getNumberOfColumns($row);
333+
}
334+
335+
return $this->numberOfColumns = max($columns);
336+
}
337+
338+
private function buildTableRows($rows)
339+
{
340+
$unmergedRows = array();
341+
for ($rowKey = 0; $rowKey < count($rows); $rowKey++) {
342+
$rows = $this->fillNextRows($rows, $rowKey);
343+
344+
// Remove any new line breaks and replace it with a new line
345+
foreach ($rows[$rowKey] as $column => $cell) {
346+
$rows[$rowKey] = $this->fillCells($rows[$rowKey], $column);
347+
if (!strstr($cell, "\n")) {
348+
continue;
349+
}
350+
$lines = explode("\n", $cell);
351+
foreach ($lines as $lineKey => $line) {
352+
if ($cell instanceof TableCell) {
353+
$line = new TableCell($line, array('colspan' => $cell->getColspan()));
354+
}
355+
if (0 === $lineKey) {
356+
$rows[$rowKey][$column] = $line;
357+
} else {
358+
$unmergedRows[$rowKey][$lineKey][$column] = $line;
359+
}
360+
}
361+
}
362+
}
363+
364+
$tableRows = array();
365+
foreach ($rows as $rowKey => $row) {
366+
$tableRows[] = $row;
367+
if (isset($unmergedRows[$rowKey])) {
368+
$tableRows = array_merge($tableRows, $unmergedRows[$rowKey]);
369+
}
370+
}
371+
372+
return $tableRows;
319373
}
320374

321375
/**
322-
* Gets number of columns for this table.
376+
* fill rows that contains rowspan > 1.
377+
*
378+
* @param array $rows
379+
* @param array $line
380+
*
381+
* @return array
382+
*/
383+
private function fillNextRows($rows, $line)
384+
{
385+
$unmergedRows = array();
386+
foreach ($rows[$line] as $column => $cell) {
387+
if ($cell instanceof TableCell && $cell->getRowspan() > 1) {
388+
$nbLines = $cell->getRowspan()-1;
389+
$lines = array($cell);
390+
if (strstr($cell, "\n")) {
391+
$lines = explode("\n", $cell);
392+
$nbLines = count($lines) > $nbLines ? substr_count($cell, "\n") : $nbLines;
393+
394+
$rows[$line][$column] = new TableCell($lines[0], array('colspan' => $cell->getColspan()));
395+
unset($lines[0]);
396+
}
397+
398+
// create a two dimensional array (rowspan x colspan)
399+
$unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, ''), $unmergedRows);
400+
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
401+
$value = isset($lines[$unmergedRowKey - $line]) ? $lines[$unmergedRowKey - $line] : '';
402+
$unmergedRows[$unmergedRowKey][$column] = new TableCell($value, array('colspan' => $cell->getColspan()));
403+
}
404+
}
405+
}
406+
407+
foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) {
408+
// we need to know if $unmergedRow will be merged or inserted into $rows
409+
if (isset($rows[$unmergedRowKey]) && is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) {
410+
foreach ($unmergedRow as $cellKey => $cell) {
411+
// insert cell into row at cellKey position
412+
array_splice($rows[$unmergedRowKey], $cellKey, 0, array($cell));
413+
}
414+
} else {
415+
$row = $this->copyRow($rows, $unmergedRowKey-1);
416+
foreach ($unmergedRow as $column => $cell) {
417+
if (!empty($cell)) {
418+
$row[$column] = $unmergedRow[$column];
419+
}
420+
}
421+
array_splice($rows, $unmergedRowKey, 0, array($row));
422+
}
423+
}
424+
425+
return $rows;
426+
}
427+
428+
/**
429+
* fill cells for a row that contains colspan > 1.
430+
*
431+
* @param array $row
432+
* @param array $column
433+
*
434+
* @return array
435+
*/
436+
private function fillCells($row, $column)
437+
{
438+
$cell = $row[$column];
439+
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
440+
foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) {
441+
// insert empty value into rows at column position
442+
array_splice($row, $position, 0, '');
443+
}
444+
}
445+
446+
return $row;
447+
}
448+
449+
/**
450+
* @param array $rows
451+
* @param int $line
452+
*
453+
* @return array
454+
*/
455+
private function copyRow($rows, $line)
456+
{
457+
$row = $rows[$line];
458+
foreach ($row as $cellKey => $cellValue) {
459+
$row[$cellKey] = '';
460+
if ($cellValue instanceof TableCell) {
461+
$row[$cellKey] = new TableCell('', array('colspan' => $cellValue->getColspan()));
462+
}
463+
}
464+
465+
return $row;
466+
}
467+
468+
/**
469+
* Gets number of columns by row.
470+
*
471+
* @param array $row
323472
*
324473
* @return int
325474
*/
326-
private function getNumberOfColumns()
475+
private function getNumberOfColumns(array $row)
327476
{
328-
if (null !== $this->numberOfColumns) {
329-
return $this->numberOfColumns;
477+
$columns = count($row);
478+
foreach ($row as $column) {
479+
$columns += $column instanceof TableCell ? ($column->getColspan()-1) : 0;
330480
}
331481

332-
$columns = array(count($this->headers));
333-
foreach ($this->rows as $row) {
334-
$columns[] = count($row);
482+
return $columns;
483+
}
484+
485+
/**
486+
* Gets list of columns for the given row.
487+
*
488+
* @param array $row
489+
*
490+
* @return array()
491+
*/
492+
private function getRowColumns($row)
493+
{
494+
$columns = range(0, $this->numberOfColumns-1);
495+
foreach ($row as $cellKey => $cell) {
496+
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
497+
// exclude grouped columns.
498+
$columns = array_diff($columns, range($cellKey+1, $cellKey + $cell->getColspan()-1));
499+
}
335500
}
336501

337-
return $this->numberOfColumns = max($columns);
502+
return $columns;
338503
}
339504

340505
/**
@@ -350,8 +515,7 @@ private function getColumnWidth($column)
350515
return $this->columnWidths[$column];
351516
}
352517

353-
$lengths = array($this->getCellWidth($this->headers, $column));
354-
foreach ($this->rows as $row) {
518+
foreach (array_merge($this->headers, $this->rows) as $row) {
355519
if ($row instanceof TableSeparator) {
356520
continue;
357521
}
@@ -362,6 +526,18 @@ private function getColumnWidth($column)
362526
return $this->columnWidths[$column] = max($lengths) + strlen($this->style->getCellRowContentFormat()) - 2;
363527
}
364528

529+
/**
530+
* Gets column width.
531+
*
532+
* @param int $column
533+
*
534+
* @return int
535+
*/
536+
private function getColumnSeparatorWidth()
537+
{
538+
return strlen(sprintf($this->style->getBorderFormat(), $this->style->getVerticalBorderChar()));
539+
}
540+
365541
/**
366542
* Gets cell width.
367543
*
@@ -372,7 +548,17 @@ private function getColumnWidth($column)
372548
*/
373549
private function getCellWidth(array $row, $column)
374550
{
375-
return isset($row[$column]) ? Helper::strlenWithoutDecoration($this->output->getFormatter(), $row[$column]) : 0;
551+
if (isset($row[$column])) {
552+
$cell = $row[$column];
553+
if ($cell instanceof TableCell && $cell->getColspan() > 1) {
554+
// we assume that cell value will be across more than one column.
555+
$cell = substr($cell, 0, strlen($cell)/$cell->getColspan());
556+
}
557+
558+
return Helper::strlenWithoutDecoration($this->output->getFormatter(), $cell);
559+
}
560+
561+
return 0;
376562
}
377563

378564
/**

0 commit comments

Comments
 (0)
0