diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index f071bdab6a..16f16a33d9 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/src/Symfony/Component/Console/Application.php @@ -27,6 +27,7 @@ use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\FormatterHelper; use Symfony\Component\Console\Helper\DialogHelper; use Symfony\Component\Console\Helper\ProgressHelper; +use Symfony\Component\Console\Helper\TableHelper; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleForExceptionEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; @@ -1013,6 +1014,7 @@ class Application new FormatterHelper(), new DialogHelper(), new ProgressHelper(), + new TableHelper(), )); } diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 8402f3f8cd..512903e20b 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 2.3.0 ----- + * added Table Helper for tabular data rendering * added support for events in `Application` * added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()` * added a way to set the progress bar progress via the `setCurrent` method diff --git a/src/Symfony/Component/Console/Helper/FormatterHelper.php b/src/Symfony/Component/Console/Helper/FormatterHelper.php index 20c754ccf1..a5f1d1ccad 100644 --- a/src/Symfony/Component/Console/Helper/FormatterHelper.php +++ b/src/Symfony/Component/Console/Helper/FormatterHelper.php @@ -70,26 +70,6 @@ class FormatterHelper extends Helper return implode("\n", $messages); } - /** - * Returns the length of a string, using mb_strlen if it is available. - * - * @param string $string The string to check its length - * - * @return integer The length of the string - */ - private function strlen($string) - { - if (!function_exists('mb_strlen')) { - return strlen($string); - } - - if (false === $encoding = mb_detect_encoding($string)) { - return strlen($string); - } - - return mb_strlen($string, $encoding); - } - /** * {@inheritDoc} */ diff --git a/src/Symfony/Component/Console/Helper/Helper.php b/src/Symfony/Component/Console/Helper/Helper.php index 28488cafd8..534b9f4319 100644 --- a/src/Symfony/Component/Console/Helper/Helper.php +++ b/src/Symfony/Component/Console/Helper/Helper.php @@ -39,4 +39,24 @@ abstract class Helper implements HelperInterface { return $this->helperSet; } + + /** + * Returns the length of a string, using mb_strlen if it is available. + * + * @param string $string The string to check its length + * + * @return integer The length of the string + */ + protected function strlen($string) + { + if (!function_exists('mb_strlen')) { + return strlen($string); + } + + if (false === $encoding = mb_detect_encoding($string)) { + return strlen($string); + } + + return mb_strlen($string, $encoding); + } } diff --git a/src/Symfony/Component/Console/Helper/ProgressHelper.php b/src/Symfony/Component/Console/Helper/ProgressHelper.php index 0a32545ba4..96a6ffb3fc 100644 --- a/src/Symfony/Component/Console/Helper/ProgressHelper.php +++ b/src/Symfony/Component/Console/Helper/ProgressHelper.php @@ -14,7 +14,7 @@ namespace Symfony\Component\Console\Helper; use Symfony\Component\Console\Output\OutputInterface; /** - * The Progress class providers helpers to display progress output. + * The Progress class provides helpers to display progress output. * * @author Chris Jones * @author Fabien Potencier @@ -320,7 +320,7 @@ class ProgressHelper extends Helper } if ($this->max > 0) { - $this->widths['max'] = $this->getLength($this->max); + $this->widths['max'] = $this->strlen($this->max); $this->widths['current'] = $this->widths['max']; } else { $this->barCharOriginal = $this->barChar; @@ -356,7 +356,7 @@ class ProgressHelper extends Helper } } - $emptyBars = $this->barWidth - $completeBars - $this->getLength($this->progressChar); + $emptyBars = $this->barWidth - $completeBars - $this->strlen($this->progressChar); $bar = str_repeat($this->barChar, $completeBars); if ($completeBars < $this->barWidth) { $bar .= $this->progressChar; @@ -419,7 +419,7 @@ class ProgressHelper extends Helper */ private function overwrite(OutputInterface $output, $message) { - $length = $this->getLength($message); + $length = $this->strlen($message); // append whitespace to match the last line's length if (null !== $this->lastMessagesLength && $this->lastMessagesLength > $length) { @@ -430,26 +430,7 @@ class ProgressHelper extends Helper $output->write("\x0D"); $output->write($message); - $this->lastMessagesLength = $this->getLength($message); - } - - /** - * Wrapper arround strlen: uses multi-byte function if available - * - * @param string $string - * @return integer - */ - private function getLength($string) - { - if (!function_exists('mb_strlen')) { - return strlen($string); - } - - if (false === $encoding = mb_detect_encoding($string)) { - return strlen($string); - } - - return mb_strlen($string, $encoding); + $this->lastMessagesLength = $this->strlen($message); } /** diff --git a/src/Symfony/Component/Console/Helper/TableHelper.php b/src/Symfony/Component/Console/Helper/TableHelper.php new file mode 100644 index 0000000000..cf18e4913c --- /dev/null +++ b/src/Symfony/Component/Console/Helper/TableHelper.php @@ -0,0 +1,453 @@ + + * + * 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; +use InvalidArgumentException; + +/** + * Provides helpers to display table output. + * + * @author Саша Стаменковић + */ +class TableHelper extends Helper +{ + const LAYOUT_DEFAULT = 0; + const LAYOUT_BORDERLESS = 1; + + /** + * Table headers. + * + * @var array + */ + private $headers = array(); + + /** + * Table rows. + * + * @var array + */ + private $rows = array(); + + // Rendering options + private $paddingChar; + private $horizontalBorderChar; + private $verticalBorderChar; + private $crossingChar; + private $cellHeaderFormat; + private $cellRowFormat; + private $borderFormat; + private $padType; + + /** + * Column widths cache. + * + * @var array + */ + private $columnWidths = array(); + + /** + * Number of columns cache. + * + * @var array + */ + private $numberOfColumns; + + /** + * @var OutputInterface + */ + private $output; + + public function __construct() + { + $this->setLayout(self::LAYOUT_DEFAULT); + } + + /** + * Sets table layout type. + * + * @param int $layout self::LAYOUT_* + * + * @return TableHelper + */ + public function setLayout($layout) + { + switch ($layout) { + case self::LAYOUT_BORDERLESS: + $this + ->setPaddingChar(' ') + ->setHorizontalBorderChar('=') + ->setVerticalBorderChar(' ') + ->setCrossingChar(' ') + ->setCellHeaderFormat('%s') + ->setCellRowFormat('%s') + ->setBorderFormat('%s') + ->setPadType(STR_PAD_RIGHT) + ; + break; + + case self::LAYOUT_DEFAULT: + $this + ->setPaddingChar(' ') + ->setHorizontalBorderChar('-') + ->setVerticalBorderChar('|') + ->setCrossingChar('+') + ->setCellHeaderFormat('%s') + ->setCellRowFormat('%s') + ->setBorderFormat('%s') + ->setPadType(STR_PAD_RIGHT) + ; + break; + + default: + throw new InvalidArgumentException(sprintf('Invalid table layout "%s".', $layout)); + break; + }; + + return $this; + } + + public function setHeaders(array $headers) + { + $this->headers = array_values($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(array $row) + { + $this->rows[] = array_values($row); + + return $this; + } + + public function setRow($column, array $row) + { + $this->rows[$column] = $row; + + return $this; + } + + /** + * Sets padding character, used for cell padding. + * + * @param string $paddingChar + * + * @return TableHelper + */ + public function setPaddingChar($paddingChar) + { + $this->paddingChar = $paddingChar; + + return $this; + } + + /** + * Sets horizontal border character. + * + * @param string $horizontalBorderChar + * + * @return TableHelper + */ + public function setHorizontalBorderChar($horizontalBorderChar) + { + $this->horizontalBorderChar = $horizontalBorderChar; + + return $this; + } + + /** + * Sets vertical border character. + * + * @param string $verticalBorderChar + * + * @return TableHelper + */ + public function setVerticalBorderChar($verticalBorderChar) + { + $this->verticalBorderChar = $verticalBorderChar; + + return $this; + } + + /** + * Sets crossing character. + * + * @param string $crossingChar + * + * @return TableHelper + */ + public function setCrossingChar($crossingChar) + { + $this->crossingChar = $crossingChar; + + return $this; + } + + /** + * Sets header cell format. + * + * @param string $cellHeaderFormat + * + * @return TableHelper + */ + public function setCellHeaderFormat($cellHeaderFormat) + { + $this->cellHeaderFormat = $cellHeaderFormat; + + return $this; + } + + /** + * Sets row cell format. + * + * @param string $cellRowFormat + * + * @return TableHelper + */ + public function setCellRowFormat($cellRowFormat) + { + $this->cellRowFormat = $cellRowFormat; + + return $this; + } + + /** + * Sets table border format. + * + * @param string $borderFormat + * + * @return TableHelper + */ + public function setBorderFormat($borderFormat) + { + $this->borderFormat = $borderFormat; + + return $this; + } + + /** + * Sets cell padding type. + * + * @param integer $padType STR_PAD_* + * + * @return TableHelper + */ + public function setPadType($padType) + { + $this->padType = $padType; + + 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-059-0 | The Lord of the Rings | J. R. R. Tolkien | + * +---------------+-----------------------+------------------+ + * + * @param OutputInterface $output + */ + public function render(OutputInterface $output) + { + $this->output = $output; + + $this->renderRowSeparator(); + $this->renderRow($this->headers, $this->cellHeaderFormat); + if (!empty($this->headers)) { + $this->renderRowSeparator(); + } + foreach ($this->rows as $row) { + $this->renderRow($row, $this->cellRowFormat); + } + if (!empty($this->rows)) { + $this->renderRowSeparator(); + } + + $this->cleanup(); + } + + /** + * Renders horizontal header separator. + * + * Example: +-----+-----------+-------+ + */ + private function renderRowSeparator() + { + if (0 === $count = $this->getNumberOfColumns()) { + return; + } + + $markup = $this->crossingChar; + for ($column = 0; $column < $count; $column++) { + $markup .= str_repeat($this->horizontalBorderChar, $this->getColumnWidth($column)) + .$this->crossingChar + ; + } + + $this->output->writeln(sprintf($this->borderFormat, $markup)); + } + + /** + * Renders vertical column separator. + */ + private function renderColumnSeparator() + { + $this->output->write(sprintf($this->borderFormat, $this->verticalBorderChar)); + } + + /** + * 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; + } + + $this->renderColumnSeparator(); + for ($column = 0, $count = $this->getNumberOfColumns(); $column < $count; $column++) { + $this->renderCell($row, $column, $cellFormat); + $this->renderColumnSeparator(); + } + $this->output->writeln(''); + } + + /** + * Renders table cell with padding. + * + * @param array $row + * @param integer $column + * @param string $cellFormat + */ + private function renderCell(array $row, $column, $cellFormat) + { + $cell = isset($row[$column]) ? $row[$column] : ''; + + $this->output->write(sprintf( + $cellFormat, + str_pad( + $this->paddingChar.$cell.$this->paddingChar, + $this->getColumnWidth($column), + $this->paddingChar, + $this->padType + ) + )); + } + + /** + * Gets number of columns for this table. + * + * @return int + */ + private function getNumberOfColumns() + { + if (null !== $this->numberOfColumns) { + return $this->numberOfColumns; + } + + $columns = array(0); + $columns[] = count($this->headers); + foreach ($this->rows as $row) { + $columns[] = count($row); + } + + return $this->numberOfColumns = max($columns); + } + + /** + * Gets column width. + * + * @param integer $column + * + * @return int + */ + private function getColumnWidth($column) + { + if (isset($this->columnWidths[$column])) { + return $this->columnWidths[$column]; + } + + $lengths = array(0); + $lengths[] = $this->getCellWidth($this->headers, $column); + foreach ($this->rows as $row) { + $lengths[] = $this->getCellWidth($row, $column); + } + + return $this->columnWidths[$column] = max($lengths) + 2; + } + + /** + * Gets cell width. + * + * @param array $row + * @param integer $column + * + * @return int + */ + private function getCellWidth(array $row, $column) + { + if ($column < 0) { + return 0; + } + + if (isset($row[$column])) { + return $this->strlen($row[$column]); + } + + return $this->getCellWidth($row, $column - 1); + } + + /** + * Called after rendering to cleanup cache data. + */ + private function cleanup() + { + $this->columnWidths = array(); + $this->numberOfColumns = null; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'table'; + } +} diff --git a/src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php new file mode 100644 index 0000000000..9d31a869f9 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use Symfony\Component\Console\Helper\TableHelper; +use Symfony\Component\Console\Output\StreamOutput; + +class TableHelperTest extends \PHPUnit_Framework_TestCase +{ + protected $stream; + + protected function setUp() + { + $this->stream = fopen('php://memory', 'r+'); + } + + protected function tearDown() + { + fclose($this->stream); + $this->stream = null; + } + + /** + * @dataProvider testRenderProvider + */ + public function testRender($headers, $rows, $layout, $expected) + { + $table = new TableHelper(); + $table + ->setHeaders($headers) + ->setRows($rows) + ->setLayout($layout) + ; + $table->render($output = $this->getOutputStream()); + + $this->assertEquals($expected, $this->getOutputContent($output)); + } + + /** + * @dataProvider testRenderProvider + */ + public function testRenderAddRows($headers, $rows, $layout, $expected) + { + $table = new TableHelper(); + $table + ->setHeaders($headers) + ->addRows($rows) + ->setLayout($layout) + ; + $table->render($output = $this->getOutputStream()); + + $this->assertEquals($expected, $this->getOutputContent($output)); + } + + /** + * @dataProvider testRenderProvider + */ + public function testRenderAddRowsOneByOne($headers, $rows, $layout, $expected) + { + $table = new TableHelper(); + $table + ->setHeaders($headers) + ->setLayout($layout) + ; + foreach ($rows as $row) { + $table->addRow($row); + } + $table->render($output = $this->getOutputStream()); + + $this->assertEquals($expected, $this->getOutputContent($output)); + } + + public function testRenderProvider() + { + return array( + array( + array('ISBN', 'Title', 'Author'), + array( + array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'), + array('9971-5-0210-0', 'A Tale of Two Cities', 'Charles Dickens'), + array('960-425-059-0', 'The Lord of the Rings', 'J. R. R. Tolkien'), + array('80-902734-1-6', 'And Then There Were None', 'Agatha Christie'), + ), + TableHelper::LAYOUT_DEFAULT, +<<stream); + } + + protected function getOutputContent(StreamOutput $output) + { + rewind($output->getStream()); + + return stream_get_contents($output->getStream()); + } +}