1: | <?php
|
2: | |
3: | |
4: | |
5: | |
6: | |
7: | |
8: | |
9: | |
10: | |
11: | |
12: | |
13: | |
14: | |
15: |
|
16: |
|
17: | namespace LucidFrame\Console;
|
18: |
|
19: | |
20: | |
21: |
|
22: | class ConsoleTable
|
23: | {
|
24: | const HEADER_INDEX = -1;
|
25: | const FOOTER_INDEX = -2;
|
26: | const HR = 'HR';
|
27: |
|
28: | const ALIGN_LEFT = 'left';
|
29: | const ALIGN_RIGHT = 'right';
|
30: |
|
31: |
|
32: | protected $data = array();
|
33: |
|
34: | protected $border = true;
|
35: |
|
36: | protected $allBorders = false;
|
37: |
|
38: | protected $padding = 1;
|
39: |
|
40: | protected $indent = 0;
|
41: |
|
42: | private $rowIndex = -1;
|
43: |
|
44: | private $columnWidths = array();
|
45: |
|
46: | private $maxColumnCount = 0;
|
47: |
|
48: | private $headerColumnAligns = [];
|
49: |
|
50: | private $columnAligns = [];
|
51: |
|
52: | private $footerColumnAligns = [];
|
53: |
|
54: | |
55: | |
56: | |
57: | |
58: | |
59: |
|
60: | public function addHeader($content = '', $align = self::ALIGN_LEFT)
|
61: | {
|
62: | $this->data[self::HEADER_INDEX][] = $content;
|
63: | $this->headerColumnAligns[self::HEADER_INDEX][] = $align === self::ALIGN_RIGHT ? STR_PAD_LEFT : STR_PAD_RIGHT;
|
64: |
|
65: | return $this;
|
66: | }
|
67: |
|
68: | |
69: | |
70: | |
71: | |
72: |
|
73: | public function setHeaders(array $content)
|
74: | {
|
75: | $this->data[self::HEADER_INDEX] = $content;
|
76: |
|
77: | return $this;
|
78: | }
|
79: |
|
80: | |
81: | |
82: |
|
83: | public function getHeaders()
|
84: | {
|
85: | return $this->data[self::HEADER_INDEX] ?? null;
|
86: | }
|
87: |
|
88: | |
89: | |
90: | |
91: | |
92: |
|
93: | public function addRow(?array $data = null)
|
94: | {
|
95: | $this->rowIndex++;
|
96: |
|
97: | if (is_array($data)) {
|
98: | foreach ($data as $col => $content) {
|
99: | $this->data[$this->rowIndex][$col] = $content;
|
100: | }
|
101: |
|
102: | $this->setMaxColumnCount(count($this->data[$this->rowIndex]));
|
103: | }
|
104: |
|
105: | return $this;
|
106: | }
|
107: |
|
108: | |
109: | |
110: | |
111: | |
112: | |
113: | |
114: | |
115: |
|
116: | public function addColumn($content, $col = null, $row = null, $align = self::ALIGN_LEFT)
|
117: | {
|
118: | $row = $row ?? $this->rowIndex;
|
119: | if ($col === null) {
|
120: | $col = isset($this->data[$row]) ? count($this->data[$row]) : 0;
|
121: | }
|
122: |
|
123: | $this->data[$row][$col] = $content;
|
124: | $this->setMaxColumnCount(count($this->data[$row]));
|
125: |
|
126: |
|
127: | if (!isset($this->columnAligns[$col]) && $align === self::ALIGN_RIGHT) {
|
128: | $this->columnAligns[$col] = STR_PAD_LEFT;
|
129: | }
|
130: |
|
131: | return $this;
|
132: | }
|
133: |
|
134: | |
135: | |
136: | |
137: | |
138: | |
139: |
|
140: | public function setColumnAlign($col, $align = self::ALIGN_LEFT)
|
141: | {
|
142: | $this->columnAligns[$col] = $align === self::ALIGN_RIGHT ? STR_PAD_LEFT : STR_PAD_RIGHT;
|
143: |
|
144: | return $this;
|
145: | }
|
146: |
|
147: | |
148: | |
149: | |
150: |
|
151: | public function showBorder()
|
152: | {
|
153: | $this->border = true;
|
154: |
|
155: | return $this;
|
156: | }
|
157: |
|
158: | |
159: | |
160: | |
161: |
|
162: | public function hideBorder()
|
163: | {
|
164: | $this->border = false;
|
165: |
|
166: | return $this;
|
167: | }
|
168: |
|
169: | |
170: | |
171: | |
172: |
|
173: | public function showAllBorders()
|
174: | {
|
175: | $this->showBorder();
|
176: | $this->allBorders = true;
|
177: |
|
178: | return $this;
|
179: | }
|
180: |
|
181: | |
182: | |
183: | |
184: | |
185: |
|
186: | public function setPadding($value = 1)
|
187: | {
|
188: | $this->padding = $value;
|
189: |
|
190: | return $this;
|
191: | }
|
192: |
|
193: | |
194: | |
195: | |
196: | |
197: |
|
198: | public function setIndent($value = 0)
|
199: | {
|
200: | $this->indent = $value;
|
201: |
|
202: | return $this;
|
203: | }
|
204: |
|
205: | |
206: | |
207: | |
208: |
|
209: | public function addBorderLine()
|
210: | {
|
211: | $this->rowIndex++;
|
212: | $this->data[$this->rowIndex] = self::HR;
|
213: |
|
214: | return $this;
|
215: | }
|
216: |
|
217: | |
218: | |
219: | |
220: | |
221: | |
222: |
|
223: | public function addFooter($content = '', $align = self::ALIGN_LEFT)
|
224: | {
|
225: | $this->data[self::FOOTER_INDEX][] = $content;
|
226: | $this->footerColumnAligns[self::FOOTER_INDEX][] = $align === self::ALIGN_RIGHT ? STR_PAD_LEFT : STR_PAD_RIGHT;
|
227: |
|
228: | return $this;
|
229: | }
|
230: |
|
231: | |
232: | |
233: | |
234: | |
235: |
|
236: | public function setFooters(array $content)
|
237: | {
|
238: | $this->data[self::FOOTER_INDEX] = $content;
|
239: |
|
240: | return $this;
|
241: | }
|
242: |
|
243: | |
244: | |
245: |
|
246: | public function getFooters()
|
247: | {
|
248: | return $this->data[self::FOOTER_INDEX] ?? null;
|
249: | }
|
250: |
|
251: | |
252: | |
253: | |
254: |
|
255: | public function display()
|
256: | {
|
257: | echo $this->getTable();
|
258: | }
|
259: |
|
260: | |
261: | |
262: | |
263: |
|
264: | public function getTable()
|
265: | {
|
266: | $this->calculateColumnWidth();
|
267: |
|
268: | $output = $this->border ? $this->getBorderLine() : '';
|
269: | foreach ($this->data as $y => $row) {
|
270: | if ($y === self::FOOTER_INDEX) {
|
271: | continue;
|
272: | }
|
273: |
|
274: | if ($row === self::HR) {
|
275: | if (!$this->allBorders) {
|
276: | $output .= $this->getBorderLine();
|
277: | unset($this->data[$y]);
|
278: | }
|
279: |
|
280: | continue;
|
281: | }
|
282: |
|
283: | if ($y === self::HEADER_INDEX && count($row) < $this->maxColumnCount) {
|
284: | $row += array_fill(count($row), $this->maxColumnCount - count($row), ' ');
|
285: | }
|
286: |
|
287: | foreach ($row as $x => $cell) {
|
288: | $output .= $this->getCellOutput($x, $y, $row);
|
289: | }
|
290: | $output .= PHP_EOL;
|
291: |
|
292: | if ($y === self::HEADER_INDEX) {
|
293: | $output .= $this->getBorderLine();
|
294: | } else if ($this->allBorders) {
|
295: | $output .= $this->getBorderLine();
|
296: | }
|
297: | }
|
298: |
|
299: |
|
300: | if (isset($this->data[self::FOOTER_INDEX])) {
|
301: | if (!$this->allBorders) {
|
302: | $output .= $this->getBorderLine();
|
303: | }
|
304: |
|
305: | $row = $this->data[self::FOOTER_INDEX];
|
306: | if (count($row) < $this->maxColumnCount) {
|
307: | $row += array_fill(count($row), $this->maxColumnCount - count($row), ' ');
|
308: | }
|
309: |
|
310: | foreach ($row as $x => $cell) {
|
311: | $output .= $this->getCellOutput($x, self::FOOTER_INDEX, $row);
|
312: | }
|
313: | $output .= PHP_EOL;
|
314: | }
|
315: |
|
316: | if (!$this->allBorders) {
|
317: | $output .= $this->border ? $this->getBorderLine() : '';
|
318: | }
|
319: |
|
320: | if (PHP_SAPI !== 'cli') {
|
321: | $output = '<pre>'.$output.'</pre>';
|
322: | }
|
323: |
|
324: | return $output;
|
325: | }
|
326: |
|
327: | |
328: | |
329: | |
330: |
|
331: | private function getBorderLine()
|
332: | {
|
333: | $output = '';
|
334: |
|
335: | if (isset($this->data[0])) {
|
336: | $columnCount = count($this->data[0]);
|
337: | } elseif (isset($this->data[self::HEADER_INDEX])) {
|
338: | $columnCount = count($this->data[self::HEADER_INDEX]);
|
339: | } else {
|
340: | return $output;
|
341: | }
|
342: |
|
343: | for ($col = 0; $col < $columnCount; $col++) {
|
344: | $output .= $this->getCellOutput($col);
|
345: | }
|
346: |
|
347: | if ($this->border) {
|
348: | $output .= '+';
|
349: | }
|
350: | $output .= PHP_EOL;
|
351: |
|
352: | return $output;
|
353: | }
|
354: |
|
355: | |
356: | |
357: | |
358: | |
359: | |
360: | |
361: | |
362: |
|
363: | private function getCellOutput($colIndex, $rowIndex = null, $row = [])
|
364: | {
|
365: | $cell = $row ? $row[$colIndex] : '-';
|
366: | $padding = str_repeat($row ? ' ' : '-', $this->padding);
|
367: | $output = '';
|
368: |
|
369: | if ($colIndex === 0) {
|
370: | $output .= str_repeat(' ', $this->indent);
|
371: | }
|
372: |
|
373: | if ($this->border) {
|
374: | $output .= $row ? '|' : '+';
|
375: | }
|
376: |
|
377: | $output .= $padding;
|
378: |
|
379: |
|
380: | if ($rowIndex === self::HEADER_INDEX) {
|
381: | $alignment = $this->headerColumnAligns[$rowIndex][$colIndex] ?? STR_PAD_RIGHT;
|
382: | } elseif ($rowIndex === self::FOOTER_INDEX) {
|
383: | $alignment = $this->footerColumnAligns[$rowIndex][$colIndex] ?? STR_PAD_RIGHT;
|
384: | } else {
|
385: | $alignment = $this->columnAligns[$colIndex] ?? STR_PAD_RIGHT;
|
386: | }
|
387: |
|
388: | $output .= $this->strPadUnicode($cell, $this->getVisualWidth($colIndex, $cell), $row ? ' ' : '-', $alignment);
|
389: | $output .= $padding;
|
390: | if ($colIndex === count($row) - 1 && $this->border) {
|
391: | $output .= $row ? '|' : '+';
|
392: | }
|
393: |
|
394: | return $output;
|
395: | }
|
396: |
|
397: | |
398: | |
399: | |
400: | |
401: | |
402: | |
403: |
|
404: | private function getVisualWidth($index, $content)
|
405: | {
|
406: | $colWidth = $this->columnWidths[$index];
|
407: |
|
408: | $cell = trim(preg_replace('/\s+/', ' ', $content));
|
409: |
|
410: | $cleanContent = $this->clearTextFormatting($cell);
|
411: |
|
412: | $originalContentLen = mb_strlen($cell, 'UTF-8');
|
413: | $cleanContentLen = mb_strlen($cleanContent, 'UTF-8');
|
414: |
|
415: |
|
416: | $delta = $originalContentLen - $cleanContentLen;
|
417: |
|
418: | return $colWidth + $delta - $this->countEmojis($cleanContent);
|
419: | }
|
420: |
|
421: | |
422: | |
423: | |
424: |
|
425: | private function calculateColumnWidth()
|
426: | {
|
427: | $maxEmojiCount = [];
|
428: |
|
429: | foreach ($this->data as $row) {
|
430: | if (!is_array($row)) {
|
431: | continue;
|
432: | }
|
433: |
|
434: | foreach ($row as $x => $cell) {
|
435: | $content = $this->clearTextFormatting($cell);
|
436: | $emojiCount = $this->countEmojis($content);
|
437: | if (!isset($maxEmojiCount[$x])) {
|
438: | $maxEmojiCount[$x] = $emojiCount;
|
439: | } elseif ($emojiCount > $maxEmojiCount[$x]) {
|
440: | $maxEmojiCount[$x] = $emojiCount;
|
441: | }
|
442: | }
|
443: | }
|
444: |
|
445: | foreach ($this->data as $row) {
|
446: | if (is_array($row)) {
|
447: | foreach ($row as $x => $cell) {
|
448: | $content = $this->clearTextFormatting($cell);
|
449: | $textLength = mb_strlen($content, 'UTF-8');
|
450: | $colWidth = $textLength + $maxEmojiCount[$x];
|
451: |
|
452: | if (!isset($this->columnWidths[$x])) {
|
453: | $this->columnWidths[$x] = $colWidth;
|
454: | } else {
|
455: | if ($colWidth > $this->columnWidths[$x]) {
|
456: | $this->columnWidths[$x] = $colWidth;
|
457: | }
|
458: | }
|
459: | }
|
460: | }
|
461: | }
|
462: |
|
463: | return $this->columnWidths;
|
464: | }
|
465: |
|
466: | |
467: | |
468: | |
469: |
|
470: | private function strPadUnicode($str, $padLength, $padString = ' ', $dir = STR_PAD_RIGHT)
|
471: | {
|
472: | $strLen = mb_strlen($str, 'UTF-8');
|
473: | $padStrLen = mb_strlen($padString, 'UTF-8');
|
474: |
|
475: | if (!$strLen && ($dir == STR_PAD_RIGHT || $dir == STR_PAD_LEFT)) {
|
476: | $strLen = 1;
|
477: | }
|
478: |
|
479: | if (!$padLength || !$padStrLen || $padLength <= $strLen) {
|
480: | return $str;
|
481: | }
|
482: |
|
483: | $result = null;
|
484: | $repeat = ceil($strLen - $padStrLen + $padLength);
|
485: | if ($dir == STR_PAD_RIGHT) {
|
486: | $result = $str . str_repeat($padString, $repeat);
|
487: | $result = mb_substr($result, 0, $padLength, 'UTF-8');
|
488: | } elseif ($dir == STR_PAD_LEFT) {
|
489: | $result = str_repeat($padString, $repeat) . $str;
|
490: | $result = mb_substr($result, -$padLength, null, 'UTF-8');
|
491: | } elseif ($dir == STR_PAD_BOTH) {
|
492: | $length = ($padLength - $strLen) / 2;
|
493: | $repeat = ceil($length / $padStrLen);
|
494: | $result = mb_substr(str_repeat($padString, $repeat), 0, floor($length), 'UTF-8')
|
495: | . $str
|
496: | . mb_substr(str_repeat($padString, $repeat), 0, ceil($length), 'UTF-8');
|
497: | }
|
498: |
|
499: | return $result;
|
500: | }
|
501: |
|
502: | |
503: | |
504: | |
505: |
|
506: | private function setMaxColumnCount($count)
|
507: | {
|
508: | if ($count > $this->maxColumnCount) {
|
509: | $this->maxColumnCount = $count;
|
510: | }
|
511: | }
|
512: |
|
513: | |
514: | |
515: | |
516: | |
517: | |
518: | |
519: |
|
520: | private function clearTextFormatting($content)
|
521: | {
|
522: | return preg_replace('#\x1b[[][^A-Za-z]*[A-Za-z]#', '', $content);
|
523: | }
|
524: |
|
525: | |
526: | |
527: | |
528: | |
529: |
|
530: | private function countEmojis($text)
|
531: | {
|
532: | $emojiPattern = '([*#0-9](?>\\xEF\\xB8\\x8F)?\\xE2\\x83\\xA3|\\xC2[\\xA9\\xAE]|\\xE2..(\\xF0\\x9F\\x8F[\\xBB-\\xBF])?(?>\\xEF\\xB8\\x8F)?|\\xE3(?>\\x80[\\xB0\\xBD]|\\x8A[\\x97\\x99])(?>\\xEF\\xB8\\x8F)?|\\xF0\\x9F(?>[\\x80-\\x86].(?>\\xEF\\xB8\\x8F)?|\\x87.\\xF0\\x9F\\x87.|..(\\xF0\\x9F\\x8F[\\xBB-\\xBF])?|(((?<zwj>\\xE2\\x80\\x8D)\\xE2\\x9D\\xA4\\xEF\\xB8\\x8F\k<zwj>\\xF0\\x9F..(\k<zwj>\\xF0\\x9F\\x91.)?|(\\xE2\\x80\\x8D\\xF0\\x9F\\x91.){2,3}))?))';
|
533: |
|
534: | preg_match_all($emojiPattern, $text, $matches);
|
535: |
|
536: | return count($matches[0]);
|
537: | }
|
538: | }
|
539: | |