Table.php 13 KB

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