1: <?php
2: /**
3: * This file is part of the PHPLucidFrame library.
4: * The class makes you easy to build console style tables
5: *
6: * @package PHPLucidFrame\Console
7: * @since PHPLucidFrame v 1.12.0
8: * @copyright Copyright (c), PHPLucidFrame.
9: * @link http://phplucidframe.com
10: * @license http://www.opensource.org/licenses/mit-license.php MIT License
11: *
12: * This source file is subject to the MIT license that is bundled
13: * with this source code in the file LICENSE
14: */
15:
16: namespace LucidFrame\Console;
17:
18: /**
19: * The class makes you easy to build console style tables
20: */
21: class ConsoleTable
22: {
23: const HEADER_INDEX = -1;
24: const HR = 'HR';
25:
26: /** @var array Array of table data */
27: protected $data = array();
28: /** @var boolean Border shown or not */
29: protected $border = true;
30: /** @var boolean All borders shown or not */
31: protected $allBorders = false;
32: /** @var integer Table padding */
33: protected $padding = 1;
34: /** @var integer Table left margin */
35: protected $indent = 0;
36: /** @var integer */
37: private $rowIndex = -1;
38: /** @var array */
39: private $columnWidths = array();
40: /** @var int */
41: private $maxColumnCount = 0;
42:
43: /**
44: * Adds a column to the table header
45: * @param mixed Header cell content
46: * @return object LucidFrame\Console\ConsoleTable
47: */
48: public function addHeader($content = '')
49: {
50: $this->data[self::HEADER_INDEX][] = $content;
51:
52: return $this;
53: }
54:
55: /**
56: * Set headers for the columns in one-line
57: * @param array Array of header cell content
58: * @return object LucidFrame\Console\ConsoleTable
59: */
60: public function setHeaders(array $content)
61: {
62: $this->data[self::HEADER_INDEX] = $content;
63:
64: return $this;
65: }
66:
67: /**
68: * Get the row of header
69: */
70: public function getHeaders()
71: {
72: return isset($this->data[self::HEADER_INDEX]) ? $this->data[self::HEADER_INDEX] : null;
73: }
74:
75: /**
76: * Adds a row to the table
77: * @param array $data The row data to add
78: * @return object LucidFrame\Console\ConsoleTable
79: */
80: public function addRow(array $data = null)
81: {
82: $this->rowIndex++;
83:
84: if (is_array($data)) {
85: foreach ($data as $col => $content) {
86: $this->data[$this->rowIndex][$col] = $content;
87: }
88:
89: $this->setMaxColumnCount(count($this->data[$this->rowIndex]));
90: }
91:
92: return $this;
93: }
94:
95: /**
96: * Adds a column to the table
97: * @param mixed $content The data of the column
98: * @param integer $col The column index to populate
99: * @param integer $row If starting row is not zero, specify it here
100: * @return object LucidFrame\Console\ConsoleTable
101: */
102: public function addColumn($content, $col = null, $row = null)
103: {
104: $row = $row === null ? $this->rowIndex : $row;
105: if ($col === null) {
106: $col = isset($this->data[$row]) ? count($this->data[$row]) : 0;
107: }
108:
109: $this->data[$row][$col] = $content;
110: $this->setMaxColumnCount(count($this->data[$row]));
111:
112: return $this;
113: }
114:
115: /**
116: * Show table border
117: * @return object LucidFrame\Console\ConsoleTable
118: */
119: public function showBorder()
120: {
121: $this->border = true;
122:
123: return $this;
124: }
125:
126: /**
127: * Hide table border
128: * @return object LucidFrame\Console\ConsoleTable
129: */
130: public function hideBorder()
131: {
132: $this->border = false;
133:
134: return $this;
135: }
136:
137: /**
138: * Show all table borders
139: * @return object LucidFrame\Console\ConsoleTable
140: */
141: public function showAllBorders()
142: {
143: $this->showBorder();
144: $this->allBorders = true;
145:
146: return $this;
147: }
148:
149: /**
150: * Set padding for each cell
151: * @param integer $value The integer value, defaults to 1
152: * @return object LucidFrame\Console\ConsoleTable
153: */
154: public function setPadding($value = 1)
155: {
156: $this->padding = $value;
157:
158: return $this;
159: }
160:
161: /**
162: * Set left indentation for the table
163: * @param integer $value The integer value, defaults to 1
164: * @return object LucidFrame\Console\ConsoleTable
165: */
166: public function setIndent($value = 0)
167: {
168: $this->indent = $value;
169:
170: return $this;
171: }
172:
173: /**
174: * Add horizontal border line
175: * @return object LucidFrame\Console\ConsoleTable
176: */
177: public function addBorderLine()
178: {
179: $this->rowIndex++;
180: $this->data[$this->rowIndex] = self::HR;
181:
182: return $this;
183: }
184:
185: /**
186: * Print the table
187: * @return void
188: */
189: public function display()
190: {
191: echo $this->getTable();
192: }
193:
194: /**
195: * Get the printable table content
196: * @return string
197: */
198: public function getTable()
199: {
200: $this->calculateColumnWidth();
201:
202: $output = $this->border ? $this->getBorderLine() : '';
203: foreach ($this->data as $y => $row) {
204: if ($row === self::HR) {
205: if (!$this->allBorders) {
206: $output .= $this->getBorderLine();
207: unset($this->data[$y]);
208: }
209:
210: continue;
211: }
212:
213: if ($y === self::HEADER_INDEX && count($row) < $this->maxColumnCount) {
214: $row = $row + array_fill(count($row), $this->maxColumnCount - count($row), ' ');
215: }
216:
217: foreach ($row as $x => $cell) {
218: $output .= $this->getCellOutput($x, $row);
219: }
220: $output .= PHP_EOL;
221:
222: if ($y === self::HEADER_INDEX) {
223: $output .= $this->getBorderLine();
224: } else {
225: if ($this->allBorders) {
226: $output .= $this->getBorderLine();
227: }
228: }
229: }
230:
231: if (!$this->allBorders) {
232: $output .= $this->border ? $this->getBorderLine() : '';
233: }
234:
235: if (PHP_SAPI !== 'cli') {
236: $output = '<pre>'.$output.'</pre>';
237: }
238:
239: return $output;
240: }
241:
242: /**
243: * Get the printable border line
244: * @return string
245: */
246: private function getBorderLine()
247: {
248: $output = '';
249:
250: if (isset($this->data[0])) {
251: $columnCount = count($this->data[0]);
252: } elseif (isset($this->data[self::HEADER_INDEX])) {
253: $columnCount = count($this->data[self::HEADER_INDEX]);
254: } else {
255: return $output;
256: }
257:
258: for ($col = 0; $col < $columnCount; $col++) {
259: $output .= $this->getCellOutput($col);
260: }
261:
262: if ($this->border) {
263: $output .= '+';
264: }
265: $output .= PHP_EOL;
266:
267: return $output;
268: }
269:
270: /**
271: * Get the printable cell content
272: *
273: * @param integer $index The column index
274: * @param array $row The table row
275: * @return string
276: */
277: private function getCellOutput($index, $row = null)
278: {
279: $cell = $row ? $row[$index] : '-';
280: $width = $this->columnWidths[$index];
281: $padding = str_repeat($row ? ' ' : '-', $this->padding);
282:
283: $output = '';
284:
285: if ($index === 0) {
286: $output .= str_repeat(' ', $this->indent);
287: }
288:
289: if ($this->border) {
290: $output .= $row ? '|' : '+';
291: }
292:
293: $output .= $padding; # left padding
294: $cell = trim(preg_replace('/\s+/', ' ', $cell)); # remove line breaks
295: $content = preg_replace('#\x1b[[][^A-Za-z]*[A-Za-z]#', '', $cell);
296: $delta = mb_strlen($cell, 'UTF-8') - mb_strlen($content, 'UTF-8');
297: $output .= $this->strPadUnicode($cell, $width + $delta, $row ? ' ' : '-'); # cell content
298: $output .= $padding; # right padding
299: if ($row && $index == count($row) - 1 && $this->border) {
300: $output .= $row ? '|' : '+';
301: }
302:
303: return $output;
304: }
305:
306: /**
307: * Calculate maximum width of each column
308: * @return array
309: */
310: private function calculateColumnWidth()
311: {
312: foreach ($this->data as $row) {
313: if (is_array($row)) {
314: foreach ($row as $x => $col) {
315: $content = preg_replace('#\x1b[[][^A-Za-z]*[A-Za-z]#', '', $col);
316: if (!isset($this->columnWidths[$x])) {
317: $this->columnWidths[$x] = mb_strlen($content, 'UTF-8');
318: } else {
319: if (mb_strlen($content, 'UTF-8') > $this->columnWidths[$x]) {
320: $this->columnWidths[$x] = mb_strlen($content, 'UTF-8');
321: }
322: }
323: }
324: }
325: }
326:
327: return $this->columnWidths;
328: }
329:
330: /**
331: * Multibyte version of str_pad() function
332: * @source http://php.net/manual/en/function.str-pad.php
333: */
334: private function strPadUnicode($str, $padLength, $padString = ' ', $dir = STR_PAD_RIGHT)
335: {
336: $strLen = mb_strlen($str, 'UTF-8');
337: $padStrLen = mb_strlen($padString, 'UTF-8');
338:
339: if (!$strLen && ($dir == STR_PAD_RIGHT || $dir == STR_PAD_LEFT)) {
340: $strLen = 1;
341: }
342:
343: if (!$padLength || !$padStrLen || $padLength <= $strLen) {
344: return $str;
345: }
346:
347: $result = null;
348: $repeat = ceil($strLen - $padStrLen + $padLength);
349: if ($dir == STR_PAD_RIGHT) {
350: $result = $str . str_repeat($padString, $repeat);
351: $result = mb_substr($result, 0, $padLength, 'UTF-8');
352: } elseif ($dir == STR_PAD_LEFT) {
353: $result = str_repeat($padString, $repeat) . $str;
354: $result = mb_substr($result, -$padLength, null, 'UTF-8');
355: } elseif ($dir == STR_PAD_BOTH) {
356: $length = ($padLength - $strLen) / 2;
357: $repeat = ceil($length / $padStrLen);
358: $result = mb_substr(str_repeat($padString, $repeat), 0, floor($length), 'UTF-8')
359: . $str
360: . mb_substr(str_repeat($padString, $repeat), 0, ceil($length), 'UTF-8');
361: }
362:
363: return $result;
364: }
365:
366: /**
367: * Set max column count
368: * @param int $count The column count
369: */
370: private function setMaxColumnCount($count)
371: {
372: if ($count > $this->maxColumnCount) {
373: $this->maxColumnCount = $count;
374: }
375: }
376: }
377: