Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 28 additions & 4 deletions lib/cli/Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use cli\Shell;
use cli\Streams;
use cli\table\Ascii;
use cli\table\Column;
use cli\table\Renderer;
use cli\table\Tabular;

Expand All @@ -27,6 +28,7 @@ class Table {
protected $_footers = array();
protected $_width = array();
protected $_rows = array();
protected $_alignments = array();

/**
* Initializes the `Table` class.
Expand All @@ -40,11 +42,12 @@ class Table {
* table are used as the header values.
* 3. Pass nothing and use `setHeaders()` and `addRow()` or `setRows()`.
*
* @param array $headers Headers used in this table. Optional.
* @param array $rows The rows of data for this table. Optional.
* @param array $footers Footers used in this table. Optional.
* @param array $headers Headers used in this table. Optional.
* @param array $rows The rows of data for this table. Optional.
* @param array $footers Footers used in this table. Optional.
* @param array $alignments Column alignments. Optional.
*/
public function __construct(array $headers = array(), array $rows = array(), array $footers = array()) {
public function __construct(array $headers = array(), array $rows = array(), array $footers = array(), array $alignments = array()) {
if (!empty($headers)) {
// If all the rows is given in $headers we use the keys from the
// first row for the header values
Expand All @@ -62,6 +65,8 @@ public function __construct(array $headers = array(), array $rows = array(), arr
$this->setRows($rows);
}

$this->setAlignments($alignments);

if (!empty($footers)) {
$this->setFooters($footers);
}
Expand All @@ -79,6 +84,7 @@ public function resetTable()
$this->_width = array();
$this->_rows = array();
$this->_footers = array();
$this->_alignments = array();
return $this;
}

Expand Down Expand Up @@ -137,6 +143,7 @@ public function display() {
*/
public function getDisplayLines() {
$this->_renderer->setWidths($this->_width, $fallback = true);
$this->_renderer->setAlignments($this->_alignments);
$border = $this->_renderer->border();

$out = array();
Expand Down Expand Up @@ -201,6 +208,23 @@ public function setFooters(array $footers) {
$this->_footers = $this->checkRow($footers);
}

/**
* Set the alignments of the table.
*
* @param array $alignments An array of strings containing column alignments.
*/
public function setAlignments(array $alignments) {
$validAlignments = array(Column::ALIGN_LEFT, Column::ALIGN_RIGHT, Column::ALIGN_CENTER);
foreach ($alignments as $column => $alignment) {
if (!in_array($alignment, $validAlignments, true)) {
throw new \InvalidArgumentException("Invalid alignment value '$alignment' for column '$column'.");
}
if (!in_array($column, $this->_headers, true)) {
throw new \InvalidArgumentException("Column '$column' does not exist in table headers.");
}
}
$this->_alignments = $alignments;
}
Comment on lines +216 to +227
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation on line 222 checks if headers exist before alignments are set, but setAlignments() can be called in the constructor at line 68 before setHeaders() is called at line 64 when the constructor is invoked with an empty headers array and a non-empty alignments array. This will cause the validation to always fail in that case because $this->_headers will be empty. Consider either validating alignments lazily when they're used, or reordering the constructor calls, or skipping validation when headers are empty.

Copilot uses AI. Check for mistakes.

/**
* Add a row to the table.
Expand Down
9 changes: 8 additions & 1 deletion lib/cli/table/Ascii.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Ascii extends Renderer {
protected $_border = null;
protected $_constraintWidth = null;
protected $_pre_colorized = false;
protected $_headers = null;

/**
* Set the widths of each column in the table.
Expand Down Expand Up @@ -131,6 +132,10 @@ public function border() {
*/
public function row( array $row ) {

if ($this->_headers === null) {
$this->_headers = array_values($row);
}
Comment on lines +135 to +137
Copy link

Copilot AI Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header tracking logic assumes the first row rendered is always the headers, but this may not be reliable if the renderer is reused or if rows are rendered in an unexpected order. Consider passing headers explicitly to the renderer via a dedicated method (e.g., setHeaders()) rather than inferring them from the first row call to row().

Copilot uses AI. Check for mistakes.

$extra_row_count = 0;

if ( count( $row ) > 0 ) {
Expand Down Expand Up @@ -198,8 +203,10 @@ public function row( array $row ) {
}

private function padColumn($content, $column) {
$column_name = isset( $this->_headers[$column] ) ? $this->_headers[$column] : '';
$alignment = ($column_name !== '' && array_key_exists($column_name, $this->_alignments)) ? $this->_alignments[$column_name] : Column::ALIGN_LEFT;
$content = str_replace( "\t", ' ', (string) $content );
return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ) ) . $this->_characters['padding'];
return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ), false, $alignment) . $this->_characters['padding'];
}

/**
Expand Down
10 changes: 10 additions & 0 deletions lib/cli/table/Column.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace cli\table;

interface Column {

const ALIGN_RIGHT = STR_PAD_LEFT;
const ALIGN_LEFT = STR_PAD_RIGHT;
const ALIGN_CENTER = STR_PAD_BOTH;
}
13 changes: 12 additions & 1 deletion lib/cli/table/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,20 @@
*/
abstract class Renderer {
protected $_widths = array();
protected $_alignments = array();

public function __construct(array $widths = array()) {
public function __construct(array $widths = array(), array $alignments = array()) {
$this->setWidths($widths);
$this->setAlignments($alignments);
}

/**
* Set the alignments of each column in the table.
*
* @param array $alignments The alignments of the columns.
*/
public function setAlignments(array $alignments) {
$this->_alignments = $alignments;
}

/**
Expand Down
66 changes: 66 additions & 0 deletions tests/Test_Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,70 @@
];
$this->assertSame( $expected, $out, 'Null values should be safely converted to empty strings in table output.' );
}

public function test_default_alignment() {
$table = new cli\Table();
$table->setHeaders( array( 'Header1', 'Header2' ) );
$table->addRow( array( 'Row1Col1', 'Row1Col2' ) );

$out = $table->getDisplayLines();

// By default, columns should be left-aligned.
$this->assertStringContainsString( '| Header1 | Header2 |', $out[1] );

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.0

Failed asserting that 'Row1Col1 Row1Col2' contains "| Header1 | Header2 |".

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.2

Failed asserting that 'Row1Col1 Row1Col2' [ASCII](length: 17) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.1

Failed asserting that 'Row1Col1 Row1Col2' contains "| Header1 | Header2 |".

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.4

Failed asserting that 'Row1Col1 Row1Col2' contains "| Header1 | Header2 |".

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.2

Failed asserting that 'Row1Col1 Row1Col2' contains "| Header1 | Header2 |".

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.3 (with coverage)

Failed asserting that 'Row1Col1 Row1Col2' [ASCII](length: 17) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP nightly

Failed asserting that 'Row1Col1 Row1Col2' [ASCII](length: 17) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.3

Failed asserting that 'Row1Col1 Row1Col2' contains "| Header1 | Header2 |".

Check failure on line 301 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.4

Failed asserting that 'Row1Col1 Row1Col2' [ASCII](length: 17) contains "| Header1 | Header2 |" [ASCII](length: 23).
$this->assertStringContainsString( '| Row1Col1 | Row1Col2 |', $out[3] );
}

public function test_right_alignment() {
$table = new cli\Table();
$table->setHeaders( array( 'Header1', 'Header2' ) );
$table->setAlignments( array( 'Header1' => \cli\table\Column::ALIGN_RIGHT, 'Header2' => \cli\table\Column::ALIGN_RIGHT ) );
$table->addRow( array( 'R1C1', 'R1C2' ) );

$out = $table->getDisplayLines();

$this->assertStringContainsString( '| Header1 | Header2 |', $out[1] );

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.0

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.2

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.1

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.4

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.2

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.3 (with coverage)

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP nightly

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.3

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 313 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.4

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).
$this->assertStringContainsString( '| R1C1 | R1C2 |', $out[3] );
}

public function test_center_alignment() {
$table = new cli\Table();
$table->setHeaders( array( 'Header1', 'Header2' ) );
$table->setAlignments( array( 'Header1' => \cli\table\Column::ALIGN_CENTER, 'Header2' => \cli\table\Column::ALIGN_CENTER ) );
$table->addRow( array( 'R1C1', 'R1C2' ) );

$out = $table->getDisplayLines();

$this->assertStringContainsString( '| Header1 | Header2 |', $out[1] );

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.0

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.2

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.1

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.4

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.2

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.3 (with coverage)

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP nightly

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.3

Failed asserting that 'R1C1 R1C2' contains "| Header1 | Header2 |".

Check failure on line 325 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.4

Failed asserting that 'R1C1 R1C2' [ASCII](length: 9) contains "| Header1 | Header2 |" [ASCII](length: 23).
$this->assertStringContainsString( '| R1C1 | R1C2 |', $out[3] );
}

public function test_mixed_alignments() {
$table = new cli\Table();
$table->setHeaders( array( 'Left', 'Right', 'Center' ) );
$table->setAlignments( array(
'Left' => \cli\table\Column::ALIGN_LEFT,
'Right' => \cli\table\Column::ALIGN_RIGHT,
'Center' => \cli\table\Column::ALIGN_CENTER,
) );
$table->addRow( array( 'l', 'r', 'c' ) );

$out = $table->getDisplayLines();

$this->assertStringContainsString( '| Left | Right | Center |', $out[1] );

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.0

Failed asserting that 'l r c' contains "| Left | Right | Center |".

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.2

Failed asserting that 'l r c' [ASCII](length: 5) contains "| Left | Right | Center |" [ASCII](length: 27).

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.1

Failed asserting that 'l r c' contains "| Left | Right | Center |".

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.4

Failed asserting that 'l r c' contains "| Left | Right | Center |".

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.2

Failed asserting that 'l r c' contains "| Left | Right | Center |".

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.3 (with coverage)

Failed asserting that 'l r c' [ASCII](length: 5) contains "| Left | Right | Center |" [ASCII](length: 27).

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP nightly

Failed asserting that 'l r c' [ASCII](length: 5) contains "| Left | Right | Center |" [ASCII](length: 27).

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 7.3

Failed asserting that 'l r c' contains "| Left | Right | Center |".

Check failure on line 341 in tests/Test_Table.php

View workflow job for this annotation

GitHub Actions / test / Unit test / PHP 8.4

Failed asserting that 'l r c' [ASCII](length: 5) contains "| Left | Right | Center |" [ASCII](length: 27).
$this->assertStringContainsString( '| l | r | c |', $out[3] );
}

public function test_invalid_alignment_value() {
$this->expectException( \InvalidArgumentException::class );
$table = new cli\Table();
$table->setHeaders( array( 'Header1' ) );
$table->setAlignments( array( 'Header1' => 'invalid-alignment' ) );
}

public function test_invalid_alignment_column() {
$this->expectException( \InvalidArgumentException::class );
$table = new cli\Table();
$table->setHeaders( array( 'Header1' ) );
$table->setAlignments( array( 'NonExistent' => \cli\table\Column::ALIGN_LEFT ) );
}
}
Loading