mirror of
				https://github.com/yiisoft/yii2.git
				synced 2025-10-31 10:39:59 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			441 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			441 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| /**
 | |
|  * @link https://www.yiiframework.com/
 | |
|  * @copyright Copyright (c) 2008 Yii Software LLC
 | |
|  * @license https://www.yiiframework.com/license/
 | |
|  */
 | |
| 
 | |
| namespace yii\console\widgets;
 | |
| 
 | |
| use Yii;
 | |
| use yii\base\Widget;
 | |
| use yii\helpers\ArrayHelper;
 | |
| use yii\helpers\Console;
 | |
| 
 | |
| /**
 | |
|  * Table class displays a table in console.
 | |
|  *
 | |
|  * For example,
 | |
|  *
 | |
|  * ```php
 | |
|  * $table = new Table();
 | |
|  *
 | |
|  * echo $table
 | |
|  *     ->setHeaders(['test1', 'test2', 'test3'])
 | |
|  *     ->setRows([
 | |
|  *         ['col1', 'col2', 'col3'],
 | |
|  *         ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
 | |
|  *     ])
 | |
|  *     ->run();
 | |
|  * ```
 | |
|  *
 | |
|  * or
 | |
|  *
 | |
|  * ```php
 | |
|  * echo Table::widget([
 | |
|  *     'headers' => ['test1', 'test2', 'test3'],
 | |
|  *     'rows' => [
 | |
|  *         ['col1', 'col2', 'col3'],
 | |
|  *         ['col1', 'col2', ['col3-0', 'col3-1', 'col3-2']],
 | |
|  *     ],
 | |
|  * ]);
 | |
|  *
 | |
|  * @property-write string $listPrefix List prefix.
 | |
|  * @property-write int $screenWidth Screen width.
 | |
|  *
 | |
|  * @author Daniel Gomez Pan <pana_1990@hotmail.com>
 | |
|  * @since 2.0.13
 | |
|  */
 | |
| class Table extends Widget
 | |
