<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
* @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
namespace Zend\Text\Table;
use Traversable;
use Zend\Stdlib\ArrayUtils;
use Zend\Text\Table\Decorator\DecoratorInterface as Decorator;
/**
* Zend\Text\Table\Table enables developers to create tables out of characters
*/
class Table
{
/**
* Auto separator settings
*/
const AUTO_SEPARATE_NONE = 0x0;
const AUTO_SEPARATE_HEADER = 0x1;
const AUTO_SEPARATE_FOOTER = 0x2;
const AUTO_SEPARATE_ALL = 0x4;
/**
* Decorator used for the table borders
*
* @var Decorator
*/
protected $decorator = null;
/**
* List of all column widths
*
* @var array
*/
protected $columnWidths = null;
/**
* Rows of the table
*
* @var array
*/
protected $rows = array();
/**
* Auto separation mode
*
* @var int
*/
protected $autoSeparate = self::AUTO_SEPARATE_ALL;
/**
* Padding for columns
*
* @var int
*/
protected $padding = 0;
/**
* Default column aligns for rows created by appendRow(array $data)
*
* @var array
*/
protected $defaultColumnAligns = array();
/**
* Plugin loader for decorators
*
* @var DecoratorManager
*/
protected $decoratorManager = null;
/**
* Charset which is used for input by default
*
* @var string
*/
protected static $inputCharset = 'utf-8';
/**
* Charset which is used internally
*
* @var string
*/
protected static $outputCharset = 'utf-8';
/**
* Option keys to skip when calling setOptions()
*
* @var array
*/
protected $skipOptions = array(
'options',
'config',
'defaultColumnAlign',
);
/**
* Create a basic table object
*
* @param array|Traversable $options Configuration options
* @throws Exception\UnexpectedValueException When no columns widths were set
*/
public function __construct($options = null)
{
// Set options
if ($options instanceof Traversable) {
$options = ArrayUtils::iteratorToArray($options);
}
if (is_array($options)) {
$this->setOptions($options);
}
// If no decorator was given, use default unicode decorator
if ($this->decorator === null) {
if (static::getOutputCharset() === 'utf-8') {
$this->setDecorator('unicode');
} else {
$this->setDecorator('ascii');
}
}
}
/**
* Set options from array
*
* @param array $options Configuration for Table
* @return Table
*/
public function setOptions(array $options)
{
foreach ($options as $key => $value) {
if (in_array(strtolower($key), $this->skipOptions)) {
continue;
}
$method = 'set' . ucfirst($key);
if (method_exists($this, $method)) {
$this->$method($value);
}
}
return $this;
}
/**
* Set column widths
*
* @param array $columnWidths Widths of all columns
* @throws Exception\InvalidArgumentException When no columns were supplied
* @throws Exception\InvalidArgumentException When a column has an invalid width
* @return Table
*/
public function setColumnWidths(array $columnWidths)
{
if (count($columnWidths) === 0) {
throw new Exception\InvalidArgumentException('You must supply at least one column');
}
foreach ($columnWidths as $columnNum => $columnWidth) {
if (is_int($columnWidth) === false or $columnWidth < 1) {
throw new Exception\InvalidArgumentException('Column ' . $columnNum . ' has an invalid'
. ' column width');
}
}
$this->columnWidths = $columnWidths;
return $this;
}
/**
* Set auto separation mode
*
* @param int $autoSeparate Auto separation mode
* @return Table
*/
public function setAutoSeparate($autoSeparate)
{
$this->autoSeparate = (int) $autoSeparate;
return $this;
}
/**
* Set decorator
*
* @param Decorator|string $decorator Decorator to use
* @return Table
*/
public function setDecorator($decorator)
{
if (!$decorator instanceof Decorator) {
$decorator = $this->getDecoratorManager()->get($decorator);
}
$this->decorator = $decorator;
return $this;
}
/**
* Set the column padding
*
* @param int $padding The padding for the columns
* @return Table
*/
public function setPadding($padding)
{
$this->padding = max(0, (int) $padding);
return $this;
}
/**
* Get the plugin manager for decorators
*
* @return DecoratorManager
*/
public function getDecoratorManager()
{
if ($this->decoratorManager instanceof DecoratorManager) {
return $this->decoratorManager;
}
$this->setDecoratorManager(new DecoratorManager());
return $this->decoratorManager;
}
/**
* Set the plugin manager instance for decorators
*
* @param DecoratorManager $decoratorManager
* @return Table
*/
public function setDecoratorManager(DecoratorManager $decoratorManager)
{
$this->decoratorManager = $decoratorManager;
return $this;
}
/**
* Set default column align for rows created by appendRow(array $data)
*
* @param int $columnNum
* @param string $align
* @return Table
*/
public function setDefaultColumnAlign($columnNum, $align)
{
$this->defaultColumnAligns[$columnNum] = $align;
return $this;
}
/**
* Set the input charset for column contents
*
* @param string $charset
*/
public static function setInputCharset($charset)
{
static::$inputCharset = strtolower($charset);
}
/**
* Get the input charset for column contents
*
* @return string
*/
public static function getInputCharset()
{
return static::$inputCharset;
}
/**
* Set the output charset for column contents
*
* @param string $charset
*/
public static function setOutputCharset($charset)
{
static::$outputCharset = strtolower($charset);
}
/**
* Get the output charset for column contents
*
* @return string
*/
public static function getOutputCharset()
{
return static::$outputCharset;
}
/**
* Append a row to the table
*
* @param array|Row $row The row to append to the table
* @throws Exception\InvalidArgumentException When $row is neither an array nor Zend\Text\Table\Row
* @throws Exception\OverflowException When a row contains too many columns
* @return Table
*/
public function appendRow($row)
{
if (!is_array($row) && !($row instanceof Row)) {
throw new Exception\InvalidArgumentException('$row must be an array or instance of Zend\Text\Table\Row');
}
if (is_array($row)) {
if (count($row) > count($this->columnWidths)) {
throw new Exception\OverflowException('Row contains too many columns');
}
$data = $row;
$row = new Row();
$colNum = 0;
foreach ($data as $columnData) {
if (isset($this->defaultColumnAligns[$colNum])) {
$align = $this->defaultColumnAligns[$colNum];
} else {
$align = null;
}
$row->appendColumn(new Column($columnData, $align));
$colNum++;
}
}
$this->rows[] = $row;
return $this;
}
/**
* Render the table
*
* @throws Exception\UnexpectedValueException When no rows were added to the table
* @return string
*/
public function render()
{
// There should be at least one row
if (count($this->rows) === 0) {
throw new Exception\UnexpectedValueException('No rows were added to the table yet');
}
// Initiate the result variable
$result = '';
// Count total columns
$totalNumColumns = count($this->columnWidths);
// Check if we have a horizontal character defined
$hasHorizontal = $this->decorator->getHorizontal() !== '';
// Now render all rows, starting from the first one
$numRows = count($this->rows);
foreach ($this->rows as $rowNum => $row) {
// Get all column widths
if (isset($columnWidths) === true) {
$lastColumnWidths = $columnWidths;
}
$renderedRow = $row->render($this->columnWidths, $this->decorator, $this->padding);
$columnWidths = $row->getColumnWidths();
$numColumns = count($columnWidths);
// Check what we have to draw
if ($rowNum === 0 && $hasHorizontal) {
// If this is the first row, draw the table top
$result .= $this->decorator->getTopLeft();
foreach ($columnWidths as $columnNum => $columnWidth) {
$result .= str_repeat($this->decorator->getHorizontal(),
$columnWidth);
if (($columnNum + 1) === $numColumns) {
$result .= $this->decorator->getTopRight();
} else {
$result .= $this->decorator->getHorizontalDown();
}
}
$result .= "\n";
} else {
// Else check if we have to draw the row separator
if (!$hasHorizontal) {
$drawSeparator = false; // there is no horizontal character;
} elseif ($this->autoSeparate & self::AUTO_SEPARATE_ALL) {
$drawSeparator = true;
} elseif ($rowNum === 1 && $this->autoSeparate & self::AUTO_SEPARATE_HEADER) {
$drawSeparator = true;
} elseif ($rowNum === ($numRows - 1) && $this->autoSeparate & self::AUTO_SEPARATE_FOOTER) {
$drawSeparator = true;
} else {
$drawSeparator = false;
}
if ($drawSeparator) {
$result .= $this->decorator->getVerticalRight();
$currentUpperColumn = 0;
$currentLowerColumn = 0;
$currentUpperWidth = 0;
$currentLowerWidth = 0;
// Add horizontal lines
// Loop through all column widths
foreach ($this->columnWidths as $columnNum => $columnWidth) {
// Add the horizontal line
$result .= str_repeat($this->decorator->getHorizontal(),
$columnWidth);
// If this is the last line, break out
if (($columnNum + 1) === $totalNumColumns) {
break;
}
// Else check, which connector style has to be used
$connector = 0x0;
$currentUpperWidth += $columnWidth;
$currentLowerWidth += $columnWidth;
if ($lastColumnWidths[$currentUpperColumn] === $currentUpperWidth) {
$connector |= 0x1;
$currentUpperColumn += 1;
$currentUpperWidth = 0;
} else {
$currentUpperWidth += 1;
}
if ($columnWidths[$currentLowerColumn] === $currentLowerWidth) {
$connector |= 0x2;
$currentLowerColumn += 1;
$currentLowerWidth = 0;
} else {
$currentLowerWidth += 1;
}
switch ($connector) {
case 0x0:
$result .= $this->decorator->getHorizontal();
break;
case 0x1:
$result .= $this->decorator->getHorizontalUp();
break;
case 0x2:
$result .= $this->decorator->getHorizontalDown();
break;
case 0x3:
$result .= $this->decorator->getCross();
break;
default:
// This can never happen, but the CS tells I have to have it ...
break;
}
}
$result .= $this->decorator->getVerticalLeft() . "\n";
}
}
// Add the rendered row to the result
$result .= $renderedRow;
// If this is the last row, draw the table bottom
if (($rowNum + 1) === $numRows && $hasHorizontal) {
$result .= $this->decorator->getBottomLeft();
foreach ($columnWidths as $columnNum => $columnWidth) {
$result .= str_repeat($this->decorator->getHorizontal(),
$columnWidth);
if (($columnNum + 1) === $numColumns) {
$result .= $this->decorator->getBottomRight();
} else {
$result .= $this->decorator->getHorizontalUp();
}
}
$result .= "\n";
}
}
return $result;
}
/**
* Magic method which returns the rendered table
*
* @return string
*/
public function __toString()
{
try {
return $this->render();
} catch (\Exception $e) {
trigger_error($e->getMessage(), E_USER_ERROR);
}
}
}