TableHelper.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Console\Helper;
  11. use Symfony\Component\Console\Output\OutputInterface;
  12. use InvalidArgumentException;
  13. /**
  14. * Provides helpers to display table output.
  15. *
  16. * @author Саша Стаменковић <umpirsky@gmail.com>
  17. */
  18. class TableHelper extends Helper
  19. {
  20. const LAYOUT_DEFAULT = 0;
  21. const LAYOUT_BORDERLESS = 1;
  22. /**
  23. * Table headers.
  24. *
  25. * @var array
  26. */
  27. private $headers = array();
  28. /**
  29. * Table rows.
  30. *
  31. * @var array
  32. */
  33. private $rows = array();
  34. // Rendering options
  35. private $paddingChar;
  36. private $horizontalBorderChar;
  37. private $verticalBorderChar;
  38. private $crossingChar;
  39. private $cellHeaderFormat;
  40. private $cellRowFormat;
  41. private $borderFormat;
  42. private $padType;
  43. /**
  44. * Column widths cache.
  45. *
  46. * @var array
  47. */
  48. private $columnWidths = array();
  49. /**
  50. * Number of columns cache.
  51. *
  52. * @var array
  53. */
  54. private $numberOfColumns;
  55. /**
  56. * @var OutputInterface
  57. */
  58. private $output;
  59. public function __construct()
  60. {
  61. $this->setLayout(self::LAYOUT_DEFAULT);
  62. }
  63. /**
  64. * Sets table layout type.
  65. *
  66. * @param int $layout self::LAYOUT_*
  67. *
  68. * @return TableHelper
  69. */
  70. public function setLayout($layout)
  71. {
  72. switch ($layout) {
  73. case self::LAYOUT_BORDERLESS:
  74. $this
  75. ->setPaddingChar(' ')
  76. ->setHorizontalBorderChar('=')
  77. ->setVerticalBorderChar(' ')
  78. ->setCrossingChar(' ')
  79. ->setCellHeaderFormat('<info>%s</info>')
  80. ->setCellRowFormat('<comment>%s</comment>')
  81. ->setBorderFormat('%s')
  82. ->setPadType(STR_PAD_RIGHT)
  83. ;
  84. break;
  85. case self::LAYOUT_DEFAULT:
  86. $this
  87. ->setPaddingChar(' ')
  88. ->setHorizontalBorderChar('-')
  89. ->setVerticalBorderChar('|')
  90. ->setCrossingChar('+')
  91. ->setCellHeaderFormat('<info>%s</info>')
  92. ->setCellRowFormat('<comment>%s</comment>')
  93. ->setBorderFormat('%s')
  94. ->setPadType(STR_PAD_RIGHT)
  95. ;
  96. break;
  97. default:
  98. throw new InvalidArgumentException(sprintf('Invalid table layout "%s".', $layout));
  99. break;
  100. };
  101. return $this;
  102. }
  103. public function setHeaders(array $headers)
  104. {
  105. $this->headers = array_values($headers);
  106. return $this;
  107. }
  108. public function setRows(array $rows)
  109. {
  110. $this->rows = array();
  111. return $this->addRows($rows);
  112. }
  113. public function addRows(array $rows)
  114. {
  115. foreach ($rows as $row) {
  116. $this->addRow($row);
  117. }
  118. return $this;
  119. }
  120. public function addRow(array $row)
  121. {
  122. $this->rows[] = array_values($row);
  123. return $this;
  124. }
  125. public function setRow($column, array $row)
  126. {
  127. $this->rows[$column] = $row;
  128. return $this;
  129. }
  130. /**
  131. * Sets padding character, used for cell padding.
  132. *
  133. * @param string $paddingChar
  134. *
  135. * @return TableHelper
  136. */
  137. public function setPaddingChar($paddingChar)
  138. {
  139. $this->paddingChar = $paddingChar;
  140. return $this;
  141. }
  142. /**
  143. * Sets horizontal border character.
  144. *
  145. * @param string $horizontalBorderChar
  146. *
  147. * @return TableHelper
  148. */
  149. public function setHorizontalBorderChar($horizontalBorderChar)
  150. {
  151. $this->horizontalBorderChar = $horizontalBorderChar;
  152. return $this;
  153. }
  154. /**
  155. * Sets vertical border character.
  156. *
  157. * @param string $verticalBorderChar
  158. *
  159. * @return TableHelper
  160. */
  161. public function setVerticalBorderChar($verticalBorderChar)
  162. {
  163. $this->verticalBorderChar = $verticalBorderChar;
  164. return $this;
  165. }
  166. /**
  167. * Sets crossing character.
  168. *
  169. * @param string $crossingChar
  170. *
  171. * @return TableHelper
  172. */
  173. public function setCrossingChar($crossingChar)
  174. {
  175. $this->crossingChar = $crossingChar;
  176. return $this;
  177. }
  178. /**
  179. * Sets header cell format.
  180. *
  181. * @param string $cellHeaderFormat
  182. *
  183. * @return TableHelper
  184. */
  185. public function setCellHeaderFormat($cellHeaderFormat)
  186. {
  187. $this->cellHeaderFormat = $cellHeaderFormat;
  188. return $this;
  189. }
  190. /**
  191. * Sets row cell format.
  192. *
  193. * @param string $cellRowFormat
  194. *
  195. * @return TableHelper
  196. */
  197. public function setCellRowFormat($cellRowFormat)
  198. {
  199. $this->cellRowFormat = $cellRowFormat;
  200. return $this;
  201. }
  202. /**
  203. * Sets table border format.
  204. *
  205. * @param string $borderFormat
  206. *
  207. * @return TableHelper
  208. */
  209. public function setBorderFormat($borderFormat)
  210. {
  211. $this->borderFormat = $borderFormat;
  212. return $this;
  213. }
  214. /**
  215. * Sets cell padding type.
  216. *
  217. * @param integer $padType STR_PAD_*
  218. *
  219. * @return TableHelper
  220. */
  221. public function setPadType($padType)
  222. {
  223. $this->padType = $padType;
  224. return $this;
  225. }
  226. /**
  227. * Renders table to output.
  228. *
  229. * Example:
  230. * +---------------+-----------------------+------------------+
  231. * | ISBN | Title | Author |
  232. * +---------------+-----------------------+------------------+
  233. * | 99921-58-10-7 | Divine Comedy | Dante Alighieri |
  234. * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
  235. * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien |
  236. * +---------------+-----------------------+------------------+
  237. *
  238. * @param OutputInterface $output
  239. */
  240. public function render(OutputInterface $output)
  241. {
  242. $this->output = $output;
  243. $this->renderRowSeparator();
  244. $this->renderRow($this->headers, $this->cellHeaderFormat);
  245. if (!empty($this->headers)) {
  246. $this->renderRowSeparator();
  247. }
  248. foreach ($this->rows as $row) {
  249. $this->renderRow($row, $this->cellRowFormat);
  250. }
  251. if (!empty($this->rows)) {
  252. $this->renderRowSeparator();
  253. }
  254. $this->cleanup();
  255. }
  256. /**
  257. * Renders horizontal header separator.
  258. *
  259. * Example: +-----+-----------+-------+
  260. */
  261. private function renderRowSeparator()
  262. {
  263. if (0 === $count = $this->getNumberOfColumns()) {
  264. return;
  265. }
  266. $markup = $this->crossingChar;
  267. for ($column = 0; $column < $count; $column++) {
  268. $markup .= str_repeat($this->horizontalBorderChar, $this->getColumnWidth($column))
  269. .$this->crossingChar
  270. ;
  271. }
  272. $this->output->writeln(sprintf($this->borderFormat, $markup));
  273. }
  274. /**
  275. * Renders vertical column separator.
  276. */
  277. private function renderColumnSeparator()
  278. {
  279. $this->output->write(sprintf($this->borderFormat, $this->verticalBorderChar));
  280. }
  281. /**
  282. * Renders table row.
  283. *
  284. * Example: | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens |
  285. *
  286. * @param array $row
  287. * @param string $cellFormat
  288. */
  289. private function renderRow(array $row, $cellFormat)
  290. {
  291. if (empty($row)) {
  292. return;
  293. }
  294. $this->renderColumnSeparator();
  295. for ($column = 0, $count = $this->getNumberOfColumns(); $column < $count; $column++) {
  296. $this->renderCell($row, $column, $cellFormat);
  297. $this->renderColumnSeparator();
  298. }
  299. $this->output->writeln('');
  300. }
  301. /**
  302. * Renders table cell with padding.
  303. *
  304. * @param array $row
  305. * @param integer $column
  306. * @param string $cellFormat
  307. */
  308. private function renderCell(array $row, $column, $cellFormat)
  309. {
  310. $cell = isset($row[$column]) ? $row[$column] : '';
  311. $width = $this->getColumnWidth($column);
  312. // str_pad won't work properly with multi-byte strings, we need to fix the padding
  313. if (function_exists('mb_strlen') && false !== $encoding = mb_detect_encoding($cell)) {
  314. $width += strlen($cell) - mb_strlen($cell, $encoding);
  315. }
  316. $this->output->write(sprintf(
  317. $cellFormat,
  318. str_pad(
  319. $this->paddingChar.$cell.$this->paddingChar,
  320. $width,
  321. $this->paddingChar,
  322. $this->padType
  323. )
  324. ));
  325. }
  326. /**
  327. * Gets number of columns for this table.
  328. *
  329. * @return int
  330. */
  331. private function getNumberOfColumns()
  332. {
  333. if (null !== $this->numberOfColumns) {
  334. return $this->numberOfColumns;
  335. }
  336. $columns = array(0);
  337. $columns[] = count($this->headers);
  338. foreach ($this->rows as $row) {
  339. $columns[] = count($row);
  340. }
  341. return $this->numberOfColumns = max($columns);
  342. }
  343. /**
  344. * Gets column width.
  345. *
  346. * @param integer $column
  347. *
  348. * @return int
  349. */
  350. private function getColumnWidth($column)
  351. {
  352. if (isset($this->columnWidths[$column])) {
  353. return $this->columnWidths[$column];
  354. }
  355. $lengths = array(0);
  356. $lengths[] = $this->getCellWidth($this->headers, $column);
  357. foreach ($this->rows as $row) {
  358. $lengths[] = $this->getCellWidth($row, $column);
  359. }
  360. return $this->columnWidths[$column] = max($lengths) + 2;
  361. }
  362. /**
  363. * Gets cell width.
  364. *
  365. * @param array $row
  366. * @param integer $column
  367. *
  368. * @return int
  369. */
  370. private function getCellWidth(array $row, $column)
  371. {
  372. if ($column < 0) {
  373. return 0;
  374. }
  375. if (isset($row[$column])) {
  376. return $this->strlen($row[$column]);
  377. }
  378. return $this->getCellWidth($row, $column - 1);
  379. }
  380. /**
  381. * Called after rendering to cleanup cache data.
  382. */
  383. private function cleanup()
  384. {
  385. $this->columnWidths = array();
  386. $this->numberOfColumns = null;
  387. }
  388. /**
  389. * {@inheritDoc}
  390. */
  391. public function getName()
  392. {
  393. return 'table';
  394. }
  395. }