Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
103 changes: 103 additions & 0 deletions examples/table-alignment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env php
<?php
/**
* Table Alignment Examples
*
* This example demonstrates the table column alignment feature.
* You can align columns to the left, right, or center.
*/

if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require_once __DIR__ . '/../vendor/autoload.php';
} elseif (file_exists(__DIR__ . '/../../../autoload.php')) {
require_once __DIR__ . '/../../../autoload.php';
} else {
throw new Exception('Unable to locate autoloader; please run "composer install"');
}

cli\line();
cli\line('%G===%n %CTable Column Alignment Examples%n %G===%n');
cli\line();

// Example 1: Default (Left) Alignment
cli\line('%Y## Example 1: Default Left Alignment%n');
cli\line();
$table = new cli\Table();
$table->setHeaders(['Product', 'Price', 'Stock']);
$table->addRow(['Widget', '$19.99', '150']);
$table->addRow(['Gadget', '$29.99', '75']);
$table->addRow(['Tool', '$9.99', '200']);
$table->display();
cli\line();

// Example 2: Right Alignment for Numeric Columns
cli\line('%Y## Example 2: Right Alignment for Numeric Columns%n');
cli\line('Notice how the numeric values are much easier to compare when right-aligned.');
cli\line();
$table = new cli\Table();
$table->setHeaders(['Product', 'Price', 'Stock']);
$table->setAlignments([
'Product' => cli\table\Column::ALIGN_LEFT,
'Price' => cli\table\Column::ALIGN_RIGHT,
'Stock' => cli\table\Column::ALIGN_RIGHT,
]);
$table->addRow(['Widget', '$19.99', '150']);
$table->addRow(['Gadget', '$29.99', '75']);
$table->addRow(['Tool', '$9.99', '200']);
$table->display();
cli\line();

// Example 3: Center Alignment
cli\line('%Y## Example 3: Center Alignment%n');
cli\line();
$table = new cli\Table();
$table->setHeaders(['Left', 'Center', 'Right']);
$table->setAlignments([
'Left' => cli\table\Column::ALIGN_LEFT,
'Center' => cli\table\Column::ALIGN_CENTER,
'Right' => cli\table\Column::ALIGN_RIGHT,
]);
$table->addRow(['Text', 'Centered', 'More']);
$table->addRow(['Data', 'Values', 'Here']);
$table->addRow(['A', 'B', 'C']);
$table->display();
cli\line();

// Example 4: Real-world Database Table Sizes
cli\line('%Y## Example 4: Database Table Sizes (Real-world Use Case)%n');
cli\line('This example shows how the alignment feature makes database');
cli\line('statistics much more readable and easier to compare.');
cli\line();
$table = new cli\Table();
$table->setHeaders(['Table Name', 'Rows', 'Data Size', 'Index Size']);
$table->setAlignments([
'Table Name' => cli\table\Column::ALIGN_LEFT,
'Rows' => cli\table\Column::ALIGN_RIGHT,
'Data Size' => cli\table\Column::ALIGN_RIGHT,
'Index Size' => cli\table\Column::ALIGN_RIGHT,
]);
$table->addRow(['wp_posts', '1,234', '5.2 MB', '1.1 MB']);
$table->addRow(['wp_postmeta', '45,678', '23.4 MB', '8.7 MB']);
$table->addRow(['wp_comments', '9,012', '2.3 MB', '0.8 MB']);
$table->addRow(['wp_options', '456', '1.5 MB', '0.2 MB']);
$table->addRow(['wp_users', '89', '0.1 MB', '0.05 MB']);
$table->display();
cli\line();

// Example 5: Alignment Constants
cli\line('%Y## Alignment Constants%n');
cli\line();
cli\line('You can use the following constants from %Ccli\table\Column%n:');
cli\line(' %G*%n %CALIGN_LEFT%n - Left align (default)');
cli\line(' %G*%n %CALIGN_RIGHT%n - Right align (good for numbers)');
cli\line(' %G*%n %CALIGN_CENTER%n - Center align');
cli\line();
cli\line('Example usage:');
cli\line(' %c$table->setAlignments([%n');
cli\line(' %c\'Column1\' => cli\table\Column::ALIGN_LEFT,%n');
cli\line(' %c\'Column2\' => cli\table\Column::ALIGN_RIGHT,%n');
cli\line(' %c]);%n');
cli\line();

cli\line('%GDone!%n');
cli\line();
38 changes: 34 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 @@ -66,6 +69,10 @@ public function __construct(array $headers = array(), array $rows = array(), arr
$this->setFooters($footers);
}

if (!empty($alignments)) {
$this->setAlignments($alignments);
}

if (Shell::isPiped()) {
$this->setRenderer(new Tabular());
} else {
Expand All @@ -79,6 +86,7 @@ public function resetTable()
$this->_width = array();
$this->_rows = array();
$this->_footers = array();
$this->_alignments = array();
return $this;
}

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

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

/**
* Set the alignments of the table.
*
* @param array $alignments An array of alignment constants keyed by column name.
*/
public function setAlignments(array $alignments) {
$valid_alignments = array_flip( array( Column::ALIGN_LEFT, Column::ALIGN_RIGHT, Column::ALIGN_CENTER ) );
Comment thread
swissspidy marked this conversation as resolved.
Outdated
Comment thread
swissspidy marked this conversation as resolved.
Outdated
$headers_map = ! empty( $this->_headers ) ? array_flip( $this->_headers ) : null;
foreach ( $alignments as $column => $alignment ) {
if ( ! isset( $valid_alignments[ $alignment ] ) ) {
throw new \InvalidArgumentException( "Invalid alignment value '$alignment' for column '$column'." );
}
// Only validate column names if headers are already set
if ( $headers_map !== null && ! isset( $headers_map[ $column ] ) ) {
throw new \InvalidArgumentException( "Column '$column' does not exist in table headers." );
}
}
$this->_alignments = $alignments;
}


