2015-08-17 17:00:26 -07:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Console\Helper ;
use Symfony\Component\Console\Output\OutputInterface ;
2016-04-20 09:56:34 -07:00
use Symfony\Component\Console\Exception\InvalidArgumentException ;
2015-08-17 17:00:26 -07:00
/**
* Provides helpers to display a table .
*
* @ author Fabien Potencier < fabien @ symfony . com >
* @ author Саша Стаменковић < umpirsky @ gmail . com >
* @ author Abdellatif Ait boudad < a . aitboudad @ gmail . com >
2016-04-20 09:56:34 -07:00
* @ author Max Grigorian < maxakawizard @ gmail . com >
2015-08-17 17:00:26 -07:00
*/
class Table
{
/**
* Table headers .
*
* @ var array
*/
private $headers = array ();
/**
* Table rows .
*
* @ var array
*/
private $rows = array ();
/**
* Column widths cache .
*
* @ var array
*/
private $columnWidths = array ();
/**
* Number of columns cache .
*
* @ var array
*/
private $numberOfColumns ;
/**
* @ var OutputInterface
*/
private $output ;
/**
* @ var TableStyle
*/
private $style ;
2016-04-20 09:56:34 -07:00
/**
* @ var array
*/
private $columnStyles = array ();
2015-08-17 17:00:26 -07:00
private static $styles ;
public function __construct ( OutputInterface $output )
{
$this -> output = $output ;
if ( ! self :: $styles ) {
self :: $styles = self :: initStyles ();
}
$this -> setStyle ( 'default' );
}
/**
* Sets a style definition .
*
* @ param string $name The style name
* @ param TableStyle $style A TableStyle instance
*/
public static function setStyleDefinition ( $name , TableStyle $style )
{
if ( ! self :: $styles ) {
self :: $styles = self :: initStyles ();
}
self :: $styles [ $name ] = $style ;
}
/**
* Gets a style definition by name .
*
* @ param string $name The style name
*
2017-02-02 16:28:38 -08:00
* @ return TableStyle
2015-08-17 17:00:26 -07:00
*/
public static function getStyleDefinition ( $name )
{
if ( ! self :: $styles ) {
self :: $styles = self :: initStyles ();
}
2017-02-02 16:28:38 -08:00
if ( isset ( self :: $styles [ $name ])) {
return self :: $styles [ $name ];
2015-08-17 17:00:26 -07:00
}
2017-02-02 16:28:38 -08:00
throw new InvalidArgumentException ( sprintf ( 'Style "%s" is not defined.' , $name ));
2015-08-17 17:00:26 -07:00
}
/**
* Sets table style .
*
* @ param TableStyle | string $name The style name or a TableStyle instance
*
2017-02-02 16:28:38 -08:00
* @ return $this
2015-08-17 17:00:26 -07:00
*/
public function setStyle ( $name )
{
2017-02-02 16:28:38 -08:00
$this -> style = $this -> resolveStyle ( $name );
2015-08-17 17:00:26 -07:00
return $this ;
}
/**
* Gets the current table style .
*
* @ return TableStyle
*/
public function getStyle ()
{
return $this -> style ;
}
2016-04-20 09:56:34 -07:00
/**
* Sets table column style .
*
* @ param int $columnIndex Column index
* @ param TableStyle | string $name The style name or a TableStyle instance
*
2017-02-02 16:28:38 -08:00
* @ return $this
2016-04-20 09:56:34 -07:00
*/
public function setColumnStyle ( $columnIndex , $name )
{
$columnIndex = intval ( $columnIndex );
2017-02-02 16:28:38 -08:00
$this -> columnStyles [ $columnIndex ] = $this -> resolveStyle ( $name );
2016-04-20 09:56:34 -07:00
return $this ;
}
/**
* Gets the current style for a column .
*
* If style was not set , it returns the global table style .
*
* @ param int $columnIndex Column index
*
* @ return TableStyle
*/
public function getColumnStyle ( $columnIndex )
{
if ( isset ( $this -> columnStyles [ $columnIndex ])) {
return $this -> columnStyles [ $columnIndex ];
}
return $this -> getStyle ();
}
2015-08-17 17:00:26 -07:00
public function setHeaders ( array $headers )
{
$headers = array_values ( $headers );
if ( ! empty ( $headers ) && ! is_array ( $headers [ 0 ])) {
$headers = array ( $headers );
}
$this -> headers = $headers ;
return $this ;
}
public function setRows ( array $rows )
{
$this -> rows = array ();
return $this -> addRows ( $rows );
}
public function addRows ( array $rows )
{
foreach ( $rows as $row ) {
$this -> addRow ( $row );
}
return $this ;
}
public function addRow ( $row )
{
if ( $row instanceof TableSeparator ) {
$this -> rows [] = $row ;
2015-08-27 12:03:05 -07:00
return $this ;
2015-08-17 17:00:26 -07:00
}
if ( ! is_array ( $row )) {
2016-04-20 09:56:34 -07:00
throw new InvalidArgumentException ( 'A row must be an array or a TableSeparator instance.' );
2015-08-17 17:00:26 -07:00
}
$this -> rows [] = array_values ( $row );
return $this ;
}
public function setRow ( $column , array $row )
{
$this -> rows [ $column ] = $row ;
return $this ;
}
/**
* Renders table to output .
*
* Example :
* +---------------+-----------------------+------------------+
* | ISBN | Title | Author |
* +---------------+-----------------------+------------------+
* | 99921 - 58 - 10 - 7 | Divine Comedy | Dante Alighieri |
* | 9971 - 5 - 0210 - 0 | A Tale of Two Cities | Charles Dickens |
* | 960 - 425 - 05 9 - 0 | The Lord of the Rings | J . R . R . Tolkien |
* +---------------+-----------------------+------------------+
*/
public function render ()
{
$this -> calculateNumberOfColumns ();
2016-04-20 09:56:34 -07:00
$rows = $this -> buildTableRows ( $this -> rows );
$headers = $this -> buildTableRows ( $this -> headers );
$this -> calculateColumnsWidth ( array_merge ( $headers , $rows ));
2015-08-17 17:00:26 -07:00
$this -> renderRowSeparator ();
2016-04-20 09:56:34 -07:00
if ( ! empty ( $headers )) {
foreach ( $headers as $header ) {
2015-08-17 17:00:26 -07:00
$this -> renderRow ( $header , $this -> style -> getCellHeaderFormat ());
$this -> renderRowSeparator ();
}
}
2016-04-20 09:56:34 -07:00
foreach ( $rows as $row ) {
2015-08-17 17:00:26 -07:00
if ( $row instanceof TableSeparator ) {
$this -> renderRowSeparator ();
} else {
$this -> renderRow ( $row , $this -> style -> getCellRowFormat ());
}
}
2016-04-20 09:56:34 -07:00
if ( ! empty ( $rows )) {
2015-08-17 17:00:26 -07:00
$this -> renderRowSeparator ();
}
$this -> cleanup ();
}
/**
* Renders horizontal header separator .
*
* Example : +-----+-----------+-------+
*/
private function renderRowSeparator ()
{
if ( 0 === $count = $this -> numberOfColumns ) {
return ;
}
if ( ! $this -> style -> getHorizontalBorderChar () && ! $this -> style -> getCrossingChar ()) {
return ;
}
$markup = $this -> style -> getCrossingChar ();
2015-10-08 11:40:12 -07:00
for ( $column = 0 ; $column < $count ; ++ $column ) {
2016-04-20 09:56:34 -07:00
$markup .= str_repeat ( $this -> style -> getHorizontalBorderChar (), $this -> columnWidths [ $column ]) . $this -> style -> getCrossingChar ();
2015-08-17 17:00:26 -07:00
}
$this -> output -> writeln ( sprintf ( $this -> style -> getBorderFormat (), $markup ));
}
/**
* Renders vertical column separator .
*/
private function renderColumnSeparator ()
{
2017-02-02 16:28:38 -08:00
return sprintf ( $this -> style -> getBorderFormat (), $this -> style -> getVerticalBorderChar ());
2015-08-17 17:00:26 -07:00
}
/**
* Renders table row .
*
* Example : | 9971 - 5 - 0210 - 0 | A Tale of Two Cities | Charles Dickens |
*
* @ param array $row
* @ param string $cellFormat
*/
private function renderRow ( array $row , $cellFormat )
{
if ( empty ( $row )) {
return ;
}
2017-02-02 16:28:38 -08:00
$rowContent = $this -> renderColumnSeparator ();
2015-08-17 17:00:26 -07:00
foreach ( $this -> getRowColumns ( $row ) as $column ) {
2017-02-02 16:28:38 -08:00
$rowContent .= $this -> renderCell ( $row , $column , $cellFormat );
$rowContent .= $this -> renderColumnSeparator ();
2015-08-17 17:00:26 -07:00
}
2017-02-02 16:28:38 -08:00
$this -> output -> writeln ( $rowContent );
2015-08-17 17:00:26 -07:00
}
/**
* Renders table cell with padding .
*
* @ param array $row
* @ param int $column
* @ param string $cellFormat
*/
private function renderCell ( array $row , $column , $cellFormat )
{
$cell = isset ( $row [ $column ]) ? $row [ $column ] : '' ;
2016-04-20 09:56:34 -07:00
$width = $this -> columnWidths [ $column ];
2015-08-17 17:00:26 -07:00
if ( $cell instanceof TableCell && $cell -> getColspan () > 1 ) {
// add the width of the following columns(numbers of colspan).
foreach ( range ( $column + 1 , $column + $cell -> getColspan () - 1 ) as $nextColumn ) {
2016-04-20 09:56:34 -07:00
$width += $this -> getColumnSeparatorWidth () + $this -> columnWidths [ $nextColumn ];
2015-08-17 17:00:26 -07:00
}
}
// str_pad won't work properly with multi-byte strings, we need to fix the padding
2016-04-20 09:56:34 -07:00
if ( false !== $encoding = mb_detect_encoding ( $cell , null , true )) {
2015-08-17 17:00:26 -07:00
$width += strlen ( $cell ) - mb_strwidth ( $cell , $encoding );
}
2016-04-20 09:56:34 -07:00
$style = $this -> getColumnStyle ( $column );
2015-08-17 17:00:26 -07:00
if ( $cell instanceof TableSeparator ) {
2017-02-02 16:28:38 -08:00
return sprintf ( $style -> getBorderFormat (), str_repeat ( $style -> getHorizontalBorderChar (), $width ));
2015-08-17 17:00:26 -07:00
}
2017-02-02 16:28:38 -08:00
$width += Helper :: strlen ( $cell ) - Helper :: strlenWithoutDecoration ( $this -> output -> getFormatter (), $cell );
$content = sprintf ( $style -> getCellRowContentFormat (), $cell );
return sprintf ( $cellFormat , str_pad ( $content , $width , $style -> getPaddingChar (), $style -> getPadType ()));
2015-08-17 17:00:26 -07:00
}
/**
* Calculate number of columns for this table .
*/
private function calculateNumberOfColumns ()
{
if ( null !== $this -> numberOfColumns ) {
return ;
}
$columns = array ( 0 );
foreach ( array_merge ( $this -> headers , $this -> rows ) as $row ) {
if ( $row instanceof TableSeparator ) {
continue ;
}
$columns [] = $this -> getNumberOfColumns ( $row );
}
2016-04-20 09:56:34 -07:00
$this -> numberOfColumns = max ( $columns );
2015-08-17 17:00:26 -07:00
}
private function buildTableRows ( $rows )
{
$unmergedRows = array ();
2015-10-08 11:40:12 -07:00
for ( $rowKey = 0 ; $rowKey < count ( $rows ); ++ $rowKey ) {
2015-08-17 17:00:26 -07:00
$rows = $this -> fillNextRows ( $rows , $rowKey );
// Remove any new line breaks and replace it with a new line
foreach ( $rows [ $rowKey ] as $column => $cell ) {
if ( ! strstr ( $cell , " \n " )) {
continue ;
}
2017-04-13 15:53:35 +01:00
$lines = explode ( " \n " , str_replace ( " \n " , " <fg=default;bg=default> \n </> " , $cell ));
2015-08-17 17:00:26 -07:00
foreach ( $lines as $lineKey => $line ) {
if ( $cell instanceof TableCell ) {
$line = new TableCell ( $line , array ( 'colspan' => $cell -> getColspan ()));
}
if ( 0 === $lineKey ) {
$rows [ $rowKey ][ $column ] = $line ;
} else {
$unmergedRows [ $rowKey ][ $lineKey ][ $column ] = $line ;
}
}
}
}
$tableRows = array ();
foreach ( $rows as $rowKey => $row ) {
2016-04-20 09:56:34 -07:00
$tableRows [] = $this -> fillCells ( $row );
2015-08-17 17:00:26 -07:00
if ( isset ( $unmergedRows [ $rowKey ])) {
$tableRows = array_merge ( $tableRows , $unmergedRows [ $rowKey ]);
}
}
return $tableRows ;
}
/**
* fill rows that contains rowspan > 1.
*
* @ param array $rows
2015-10-08 11:40:12 -07:00
* @ param int $line
2015-08-17 17:00:26 -07:00
*
* @ return array
*/
private function fillNextRows ( $rows , $line )
{
$unmergedRows = array ();
foreach ( $rows [ $line ] as $column => $cell ) {
if ( $cell instanceof TableCell && $cell -> getRowspan () > 1 ) {
2015-08-27 12:03:05 -07:00
$nbLines = $cell -> getRowspan () - 1 ;
2015-08-17 17:00:26 -07:00
$lines = array ( $cell );
if ( strstr ( $cell , " \n " )) {
2017-04-13 15:53:35 +01:00
$lines = explode ( " \n " , str_replace ( " \n " , " <fg=default;bg=default> \n </> " , $cell ));
2015-08-17 17:00:26 -07:00
$nbLines = count ( $lines ) > $nbLines ? substr_count ( $cell , " \n " ) : $nbLines ;
$rows [ $line ][ $column ] = new TableCell ( $lines [ 0 ], array ( 'colspan' => $cell -> getColspan ()));
unset ( $lines [ 0 ]);
}
// create a two dimensional array (rowspan x colspan)
2017-02-02 16:28:38 -08:00
$unmergedRows = array_replace_recursive ( array_fill ( $line + 1 , $nbLines , array ()), $unmergedRows );
2015-08-17 17:00:26 -07:00
foreach ( $unmergedRows as $unmergedRowKey => $unmergedRow ) {
$value = isset ( $lines [ $unmergedRowKey - $line ]) ? $lines [ $unmergedRowKey - $line ] : '' ;
$unmergedRows [ $unmergedRowKey ][ $column ] = new TableCell ( $value , array ( 'colspan' => $cell -> getColspan ()));
2017-04-13 15:53:35 +01:00
if ( $nbLines === $unmergedRowKey - $line ) {
break ;
}
2015-08-17 17:00:26 -07:00
}
}
}
foreach ( $unmergedRows as $unmergedRowKey => $unmergedRow ) {
// we need to know if $unmergedRow will be merged or inserted into $rows
if ( isset ( $rows [ $unmergedRowKey ]) && is_array ( $rows [ $unmergedRowKey ]) && ( $this -> getNumberOfColumns ( $rows [ $unmergedRowKey ]) + $this -> getNumberOfColumns ( $unmergedRows [ $unmergedRowKey ]) <= $this -> numberOfColumns )) {
foreach ( $unmergedRow as $cellKey => $cell ) {
// insert cell into row at cellKey position
array_splice ( $rows [ $unmergedRowKey ], $cellKey , 0 , array ( $cell ));
}
} else {
2015-08-27 12:03:05 -07:00
$row = $this -> copyRow ( $rows , $unmergedRowKey - 1 );
2015-08-17 17:00:26 -07:00
foreach ( $unmergedRow as $column => $cell ) {
if ( ! empty ( $cell )) {
$row [ $column ] = $unmergedRow [ $column ];
}
}
array_splice ( $rows , $unmergedRowKey , 0 , array ( $row ));
}
}
return $rows ;
}
/**
* fill cells for a row that contains colspan > 1.
*
* @ param array $row
*
* @ return array
*/
2016-04-20 09:56:34 -07:00
private function fillCells ( $row )
2015-08-17 17:00:26 -07:00
{
2016-04-20 09:56:34 -07:00
$newRow = array ();
foreach ( $row as $column => $cell ) {
$newRow [] = $cell ;
if ( $cell instanceof TableCell && $cell -> getColspan () > 1 ) {
foreach ( range ( $column + 1 , $column + $cell -> getColspan () - 1 ) as $position ) {
// insert empty value at column position
$newRow [] = '' ;
}
2015-08-17 17:00:26 -07:00
}
}
2016-04-20 09:56:34 -07:00
return $newRow ? : $row ;
2015-08-17 17:00:26 -07:00
}
/**
* @ param array $rows
* @ param int $line
*
* @ return array
*/
private function copyRow ( $rows , $line )
{
$row = $rows [ $line ];
foreach ( $row as $cellKey => $cellValue ) {
$row [ $cellKey ] = '' ;
if ( $cellValue instanceof TableCell ) {
$row [ $cellKey ] = new TableCell ( '' , array ( 'colspan' => $cellValue -> getColspan ()));
}
}
return $row ;
}
/**
* Gets number of columns by row .
*
* @ param array $row
*
* @ return int
*/
private function getNumberOfColumns ( array $row )
{
$columns = count ( $row );
foreach ( $row as $column ) {
2015-08-27 12:03:05 -07:00
$columns += $column instanceof TableCell ? ( $column -> getColspan () - 1 ) : 0 ;
2015-08-17 17:00:26 -07:00
}
return $columns ;
}
/**
* Gets list of columns for the given row .
*
* @ param array $row
*
2016-04-20 09:56:34 -07:00
* @ return array
2015-08-17 17:00:26 -07:00
*/
private function getRowColumns ( $row )
{
2015-08-27 12:03:05 -07:00
$columns = range ( 0 , $this -> numberOfColumns - 1 );
2015-08-17 17:00:26 -07:00
foreach ( $row as $cellKey => $cell ) {
if ( $cell instanceof TableCell && $cell -> getColspan () > 1 ) {
// exclude grouped columns.
2015-08-27 12:03:05 -07:00
$columns = array_diff ( $columns , range ( $cellKey + 1 , $cellKey + $cell -> getColspan () - 1 ));
2015-08-17 17:00:26 -07:00
}
}
return $columns ;
}
/**
2016-04-20 09:56:34 -07:00
* Calculates columns widths .
2015-08-17 17:00:26 -07:00
*
2016-04-20 09:56:34 -07:00
* @ param array $rows
2015-08-17 17:00:26 -07:00
*/
2016-04-20 09:56:34 -07:00
private function calculateColumnsWidth ( $rows )
2015-08-17 17:00:26 -07:00
{
2016-04-20 09:56:34 -07:00
for ( $column = 0 ; $column < $this -> numberOfColumns ; ++ $column ) {
$lengths = array ();
foreach ( $rows as $row ) {
if ( $row instanceof TableSeparator ) {
continue ;
}
2015-08-17 17:00:26 -07:00
2017-02-02 16:28:38 -08:00
foreach ( $row as $i => $cell ) {
if ( $cell instanceof TableCell ) {
2017-04-13 15:53:35 +01:00
$textContent = Helper :: removeDecoration ( $this -> output -> getFormatter (), $cell );
$textLength = Helper :: strlen ( $textContent );
2017-02-02 16:28:38 -08:00
if ( $textLength > 0 ) {
2017-04-13 15:53:35 +01:00
$contentColumns = str_split ( $textContent , ceil ( $textLength / $cell -> getColspan ()));
2017-02-02 16:28:38 -08:00
foreach ( $contentColumns as $position => $content ) {
$row [ $i + $position ] = $content ;
}
}
}
}
2016-04-20 09:56:34 -07:00
$lengths [] = $this -> getCellWidth ( $row , $column );
2015-08-17 17:00:26 -07:00
}
2016-04-20 09:56:34 -07:00
$this -> columnWidths [ $column ] = max ( $lengths ) + strlen ( $this -> style -> getCellRowContentFormat ()) - 2 ;
2015-08-17 17:00:26 -07:00
}
}
/**
* Gets column width .
*
* @ return int
*/
private function getColumnSeparatorWidth ()
{
return strlen ( sprintf ( $this -> style -> getBorderFormat (), $this -> style -> getVerticalBorderChar ()));
}
/**
* Gets cell width .
*
* @ param array $row
* @ param int $column
*
* @ return int
*/
private function getCellWidth ( array $row , $column )
{
if ( isset ( $row [ $column ])) {
$cell = $row [ $column ];
2015-08-27 12:03:05 -07:00
$cellWidth = Helper :: strlenWithoutDecoration ( $this -> output -> getFormatter (), $cell );
2015-08-17 17:00:26 -07:00
2015-08-27 12:03:05 -07:00
return $cellWidth ;
2015-08-17 17:00:26 -07:00
}
return 0 ;
}
/**
* Called after rendering to cleanup cache data .
*/
private function cleanup ()
{
$this -> columnWidths = array ();
$this -> numberOfColumns = null ;
}
private static function initStyles ()
{
$borderless = new TableStyle ();
$borderless
-> setHorizontalBorderChar ( '=' )
-> setVerticalBorderChar ( ' ' )
-> setCrossingChar ( ' ' )
;
$compact = new TableStyle ();
$compact
-> setHorizontalBorderChar ( '' )
-> setVerticalBorderChar ( ' ' )
-> setCrossingChar ( '' )
-> setCellRowContentFormat ( '%s' )
;
$styleGuide = new TableStyle ();
$styleGuide
-> setHorizontalBorderChar ( '-' )
-> setVerticalBorderChar ( ' ' )
-> setCrossingChar ( ' ' )
-> setCellHeaderFormat ( '%s' )
;
return array (
'default' => new TableStyle (),
'borderless' => $borderless ,
'compact' => $compact ,
'symfony-style-guide' => $styleGuide ,
);
}
2017-02-02 16:28:38 -08:00
private function resolveStyle ( $name )
{
if ( $name instanceof TableStyle ) {
return $name ;
}
if ( isset ( self :: $styles [ $name ])) {
return self :: $styles [ $name ];
}
throw new InvalidArgumentException ( sprintf ( 'Style "%s" is not defined.' , $name ));
}
2015-08-17 17:00:26 -07:00
}