Table.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\console\widgets;
  8. use Yii;
  9. use yii\base\Widget;
  10. use yii\helpers\ArrayHelper;
  11. use yii\helpers\Console;
  12. /**
  13. * Table class displays a table in console.
  14. *
  15. * For example,
  16. *
  17. * ```php
  18. * $table = new Table();
  19. *
  20. * echo $table
  21. * ->setHeaders(['test1', 'test2', 'test3'])
  22. * ->setRows([
  23. * ['col1', 'col2', 'col3'],
  24. * ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
  25. * ])
  26. * ->run();
  27. * ```
  28. *
  29. * or
  30. *
  31. * ```php
  32. * echo Table::widget([
  33. * 'headers' => ['test1', 'test2', 'test3'],
  34. * 'rows' => [
  35. * ['col1', 'col2', 'col3'],
  36. * ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
  37. * ],
  38. * ]);
  39. *
  40. * @author Daniel Gomez Pan <pana_1990@hotmail.com>
  41. * @since 2.0.13
  42. */
  43. class Table extends Widget
  44. {
  45. const DEFAULT_CONSOLE_SCREEN_WIDTH = 120;
  46. const CONSOLE_SCROLLBAR_OFFSET = 3;
  47. const CHAR_TOP = 'top';
  48. const CHAR_TOP_MID = 'top-mid';
  49. const CHAR_TOP_LEFT = 'top-left';
  50. const CHAR_TOP_RIGHT = 'top-right';
  51. const CHAR_BOTTOM = 'bottom';
  52. const CHAR_BOTTOM_MID = 'bottom-mid';
  53. const CHAR_BOTTOM_LEFT = 'bottom-left';
  54. const CHAR_BOTTOM_RIGHT = 'bottom-right';
  55. const CHAR_LEFT = 'left';
  56. const CHAR_LEFT_MID = 'left-mid';
  57. const CHAR_MID = 'mid';
  58. const CHAR_MID_MID = 'mid-mid';
  59. const CHAR_RIGHT = 'right';
  60. const CHAR_RIGHT_MID = 'right-mid';
  61. const CHAR_MIDDLE = 'middle';
  62. /**
  63. * @var array table headers
  64. * @since 2.0.19
  65. */
  66. protected $headers = [];
  67. /**
  68. * @var array table rows
  69. * @since 2.0.19
  70. */
  71. protected $rows = [];
  72. /**
  73. * @var array table chars
  74. * @since 2.0.19
  75. */
  76. protected $chars = [
  77. self::CHAR_TOP => '═',
  78. self::CHAR_TOP_MID => '╤',
  79. self::CHAR_TOP_LEFT => '╔',
  80. self::CHAR_TOP_RIGHT => '╗',
  81. self::CHAR_BOTTOM => '═',
  82. self::CHAR_BOTTOM_MID => '╧',
  83. self::CHAR_BOTTOM_LEFT => '╚',
  84. self::CHAR_BOTTOM_RIGHT => '╝',
  85. self::CHAR_LEFT => '║',
  86. self::CHAR_LEFT_MID => '╟',
  87. self::CHAR_MID => '─',
  88. self::CHAR_MID_MID => '┼',
  89. self::CHAR_RIGHT => '║',
  90. self::CHAR_RIGHT_MID => '╢',
  91. self::CHAR_MIDDLE => '│',
  92. ];
  93. /**
  94. * @var array table column widths
  95. * @since 2.0.19
  96. */
  97. protected $columnWidths = [];
  98. /**
  99. * @var int screen width
  100. * @since 2.0.19
  101. */
  102. protected $screenWidth;
  103. /**
  104. * @var string list prefix
  105. * @since 2.0.19
  106. */
  107. protected $listPrefix = '• ';
  108. /**
  109. * Set table headers.
  110. *
  111. * @param array $headers table headers
  112. * @return $this
  113. */
  114. public function setHeaders(array $headers)
  115. {
  116. $this->headers = array_values($headers);
  117. return $this;
  118. }
  119. /**
  120. * Set table rows.
  121. *
  122. * @param array $rows table rows
  123. * @return $this
  124. */
  125. public function setRows(array $rows)
  126. {
  127. $this->rows = array_map(function($row) {
  128. return array_map(function($value) {
  129. return empty($value) && !is_numeric($value) ? ' ' : $value;
  130. }, array_values($row));
  131. }, $rows);
  132. return $this;
  133. }
  134. /**
  135. * Set table chars.
  136. *
  137. * @param array $chars table chars
  138. * @return $this
  139. */
  140. public function setChars(array $chars)
  141. {
  142. $this->chars = $chars;
  143. return $this;
  144. }
  145. /**
  146. * Set screen width.
  147. *
  148. * @param int $width screen width
  149. * @return $this
  150. */
  151. public function setScreenWidth($width)
  152. {
  153. $this->screenWidth = $width;
  154. return $this;
  155. }
  156. /**
  157. * Set list prefix.
  158. *
  159. * @param string $listPrefix list prefix
  160. * @return $this
  161. */
  162. public function setListPrefix($listPrefix)
  163. {
  164. $this->listPrefix = $listPrefix;
  165. return $this;
  166. }
  167. /**
  168. * @return string the rendered table
  169. */
  170. public function run()
  171. {
  172. $this->calculateRowsSize();
  173. $headerCount = count($this->headers);
  174. $buffer = $this->renderSeparator(
  175. $this->chars[self::CHAR_TOP_LEFT],
  176. $this->chars[self::CHAR_TOP_MID],
  177. $this->chars[self::CHAR_TOP],
  178. $this->chars[self::CHAR_TOP_RIGHT]
  179. );
  180. // Header
  181. if ($headerCount > 0) {
  182. $buffer .= $this->renderRow($this->headers,
  183. $this->chars[self::CHAR_LEFT],
  184. $this->chars[self::CHAR_MIDDLE],
  185. $this->chars[self::CHAR_RIGHT]
  186. );
  187. }
  188. // Content
  189. foreach ($this->rows as $i => $row) {
  190. if ($i > 0 || $headerCount > 0) {
  191. $buffer .= $this->renderSeparator(
  192. $this->chars[self::CHAR_LEFT_MID],
  193. $this->chars[self::CHAR_MID_MID],
  194. $this->chars[self::CHAR_MID],
  195. $this->chars[self::CHAR_RIGHT_MID]
  196. );
  197. }
  198. $buffer .= $this->renderRow($row,
  199. $this->chars[self::CHAR_LEFT],
  200. $this->chars[self::CHAR_MIDDLE],
  201. $this->chars[self::CHAR_RIGHT]);
  202. }
  203. $buffer .= $this->renderSeparator(
  204. $this->chars[self::CHAR_BOTTOM_LEFT],
  205. $this->chars[self::CHAR_BOTTOM_MID],
  206. $this->chars[self::CHAR_BOTTOM],
  207. $this->chars[self::CHAR_BOTTOM_RIGHT]
  208. );
  209. return $buffer;
  210. }
  211. /**
  212. * Renders a row of data into a string.
  213. *
  214. * @param array $row row of data
  215. * @param string $spanLeft character for left border
  216. * @param string $spanMiddle character for middle border
  217. * @param string $spanRight character for right border
  218. * @return string
  219. * @see \yii\console\widgets\Table::render()
  220. */
  221. protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
  222. {
  223. $size = $this->columnWidths;
  224. $buffer = '';
  225. $arrayPointer = [];
  226. $finalChunk = [];
  227. for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
  228. $buffer .= $spanLeft . ' ';
  229. foreach ($size as $index => $cellSize) {
  230. $cell = isset($row[$index]) ? $row[$index] : null;
  231. $prefix = '';
  232. if ($index !== 0) {
  233. $buffer .= $spanMiddle . ' ';
  234. }
  235. if (is_array($cell)) {
  236. if (empty($finalChunk[$index])) {
  237. $finalChunk[$index] = '';
  238. $start = 0;
  239. $prefix = $this->listPrefix;
  240. if (!isset($arrayPointer[$index])) {
  241. $arrayPointer[$index] = 0;
  242. }
  243. } else {
  244. $start = mb_strwidth($finalChunk[$index], Yii::$app->charset);
  245. }
  246. $chunk = mb_substr($cell[$arrayPointer[$index]], $start, $cellSize - 4, Yii::$app->charset);
  247. $finalChunk[$index] .= $chunk;
  248. if (isset($cell[$arrayPointer[$index] + 1]) && $finalChunk[$index] === $cell[$arrayPointer[$index]]) {
  249. $arrayPointer[$index]++;
  250. $finalChunk[$index] = '';
  251. }
  252. } else {
  253. $chunk = mb_substr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2, Yii::$app->charset);
  254. }
  255. $chunk = $prefix . $chunk;
  256. $repeat = $cellSize - mb_strwidth($chunk, Yii::$app->charset) - 1;
  257. $buffer .= $chunk;
  258. if ($repeat >= 0) {
  259. $buffer .= str_repeat(' ', $repeat);
  260. }
  261. }
  262. $buffer .= "$spanRight\n";
  263. }
  264. return $buffer;
  265. }
  266. /**
  267. * Renders separator.
  268. *
  269. * @param string $spanLeft character for left border
  270. * @param string $spanMid character for middle border
  271. * @param string $spanMidMid character for middle-middle border
  272. * @param string $spanRight character for right border
  273. * @return string the generated separator row
  274. * @see \yii\console\widgets\Table::render()
  275. */
  276. protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
  277. {
  278. $separator = $spanLeft;
  279. foreach ($this->columnWidths as $index => $rowSize) {
  280. if ($index !== 0) {
  281. $separator .= $spanMid;
  282. }
  283. $separator .= str_repeat($spanMidMid, $rowSize);
  284. }
  285. $separator .= $spanRight . "\n";
  286. return $separator;
  287. }
  288. /**
  289. * Calculate the size of rows to draw anchor of columns in console.
  290. *
  291. * @see \yii\console\widgets\Table::render()
  292. */
  293. protected function calculateRowsSize()
  294. {
  295. $this->columnWidths = $columns = [];
  296. $totalWidth = 0;
  297. $screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET;
  298. $headerCount = count($this->headers);
  299. if (empty($this->rows)) {
  300. $rowColCount = 0;
  301. } else {
  302. $rowColCount = max(array_map('count', $this->rows));
  303. }
  304. $count = max($headerCount, $rowColCount);
  305. for ($i = 0; $i < $count; $i++) {
  306. $columns[] = ArrayHelper::getColumn($this->rows, $i);
  307. if ($i < $headerCount) {
  308. $columns[$i][] = $this->headers[$i];
  309. }
  310. }
  311. foreach ($columns as $column) {
  312. $columnWidth = max(array_map(function ($val) {
  313. if (is_array($val)) {
  314. $encodings = array_fill(0, count($val), Yii::$app->charset);
  315. return max(array_map('mb_strwidth', $val, $encodings)) + mb_strwidth($this->listPrefix, Yii::$app->charset);
  316. }
  317. return mb_strwidth($val, Yii::$app->charset);
  318. }, $column)) + 2;
  319. $this->columnWidths[] = $columnWidth;
  320. $totalWidth += $columnWidth;
  321. }
  322. $relativeWidth = $screenWidth / $totalWidth;
  323. if ($totalWidth > $screenWidth) {
  324. foreach ($this->columnWidths as $j => $width) {
  325. $this->columnWidths[$j] = (int) ($width * $relativeWidth);
  326. if ($j === count($this->columnWidths)) {
  327. $this->columnWidths = $totalWidth;
  328. }
  329. $totalWidth -= $this->columnWidths[$j];
  330. }
  331. }
  332. }
  333. /**
  334. * Calculate the height of a row.
  335. *
  336. * @param array $row
  337. * @return int maximum row per cell
  338. * @see \yii\console\widgets\Table::render()
  339. */
  340. protected function calculateRowHeight($row)
  341. {
  342. $rowsPerCell = array_map(function ($size, $columnWidth) {
  343. if (is_array($columnWidth)) {
  344. $rows = 0;
  345. foreach ($columnWidth as $width) {
  346. $rows += ceil($width / ($size - 2));
  347. }
  348. return $rows;
  349. }
  350. return ceil($columnWidth / ($size - 2));
  351. }, $this->columnWidths, array_map(function ($val) {
  352. if (is_array($val)) {
  353. $encodings = array_fill(0, count($val), Yii::$app->charset);
  354. return array_map('mb_strwidth', $val, $encodings);
  355. }
  356. return mb_strwidth($val, Yii::$app->charset);
  357. }, $row)
  358. );
  359. return max($rowsPerCell);
  360. }
  361. /**
  362. * Getting screen width.
  363. * If it is not able to determine screen width, default value `123` will be set.
  364. *
  365. * @return int screen width
  366. */
  367. protected function getScreenWidth()
  368. {
  369. if (!$this->screenWidth) {
  370. $size = Console::getScreenSize();
  371. $this->screenWidth = isset($size[0])
  372. ? $size[0]
  373. : self::DEFAULT_CONSOLE_SCREEN_WIDTH + self::CONSOLE_SCROLLBAR_OFFSET;
  374. }
  375. return $this->screenWidth;
  376. }
  377. }