Comment thread
swissspidy marked this conversation as resolved.
Outdated
/**
* Add a row to the table.
Expand Down
17 changes: 16 additions & 1 deletion lib/cli/table/Ascii.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,9 +197,24 @@ public function row( array $row ) {
return $ret;
}

/**
* Get the alignment for a column.
*
* @param int $column Column index.
* @return int Alignment constant (STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH).
*/
private function getColumnAlignment( $column ) {
$column_name = isset( $this->_headers[ $column ] ) ? $this->_headers[ $column ] : '';
if ( $column_name !== '' && array_key_exists( $column_name, $this->_alignments ) ) {
return $this->_alignments[ $column_name ];
}
return Column::ALIGN_LEFT;
}

private function padColumn($content, $column) {
$alignment = $this->getColumnAlignment( $column );
$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
22 changes: 22 additions & 0 deletions lib/cli/table/Column.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php
/**
* PHP Command Line Tools
*
* This source file is subject to the MIT license that is bundled
* with this package in the file LICENSE.
*
* @author James Logsdon <dwarf@girsbrain.org>
* @copyright 2010 James Logsdom (http://girsbrain.org)
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/

namespace cli\table;

/**
* Column alignment constants for table rendering.
*/
interface Column {
const ALIGN_LEFT = STR_PAD_RIGHT;
const ALIGN_RIGHT = STR_PAD_LEFT;
const ALIGN_CENTER = STR_PAD_BOTH;
}
23 changes: 22 additions & 1 deletion lib/cli/table/Renderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,30 @@
*/
abstract class Renderer {
protected $_widths = array();
protected $_alignments = array();
protected $_headers = array();

public function __construct(array $widths = array()) {
public function __construct(array $widths = array(), array $alignments = array()) {
$this->setWidths($widths);
$this->setAlignments($alignments);
}
Comment thread
swissspidy marked this conversation as resolved.

/**
* 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;
}

/**
* Set the headers of the table.
*
* @param array $headers The headers of the table.
*/
public function setHeaders(array $headers) {
$this->_headers = $headers;
}

/**
Expand Down
91 changes: 91 additions & 0 deletions tests/Test_Table.php
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,95 @@ public function test_null_values_are_handled() {
];
$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->setRenderer( new cli\Table\Ascii() );
$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] );
$this->assertStringContainsString( '| Row1Col1 | Row1Col2 |', $out[3] );
}

public function test_right_alignment() {
$table = new cli\Table();
$table->setRenderer( new cli\Table\Ascii() );
$table->setHeaders( array( 'Name', 'Size' ) );
$table->setAlignments( array( 'Name' => \cli\table\Column::ALIGN_RIGHT, 'Size' => \cli\table\Column::ALIGN_RIGHT ) );
$table->addRow( array( 'file.txt', '1024 B' ) );

$out = $table->getDisplayLines();

// Headers should be right-aligned in their columns
$this->assertStringContainsString( '| Name | Size |', $out[1] );
// Data should be right-aligned
$this->assertStringContainsString( '| file.txt | 1024 B |', $out[3] );
}

public function test_center_alignment() {
$table = new cli\Table();
$table->setRenderer( new cli\Table\Ascii() );
$table->setHeaders( array( 'A', 'B' ) );
$table->setAlignments( array( 'A' => \cli\table\Column::ALIGN_CENTER, 'B' => \cli\table\Column::ALIGN_CENTER ) );
$table->addRow( array( 'test', 'data' ) );

$out = $table->getDisplayLines();

// Headers should be center-aligned
$this->assertStringContainsString( '| A | B |', $out[1] );
// Data should be center-aligned
$this->assertStringContainsString( '| test | data |', $out[3] );
}

public function test_mixed_alignments() {
$table = new cli\Table();
$table->setRenderer( new cli\Table\Ascii() );
$table->setHeaders( array( 'Name', 'Count', 'Status' ) );
$table->setAlignments( array(
'Name' => \cli\table\Column::ALIGN_LEFT,
'Count' => \cli\table\Column::ALIGN_RIGHT,
'Status' => \cli\table\Column::ALIGN_CENTER,
) );
$table->addRow( array( 'Item', '42', 'OK' ) );

$out = $table->getDisplayLines();

// Headers line should show all three with proper alignment
$this->assertStringContainsString( '| Name | Count | Status |', $out[1] );
// Data line: Name left, Count right, Status center
$this->assertStringContainsString( '| Item | 42 | OK |', $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 ) );
}

public function test_alignment_before_headers() {
// Test that alignments can be set before headers without throwing an error
$table = new cli\Table();
$table->setRenderer( new cli\Table\Ascii() );
$table->setAlignments( array( 'Name' => \cli\table\Column::ALIGN_RIGHT ) );
$table->setHeaders( array( 'Name' ) );
$table->addRow( array( 'LongName' ) );

$out = $table->getDisplayLines();

// Should be right-aligned - "Name" is 4 chars, "LongName" is 8 chars, so column width is 8
$this->assertStringContainsString( '| Name |', $out[1] );
$this->assertStringContainsString( '| LongName |', $out[3] );
}
}