| {
 | |
|     const DEFAULT_CONSOLE_SCREEN_WIDTH = 120;
 | |
|     const CONSOLE_SCROLLBAR_OFFSET = 3;
 | |
|     const CHAR_TOP = 'top';
 | |
|     const CHAR_TOP_MID = 'top-mid';
 | |
|     const CHAR_TOP_LEFT = 'top-left';
 | |
|     const CHAR_TOP_RIGHT = 'top-right';
 | |
|     const CHAR_BOTTOM = 'bottom';
 | |
|     const CHAR_BOTTOM_MID = 'bottom-mid';
 | |
|     const CHAR_BOTTOM_LEFT = 'bottom-left';
 | |
|     const CHAR_BOTTOM_RIGHT = 'bottom-right';
 | |
|     const CHAR_LEFT = 'left';
 | |
|     const CHAR_LEFT_MID = 'left-mid';
 | |
|     const CHAR_MID = 'mid';
 | |
|     const CHAR_MID_MID = 'mid-mid';
 | |
|     const CHAR_RIGHT = 'right';
 | |
|     const CHAR_RIGHT_MID = 'right-mid';
 | |
|     const CHAR_MIDDLE = 'middle';
 | |
| 
 | |
|     /**
 | |
|      * @var array table headers
 | |
|      * @since 2.0.19
 | |
|      */
 | |
|     protected $headers = [];
 | |
|     /**
 | |
|      * @var array table rows
 | |
|      * @since 2.0.19
 | |
|      */
 | |
|     protected $rows = [];
 | |
|     /**
 | |
|      * @var array table chars
 | |
|      * @since 2.0.19
 | |
|      */
 | |
|     protected $chars = [
 | |
|         self::CHAR_TOP => '═',
 | |
|         self::CHAR_TOP_MID => '╤',
 | |
|         self::CHAR_TOP_LEFT => '╔',
 | |
|         self::CHAR_TOP_RIGHT => '╗',
 | |
|         self::CHAR_BOTTOM => '═',
 | |
|         self::CHAR_BOTTOM_MID => '╧',
 | |
|         self::CHAR_BOTTOM_LEFT => '╚',
 | |
|         self::CHAR_BOTTOM_RIGHT => '╝',
 | |
|         self::CHAR_LEFT => '║',
 | |
|         self::CHAR_LEFT_MID => '╟',
 | |
|         self::CHAR_MID => '─',
 | |
|         self::CHAR_MID_MID => '┼',
 | |
|         self::CHAR_RIGHT => '║',
 | |
|         self::CHAR_RIGHT_MID => '╢',
 | |
|         self::CHAR_MIDDLE => '│',
 | |
|     ];
 | |
|     /**
 | |
|      * @var array table column widths
 | |
|      * @since 2.0.19
 | |
|      */
 | |
|     protected $columnWidths = [];
 | |
|     /**
 | |
|      * @var int screen width
 | |
|      * @since 2.0.19
 | |
|      */
 | |
|     protected $screenWidth;
 | |
|     /**
 | |
|      * @var string list prefix
 | |
|      * @since 2.0.19
 | |
|      */
 | |
|     protected $listPrefix = '• ';
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * Set table headers.
 | |
|      *
 | |
|      * @param array $headers table headers
 | |
|      * @return $this
 | |
|      */
 | |
|     public function setHeaders(array $headers)
 | |
|     {
 | |
|         $this->headers = array_values($headers);
 | |
|         return $this;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set table rows.
 | |
|      *
 | |
|      * @param array $rows table rows
 | |
|      * @return $this
 | |
|      */
 | |
|     public function setRows(array $rows)
 | |
|     {
 | |
|         $this->rows = array_map(function ($row) {
 | |
|             return array_map(function ($value) {
 | |
|                 return empty($value) && !is_numeric($value)
 | |
|                     ? ' '
 | |
|                     :  (is_array($value)
 | |
|                         ? array_values($value)
 | |
|                         : $value);
 | |
|             }, array_values($row));
 | |
|         }, $rows);
 | |
|         return $this;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set table chars.
 | |
|      *
 | |
|      * @param array $chars table chars
 | |
|      * @return $this
 | |
|      */
 | |
|     public function setChars(array $chars)
 | |
|     {
 | |
|         $this->chars = $chars;
 | |
|         return $this;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set screen width.
 | |
|      *
 | |
|      * @param int $width screen width
 | |
|      * @return $this
 | |
|      */
 | |
|     public function setScreenWidth($width)
 | |
|     {
 | |
|         $this->screenWidth = $width;
 | |
|         return $this;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Set list prefix.
 | |
|      *
 | |
|      * @param string $listPrefix list prefix
 | |
|      * @return $this
 | |
|      */
 | |
|     public function setListPrefix($listPrefix)
 | |
|     {
 | |
|         $this->listPrefix = $listPrefix;
 | |
|         return $this;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * @return string the rendered table
 | |
|      */
 | |
|     public function run()
 | |
|     {
 | |
|         $this->calculateRowsSize();
 | |
|         $headerCount = count($this->headers);
 | |
| 
 | |
|         $buffer = $this->renderSeparator(
 | |
|             $this->chars[self::CHAR_TOP_LEFT],
 | |
|             $this->chars[self::CHAR_TOP_MID],
 | |
|             $this->chars[self::CHAR_TOP],
 | |
|             $this->chars[self::CHAR_TOP_RIGHT]
 | |
|         );
 | |
|         // Header
 | |
|         if ($headerCount > 0) {
 | |
|             $buffer .= $this->renderRow(
 | |
|                 $this->headers,
 | |
|                 $this->chars[self::CHAR_LEFT],
 | |
|                 $this->chars[self::CHAR_MIDDLE],
 | |
|                 $this->chars[self::CHAR_RIGHT]
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         // Content
 | |
|         foreach ($this->rows as $i => $row) {
 | |
|             if ($i > 0 || $headerCount > 0) {
 | |
|                 $buffer .= $this->renderSeparator(
 | |
|                     $this->chars[self::CHAR_LEFT_MID],
 | |
|                     $this->chars[self::CHAR_MID_MID],
 | |
|                     $this->chars[self::CHAR_MID],
 | |
|                     $this->chars[self::CHAR_RIGHT_MID]
 | |
|                 );
 | |
|             }
 | |
|             $buffer .= $this->renderRow(
 | |
|                 $row,
 | |
|                 $this->chars[self::CHAR_LEFT],
 | |
|                 $this->chars[self::CHAR_MIDDLE],
 | |
|                 $this->chars[self::CHAR_RIGHT]
 | |
|             );
 | |
|         }
 | |
| 
 | |
|         $buffer .= $this->renderSeparator(
 | |
|             $this->chars[self::CHAR_BOTTOM_LEFT],
 | |
|             $this->chars[self::CHAR_BOTTOM_MID],
 | |
|             $this->chars[self::CHAR_BOTTOM],
 | |
|             $this->chars[self::CHAR_BOTTOM_RIGHT]
 | |
|         );
 | |
| 
 | |
|         return $buffer;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Renders a row of data into a string.
 | |
|      *
 | |
|      * @param array $row row of data
 | |
|      * @param string $spanLeft character for left border
 | |
|      * @param string $spanMiddle character for middle border
 | |
|      * @param string $spanRight character for right border
 | |
|      * @return string
 | |
|      * @see \yii\console\widgets\Table::render()
 | |
|      */
 | |
|     protected function renderRow(array $row, $spanLeft, $spanMiddle, $spanRight)
 | |
|     {
 | |
|         $size = $this->columnWidths;
 | |
| 
 | |
|         $buffer = '';
 | |
|         $arrayPointer = [];
 | |
|         $renderedChunkTexts = [];
 | |
|         for ($i = 0, ($max = $this->calculateRowHeight($row)) ?: $max = 1; $i < $max; $i++) {
 | |
|             $buffer .= $spanLeft . ' ';
 | |
|             foreach ($size as $index => $cellSize) {
 | |
|                 $cell = isset($row[$index]) ? $row[$index] : null;
 | |
|                 $prefix = '';
 | |
|                 if ($index !== 0) {
 | |
|                     $buffer .= $spanMiddle . ' ';
 | |
|                 }
 | |
| 
 | |
|                 $arrayFromMultilineString = false;
 | |
|                 if (is_string($cell)) {
 | |
|                     $cellLines = explode(PHP_EOL, $cell);
 | |
|                     if (count($cellLines) > 1) {
 | |
|                         $cell = $cellLines;
 | |
|                         $arrayFromMultilineString = true;
 | |
|                     }
 | |
|                 }
 | |
| 
 | |
|                 if (is_array($cell)) {
 | |
|                     if (empty($renderedChunkTexts[$index])) {
 | |
|                         $renderedChunkTexts[$index] = '';
 | |
|                         $start = 0;
 | |
|                         $prefix = $arrayFromMultilineString ? '' : $this->listPrefix;
 | |
|                         if (!isset($arrayPointer[$index])) {
 | |
|                             $arrayPointer[$index] = 0;
 | |
|                         }
 | |
|                     } else {
 | |
|                         $start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset);
 | |
|                     }
 | |
|                     $chunk = Console::ansiColorizedSubstr(
 | |
|                         $cell[$arrayPointer[$index]],
 | |
|                         $start,
 | |
|                         $cellSize - 2 - Console::ansiStrwidth($prefix)
 | |
|                     );
 | |
|                     $renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk);
 | |
|                     $fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]);
 | |
|                     if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) {
 | |
|                         $arrayPointer[$index]++;
 | |
|                         $renderedChunkTexts[$index] = '';
 | |
|                     }
 | |
|                 } else {
 | |
|                     $chunk = Console::ansiColorizedSubstr($cell, ($cellSize * $i) - ($i * 2), $cellSize - 2);
 | |
|                 }
 | |
|                 $chunk = $prefix . $chunk;
 | |
|                 $repeat = $cellSize - Console::ansiStrwidth($chunk) - 1;
 | |
|                 $buffer .= $chunk;
 | |
|                 if ($repeat >= 0) {
 | |
|                     $buffer .= str_repeat(' ', $repeat);
 | |
|                 }
 | |
|             }
 | |
|             $buffer .= "$spanRight\n";
 | |
|         }
 | |
| 
 | |
|         return $buffer;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Renders separator.
 | |
|      *
 | |
|      * @param string $spanLeft character for left border
 | |
|      * @param string $spanMid character for middle border
 | |
|      * @param string $spanMidMid character for middle-middle border
 | |
|      * @param string $spanRight character for right border
 | |
|      * @return string the generated separator row
 | |
|      * @see \yii\console\widgets\Table::render()
 | |
|      */
 | |
|     protected function renderSeparator($spanLeft, $spanMid, $spanMidMid, $spanRight)
 | |
|     {
 | |
|         $separator = $spanLeft;
 | |
|         foreach ($this->columnWidths as $index => $rowSize) {
 | |
|             if ($index !== 0) {
 | |
|                 $separator .= $spanMid;
 | |
|             }
 | |
|             $separator .= str_repeat($spanMidMid, $rowSize);
 | |
|         }
 | |
|         $separator .= $spanRight . "\n";
 | |
|         return $separator;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Calculate the size of rows to draw anchor of columns in console.
 | |
|      *
 | |
|      * @see \yii\console\widgets\Table::render()
 | |
|      */
 | |
|     protected function calculateRowsSize()
 | |
|     {
 | |
|         $this->columnWidths = $columns = [];
 | |
|         $totalWidth = 0;
 | |
|         $screenWidth = $this->getScreenWidth() - self::CONSOLE_SCROLLBAR_OFFSET;
 | |
| 
 | |
|         $headerCount = count($this->headers);
 | |
|         if (empty($this->rows)) {
 | |
|             $rowColCount = 0;
 | |
|         } else {
 | |
|             $rowColCount = max(array_map('count', $this->rows));
 | |
|         }
 | |
|         $count = max($headerCount, $rowColCount);
 | |
|         for ($i = 0; $i < $count; $i++) {
 | |
|             $columns[] = ArrayHelper::getColumn($this->rows, $i);
 | |
|             if ($i < $headerCount) {
 | |
|                 $columns[$i][] = $this->headers[$i];
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         foreach ($columns as $column) {
 | |
|             $columnWidth = max(array_map(function ($val) {
 | |
|                 if (is_array($val)) {
 | |
|                     return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix);
 | |
|                 }
 | |
|                 if (is_string($val)) {
 | |
|                     return max(array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val)));
 | |
|                 }
 | |
|                 return Console::ansiStrwidth($val);
 | |
|             }, $column)) + 2;
 | |
|             $this->columnWidths[] = $columnWidth;
 | |
|             $totalWidth += $columnWidth;
 | |
|         }
 | |
| 
 | |
|         if ($totalWidth > $screenWidth) {
 | |
|             $minWidth = 3;
 | |
|             $fixWidths = [];
 | |
|             $relativeWidth = $screenWidth / $totalWidth;
 | |
|             foreach ($this->columnWidths as $j => $width) {
 | |
|                 $scaledWidth = (int) ($width * $relativeWidth);
 | |
|                 if ($scaledWidth < $minWidth) {
 | |
|                     $fixWidths[$j] = 3;
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             $totalFixWidth = array_sum($fixWidths);
 | |
|             $relativeWidth = ($screenWidth - $totalFixWidth) / ($totalWidth - $totalFixWidth);
 | |
|             foreach ($this->columnWidths as $j => $width) {
 | |
|                 if (!array_key_exists($j, $fixWidths)) {
 | |
|                     $this->columnWidths[$j] = (int) ($width * $relativeWidth);
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Calculate the height of a row.
 | |
|      *
 | |
|      * @param array $row
 | |
|      * @return int maximum row per cell
 | |
|      * @see \yii\console\widgets\Table::render()
 | |
|      */
 | |
|     protected function calculateRowHeight($row)
 | |
|     {
 | |
|         $rowsPerCell = array_map(function ($size, $columnWidth) {
 | |
|             if (is_array($columnWidth)) {
 | |
|                 $rows = 0;
 | |
|                 foreach ($columnWidth as $width) {
 | |
|                     $rows +=  $size == 2 ? 0 : ceil($width / ($size - 2));
 | |
|                 }
 | |
|                 return $rows;
 | |
|             }
 | |
|             return $size == 2 || $columnWidth == 0 ? 0 : ceil($columnWidth / ($size - 2));
 | |
|         }, $this->columnWidths, array_map(function ($val) {
 | |
|             if (is_array($val)) {
 | |
|                 return array_map('yii\helpers\Console::ansiStrwidth', $val);
 | |
|             }
 | |
|             if (is_string($val)) {
 | |
|                 return array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val));
 | |
|             }
 | |
|             return Console::ansiStrwidth($val);
 | |
|         }, $row));
 | |
|         return max($rowsPerCell);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Getting screen width.
 | |
|      * If it is not able to determine screen width, default value `123` will be set.
 | |
|      *
 | |
|      * @return int screen width
 | |
|      */
 | |
|     protected function getScreenWidth()
 | |
|     {
 | |
|         if (!$this->screenWidth) {
 | |
|             $size = Console::getScreenSize();
 | |
|             $this->screenWidth = isset($size[0])
 | |
|                 ? $size[0]
 | |
|                 : self::DEFAULT_CONSOLE_SCREEN_WIDTH + self::CONSOLE_SCROLLBAR_OFFSET;
 | |
|         }
 | |
|         return $this->screenWidth;
 | |
|     }
 | |
| }
 | 
