Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

[2.3] [Console] TableHelper #6368

Closed
wants to merge 11 commits into from
@umpirsky

When building a console application it may be useful to display tabular data.

TableHelper can display table header and rows, customizable alignment of columns, cell padding and colors.

Basic usage example:

$table = $app->getHelperSet()->get('table');
$table
    ->setHeaders(array('ISBN', 'Title', 'Author'))
    ->setRows(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'),
    ))
;
$table->render($output);

Output:
table

If this PR gets merged I will submit doc PR as well.

I'm sure there is a plenty of room for improvements so any feedback is welcome.

src/Symfony/Component/Console/Helper/TableHelper.php
((233 lines not shown))
+ /**
+ * 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; $column < $this->getNumberOfColumns(); $column++) {
@stloyd
stloyd added a note
for ($column = 0, $count = $this->getNumberOfColumns(); $column < $count; $column++) {

Fixed, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/Console/Helper/TableHelper.php
((201 lines not shown))
+ }
+ }
+
+ /**
+ * Renders horizontal header separator.
+ *
+ * Example: +-----+-----------+-------+
+ */
+ private function renderRowSeparator()
+ {
+ if (0 === $this->getNumberOfColumns()) {
+ return;
+ }
+
+ $markup = $this->edgeChar;
+ for ($column = 0; $column < $this->getNumberOfColumns(); $column++) {
@stloyd
stloyd added a note
for ($column = 0, $count = $this->getNumberOfColumns(); $column < $count; $column++) {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/Console/Helper/TableHelper.php
((103 lines not shown))
+ /**
+ * Sets horizontal border character.
+ *
+ * @param string $horizontalBorderChar
+ */
+ public function setHorizontalBorderChar($horizontalBorderChar)
+ {
+ $this->horizontalBorderChar = $horizontalBorderChar;
+ }
+
+ /**
+ * Sets vertical border character.
+ *
+ * @param string $verticalBorderChar
+ */
+ public function setVerticalBorderChar($verticalBorderChar)
@stloyd
stloyd added a note

Wrong indentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/Console/Helper/TableHelper.php
((85 lines not shown))
+
+ public function setRow(array $row, $column)
+ {
+ $this->rows[$column] = $row;
+
+ return $this;
+ }
+
+ /**
+ * Sets background character, used for cell padding.
+ *
+ * @param string $backgroundChar
+ */
+ public function setBackgroundChar($backgroundChar)
+ {
+ $this->backgroundChar = $backgroundChar;
@stloyd
stloyd added a note

IMO you should add return $this; too all set*() methods.

I agree, forgot them. Fixed now, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@michelsalib

Looks very useful :+1:

...ny/Component/Console/Tests/Helper/TableHelperTest.php
((62 lines not shown))
+ /**
+ * @dataProvider testRenderProvider
+ */
+ public function testRenderAddRowsOneByOne($headers, $rows, $expected)
+ {
+ $table = new TableHelper();
+ $table->setHeaders($headers);
+ foreach ($rows as $row) {
+ $table->addRow($row);
+ }
+ $table->render($output = $this->getOutputStream());
+
+ $this->assertEquals($expected, $this->getOutputContent($output));
+ }
+
+ public static function testRenderProvider()
@lyrixx
lyrixx added a note

No need for static here.

Fixed, thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stof
Collaborator

I don't like this implementation: your helper is stateful, meaning that using it twice in a command can led to weird side-effects as it will still contain some stuff from the previous table you rendered (for instance reusign the custom spearator for the second table)

@umpirsky

@stof Yes, I am aware of that, and I think users should be as well. Other helpers like `ProgressHelper' are no exception. The idea behind this helpers is to be stateful.

For example, if you want to render tables with green borders, you set green border format and they will be green until you change border format. I don't see this as a weird side-effect.

Other then that, I don't see a use case when this is not expected behavior, table style is usually standardized per application. And resetting all values to default is as easy as creating a new TableHelper instance.

@piotrpasich piotrpasich commented on the diff
src/Symfony/Component/Console/Helper/TableHelper.php
((352 lines not shown))
+ {
+ if ($column < 0) {
+ return 0;
+ }
+
+ if (isset($row[$column])) {
+ return mb_strlen($row[$column]);
+ }
+
+ return $this->getCellWidth($row, $column - 1);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getName()

I think this method should be placed higher in the file.

@stloyd
stloyd added a note

@umpirsky public methods must be declared before protected & private according to Symfony CS.

@umpirsky
umpirsky added a note

@piotrpasich @stloyd I know, I just find this method not so important, so I put it at the bottom. Fixed now.

@fabpot Owner
fabpot added a note

@umpirsky You were right, this method should be at the end like in the other helper classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@piotrpasich

+1 from my side. Nice feature

@henrikbjorn

:+1: could yield some nice debug commands :)

src/Symfony/Component/Console/Helper/TableHelper.php
((334 lines not shown))
+ $lengths = array(0);
+ $lengths[] = $this->getCellWidth($this->headers, $column);
+ foreach ($this->rows as $row) {
+ $lengths[] = $this->getCellWidth($row, $column);
+ }
+
+ return max($lengths) + 2;
+ }
+
+ /**
+ * Gets cell width.
+ *
+ * @param array $row
+ * @param int $column
+ *
+ * @return int
@Baachi
Baachi added a note

Should be integer :)

Why?
Short syntax is valid as well, check phpdoc.org:

@param bool|int $bar

and php.net:

var_dump((int) (25/7)); // int(3)

It dosent matter if it is valid or not, that is the way it is written in the rest of the phpdoc blocks :)

Fixed.

But, is this rule documented somewhere?
There are other places where short syntax is used https://gist.github.com/4352487.

Does this means that cast is done with:

(integer) $var;

as well?

Again, there are many short syntax casts in rest of the code.

yes it is. There might be some slips but we can atleast try an be consistent where it is caught :)

Can you provide link please? Don't tell me it's PSR :)

I am not saying it is a PSR or coding style. But i guess if you ack on integer instead of int, you find a lot more entries on it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Baachi

:+1: Really nice!

src/Symfony/Component/Console/Helper/TableHelper.php
((287 lines not shown))
+ /**
+ * 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->backgroundChar.$cell.$this->backgroundChar,
+ $this->getColumnWidth($column),
@Burgov
Burgov added a note

this appears to be recalculated for every row that is rendered. I'm not sure about the performance, but I think this could be improved

Yes. I was thinking about caching getNumberOfColumns() and getColumnWidth() results. It will add some complexity, and improve performance. I will check some large table benchmarks and decide if this optimization have sense.
Thanks!

I optimized rendering a bit, here are the results of my little benchmark:

+----------------+-------------------+--------------------+
| columns x rows | Before (s)        | After (s)          |
+----------------+-------------------+--------------------+
| 3 x 10         | 0.009207010269165 | 0.0074009895324707 |
| 3 x 100        | 0.20063400268555  | 0.059360027313232  |
| 3 x 1000       | 14.763768911362   | 0.61125707626343   |
+----------------+-------------------+--------------------+
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@tyler-king

+1 this is really useful to have.

@sukei

+1 really nice feature!

@WouterJ

Isn't this table much to complicated for a great display in terminals? I think it is better to have something like:

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
80-902734-1-6  And Then There Were None  Agatha Christie
@umpirsky

@WouterJ Current implementation is what I find most convenient, and is a default now, you can easily change this with something like:

$table
    ->setEdgeChar(' ')
    ->setHorizontalBorderChar('=')
    ->setVerticalBorderChar(' ')
;

If community thinks some other default layout would be better, we can change it ofc.

@stloyd

@fabpot Skipping that small CS issue, I this it's good to be merged =)

@pborreli pborreli referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
src/Symfony/Component/Console/Helper/TableHelper.php
((85 lines not shown))
+ {
+ foreach ($rows as $row) {
+ $this->addRow($row);
+ }
+
+ return $this;
+ }
+
+ public function addRow(array $row)
+ {
+ $this->rows[] = array_values($row);
+
+ return $this;
+ }
+
+ public function setRow(array $row, $column)
@fabpot Owner
fabpot added a note

This should be $column, array $row to be consistent with the rest of the framework.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/Console/Helper/TableHelper.php
((384 lines not shown))
+ {
+ if ($column < 0) {
+ return 0;
+ }
+
+ if (isset($row[$column])) {
+ return mb_strlen($row[$column]);
+ }
+
+ return $this->getCellWidth($row, $column - 1);
+ }
+
+ /**
+ * Called after rendering to cleanup cache data.
+ */
+ private function afterRender()
@fabpot Owner
fabpot added a note

what about calling it finishRendering?

Maybe postRender or even cleanup?

@fabpot Owner
fabpot added a note

cleanup sound good to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@fabpot
Owner

Looks good to me. @umpirsky Can you take my comments into account, add a note in the CHANGELOG, and submit a PR for the documentation?

src/Symfony/Component/Console/Helper/TableHelper.php
((375 lines not shown))
+ /**
+ * 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 mb_strlen($row[$column]);
@fabpot Owner
fabpot added a note

mbstring is not a required deps for Symfony, so you need to check and act accordingly like it's done elsewhere in the framework.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/Console/Helper/TableHelper.php
((26 lines not shown))
+ * @var array
+ */
+ private $headers = array();
+
+ /**
+ * Table rows.
+ *
+ * @var array
+ */
+ private $rows = array();
+
+ // Rendering options
+ private $backgroundChar = ' ';
+ private $horizontalBorderChar = '-';
+ private $verticalBorderChar = '|';
+ private $edgeChar = '+';

How about cornerChar?

Or maybe crossingChar, since it's not just for corners.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/Console/Helper/TableHelper.php
((62 lines not shown))
+ * @var OutputInterface
+ */
+ private $output;
+
+ public function setHeaders(array $headers)
+ {
+ $this->headers = array_values($headers);
+
+ return $this;
+ }
+
+ public function setRows(array $rows)
+ {
+ $this->rows = array();
+
+ foreach ($rows as $row) {

return $this->addRows($rows); ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/Symfony/Component/Console/Helper/TableHelper.php
((95 lines not shown))
+ $this->rows[] = array_values($row);
+
+ return $this;
+ }
+
+ public function setRow(array $row, $column)
+ {
+ $this->rows[$column] = $row;
+
+ return $this;
+ }
+
+ /**
+ * Sets background character, used for cell padding.
+ *
+ * @param string $backgroundChar

So maybe paddingChar according to it's purpose?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@umpirsky

@fabpot Thanks for useful comments, they are fixed.

Where in changelog this feature belongs, I see it's planned for 2.3?

I will try to submit documentation before the end of next week.

@stof
Collaborator

@umpirsky In the 2.3.0 section of the Console changelog.

@fabpot
Owner

@umpirsky Thanks for the update. I've reread the comments and I think @WouterJ suggestion is good. So, can you change the default look to what he suggests? Also, what about defining some named configuration so that you can easily use pre-defined templates?

@umpirsky

@fabpot Named configuration is great idea, I will try to fix something nice. Do you really like this borderless table? :)

For example, table layout I used is used by mysql client. It have sense for commands like doctrine:query:sql or doctrine:query:dql. On the other hand @WouterJ layout can be handy for commands like container:debug or router:debug.

It would be nice to get more feedback from other people about default table layout.

@pilot

Mysql like table layouts is more convinient, but as @umpirsky say it's depend of commands for which try to use it, so maybe have a sense add a some params which configure table view from console?

Like a git log --pretty add a --style=mysql-table or --style=borderless

@fabpot
Owner

I think we won't have an agreement on which default table layout we should use; that's why named configuration is probably needed.

@umpirsky

@fabpot But some configuration must be the default one.

@bendavies

if you are going to follow convention of most CLI tools, you start basic, then enhance from there, --pretty, --verbose etc...

@umpirsky umpirsky referenced this pull request in symfony/symfony-docs
Merged

Console table helper #2473

@fabpot fabpot referenced this pull request from a commit
@fabpot fabpot merged branch umpirsky/console-helper-table (PR #6368)
This PR was squashed before being merged into the master branch (closes #6368).

Discussion
----------

[2.3] [Console] TableHelper

When building a console application it may be useful to display tabular data.

`TableHelper` can display table header and rows, customizable alignment of columns, cell padding and colors.

Basic usage example:
```php
$table = $app->getHelperSet()->get('table');
$table
    ->setHeaders(array('ISBN', 'Title', 'Author'))
    ->setRows(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'),
    ))
;
$table->render($output);
```
Output:
![table](https://f.cloud.github.com/assets/208957/14955/6fb4f500-46ca-11e2-8435-0f6b22f96e58.png)

If this PR gets merged I will submit doc PR as well.

I'm sure there is a plenty of room for improvements so any feedback is welcome.

Commits
-------

8de7813 [2.3] [Console] TableHelper
22adfdf
@fabpot fabpot closed this pull request from a commit
@fabpot fabpot merged branch umpirsky/console-helper-table (PR #6368)
This PR was squashed before being merged into the master branch (closes #6368).

Discussion
----------

[2.3] [Console] TableHelper

When building a console application it may be useful to display tabular data.

`TableHelper` can display table header and rows, customizable alignment of columns, cell padding and colors.

Basic usage example:
```php
$table = $app->getHelperSet()->get('table');
$table
    ->setHeaders(array('ISBN', 'Title', 'Author'))
    ->setRows(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'),
    ))
;
$table->render($output);
```
Output:
![table](https://f.cloud.github.com/assets/208957/14955/6fb4f500-46ca-11e2-8435-0f6b22f96e58.png)

If this PR gets merged I will submit doc PR as well.

I'm sure there is a plenty of room for improvements so any feedback is welcome.

Commits
-------

8de7813 [2.3] [Console] TableHelper
22adfdf
@fabpot fabpot closed this in 22adfdf
@FredoVelcro

Lacks the ability to add a table footer or simply a dividing line to display totals for example...

@stof
Collaborator

@FredoVelcro Please open a new ticket for requesting new features. Commenting on a PR merged 6 months ago will get lost

@umpirsky umpirsky deleted the umpirsky:console-helper-table branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
2  src/Symfony/Component/Console/Application.php
@@ -27,6 +27,7 @@
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 @@ protected function getDefaultHelperSet()
new FormatterHelper(),
new DialogHelper(),
new ProgressHelper(),
+ new TableHelper(),
));
}
View
1  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
View
20 src/Symfony/Component/Console/Helper/FormatterHelper.php
@@ -71,26 +71,6 @@ public function formatBlock($messages, $style, $large = false)
}
/**
- * 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}
*/
public function getName()
View
20 src/Symfony/Component/Console/Helper/Helper.php
@@ -39,4 +39,24 @@ public function getHelperSet()
{
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);
+ }
}
View
29 src/Symfony/Component/Console/Helper/ProgressHelper.php
@@ -14,7 +14,7 @@
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 <leeked@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
@@ -320,7 +320,7 @@ private function initialize()
}
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 @@ private function generate($finish = false)
}
}
- $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 @@ private function humaneTime($secs)
*/
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 @@ private function overwrite(OutputInterface $output, $message)
$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);
}
/**
View
453 src/Symfony/Component/Console/Helper/TableHelper.php
@@ -0,0 +1,453 @@
+<?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;
+use InvalidArgumentException;
+
+/**
+ * Provides helpers to display table output.
+ *
+ * @author Саша Стаменковић <umpirsky@gmail.com>
+ */
+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('<info>%s</info>')
+ ->setCellRowFormat('<comment>%s</comment>')
+ ->setBorderFormat('%s')
+ ->setPadType(STR_PAD_RIGHT)
+ ;
+ break;
+
+ case self::LAYOUT_DEFAULT:
+ $this
+ ->setPaddingChar(' ')
+ ->setHorizontalBorderChar('-')
+ ->setVerticalBorderChar('|')
+ ->setCrossingChar('+')
+ ->setCellHeaderFormat('<info>%s</info>')
+ ->setCellRowFormat('<comment>%s</comment>')
+ ->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()

I think this method should be placed higher in the file.

@stloyd
stloyd added a note

@umpirsky public methods must be declared before protected & private according to Symfony CS.

@umpirsky
umpirsky added a note

@piotrpasich @stloyd I know, I just find this method not so important, so I put it at the bottom. Fixed now.

@fabpot Owner
fabpot added a note

@umpirsky You were right, this method should be at the end like in the other helper classes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ {
+ return 'table';
+ }
+}
View
188 src/Symfony/Component/Console/Tests/Helper/TableHelperTest.php
@@ -0,0 +1,188 @@
+<?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\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,
+<<<TABLE
++---------------+--------------------------+------------------+
+| 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 |
+| 80-902734-1-6 | And Then There Were None | Agatha Christie |
++---------------+--------------------------+------------------+
+
+TABLE
+ ),
+ 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_BORDERLESS,
+ " =============== ========================== ================== \n ISBN Title Author \n =============== ========================== ================== \n 99921-58-10-7 Divine Comedy Dante Alighieri \n 9971-5-0210-0 A Tale of Two Cities Charles Dickens \n 960-425-059-0 The Lord of the Rings J. R. R. Tolkien \n 80-902734-1-6 And Then There Were None Agatha Christie \n =============== ========================== ================== \n"
+ ),
+ array(
+ array('ISBN', 'Title'),
+ array(
+ array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'),
+ array('9971-5-0210-0'),
+ 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,
+<<<TABLE
++---------------+--------------------------+------------------+
+| ISBN | Title | |
++---------------+--------------------------+------------------+
+| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
+| 9971-5-0210-0 | | |
+| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
+| 80-902734-1-6 | And Then There Were None | Agatha Christie |
++---------------+--------------------------+------------------+
+
+TABLE
+ ),
+ array(
+ array(),
+ array(
+ array('99921-58-10-7', 'Divine Comedy', 'Dante Alighieri'),
+ array('9971-5-0210-0'),
+ 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,
+<<<TABLE
++---------------+--------------------------+------------------+
+| 99921-58-10-7 | Divine Comedy | Dante Alighieri |
+| 9971-5-0210-0 | | |
+| 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
+| 80-902734-1-6 | And Then There Were None | Agatha Christie |
++---------------+--------------------------+------------------+
+
+TABLE
+ ),
+ array(
+ array('ISBN', 'Title'),
+ array(),
+ TableHelper::LAYOUT_DEFAULT,
+<<<TABLE
++------+-------+
+| ISBN | Title |
++------+-------+
+
+TABLE
+ ),
+ array(
+ array(),
+ array(),
+ TableHelper::LAYOUT_DEFAULT,
+ '',
+ ),
+ );
+ }
+
+ protected function getOutputStream()
+ {
+ return new StreamOutput($this->stream);
+ }
+
+ protected function getOutputContent(StreamOutput $output)
+ {
+ rewind($output->getStream());
+
+ return stream_get_contents($output->getStream());
+ }
+}
Something went wrong with that request. Please try again.