Merge pull request #2671 from aswinkumar863/Table-for-Xlsx

Initial implementation of Excel's tables feature
This commit is contained in:
Mark Baker 2022-04-23 18:48:19 +02:00 committed by GitHub
commit 10d175e686
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2225 additions and 3 deletions

View File

@ -5182,7 +5182,7 @@ parameters:
-
message: "#^Parameter \\#2 \\$id of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeRelationship\\(\\) expects int, string given\\.$#"
count: 4
count: 5
path: src/PhpSpreadsheet/Writer/Xlsx/Rels.php
-

View File

@ -0,0 +1,75 @@
<?php
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
require __DIR__ . '/../Header.php';
// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();
// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('aswinkumar863')
->setLastModifiedBy('aswinkumar863')
->setTitle('PhpSpreadsheet Table Test Document')
->setSubject('PhpSpreadsheet Table Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Table');
// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()->setCellValue('A1', 'Year')
->setCellValue('B1', 'Quarter')
->setCellValue('C1', 'Country')
->setCellValue('D1', 'Sales');
$dataArray = [
['2010', 'Q1', 'United States', 790],
['2010', 'Q2', 'United States', 730],
['2010', 'Q3', 'United States', 860],
['2010', 'Q4', 'United States', 850],
['2011', 'Q1', 'United States', 800],
['2011', 'Q2', 'United States', 700],
['2011', 'Q3', 'United States', 900],
['2011', 'Q4', 'United States', 950],
['2010', 'Q1', 'Belgium', 380],
['2010', 'Q2', 'Belgium', 390],
['2010', 'Q3', 'Belgium', 420],
['2010', 'Q4', 'Belgium', 460],
['2011', 'Q1', 'Belgium', 400],
['2011', 'Q2', 'Belgium', 350],
['2011', 'Q3', 'Belgium', 450],
['2011', 'Q4', 'Belgium', 500],
];
$spreadsheet->getActiveSheet()->fromArray($dataArray, null, 'A2');
// Create Table
$helper->log('Create Table');
$table = new Table('A1:D17', 'Sales_Data');
// Create Columns
$table->getColumn('D')->setShowFilterButton(false);
// Create Table Style
$helper->log('Create Table Style');
$tableStyle = new TableStyle();
$tableStyle->setTheme(TableStyle::TABLE_STYLE_MEDIUM2);
$tableStyle->setShowRowStripes(true);
$tableStyle->setShowColumnStripes(true);
$tableStyle->setShowFirstColumn(true);
$tableStyle->setShowLastColumn(true);
$table->setStyle($tableStyle);
// Add Table to Worksheet
$helper->log('Add Table to Worksheet');
$spreadsheet->getActiveSheet()->addTable($table);
// Save
$helper->write($spreadsheet, __FILE__, ['Xlsx']);

View File

@ -0,0 +1,84 @@
<?php
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
require __DIR__ . '/../Header.php';
// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();
// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('aswinkumar863')
->setLastModifiedBy('aswinkumar863')
->setTitle('PhpSpreadsheet Table Test Document')
->setSubject('PhpSpreadsheet Table Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Table');
// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$spreadsheet->getActiveSheet()->setCellValue('A1', 'Year')
->setCellValue('B1', 'Quarter')
->setCellValue('C1', 'Country')
->setCellValue('D1', 'Sales');
$dataArray = [
['2010', 'Q1', 'United States', 790],
['2010', 'Q2', 'United States', 730],
['2010', 'Q3', 'United States', 860],
['2010', 'Q4', 'United States', 850],
['2011', 'Q1', 'United States', 800],
['2011', 'Q2', 'United States', 700],
['2011', 'Q3', 'United States', 900],
['2011', 'Q4', 'United States', 950],
['2010', 'Q1', 'Belgium', 380],
['2010', 'Q2', 'Belgium', 390],
['2010', 'Q3', 'Belgium', 420],
['2010', 'Q4', 'Belgium', 460],
['2011', 'Q1', 'Belgium', 400],
['2011', 'Q2', 'Belgium', 350],
['2011', 'Q3', 'Belgium', 450],
['2011', 'Q4', 'Belgium', 500],
];
$spreadsheet->getActiveSheet()->fromArray($dataArray, null, 'A2');
// Table
$helper->log('Create Table');
$table = new Table();
$table->setName('SalesData');
$table->setShowTotalsRow(true);
$table->setRange('A1:D18'); // +1 row for totalsRow
$helper->log('Add Totals Row');
// Table column label not implemented yet,
$table->getColumn('A')->setTotalsRowLabel('Total');
// So set the label directly to the cell
$spreadsheet->getActiveSheet()->getCell('A18')->setValue('Total');
// Table column function not implemented yet,
$table->getColumn('D')->setTotalsRowFunction('sum');
// So set the formula directly to the cell
$spreadsheet->getActiveSheet()->getCell('D18')->setValue('=SUBTOTAL(109,SalesData[Sales])');
// Add Table to Worksheet
$helper->log('Add Table to Worksheet');
$spreadsheet->getActiveSheet()->addTable($table);
// Save
$path = $helper->getFilename(__FILE__);
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
// Disable precalculation to add table's total row
$writer->setPreCalculateFormulas(false);
$callStartTime = microtime(true);
$writer->save($path);
$helper->logWrite($writer, $path, $callStartTime);
$helper->logEndingNotes();

View File

@ -0,0 +1,71 @@
<?php
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
require __DIR__ . '/../Header.php';
// Create new Spreadsheet object
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();
// Set document properties
$helper->log('Set document properties');
$spreadsheet->getProperties()->setCreator('aswinkumar863')
->setLastModifiedBy('aswinkumar863')
->setTitle('PhpSpreadsheet Table Test Document')
->setSubject('PhpSpreadsheet Table Test Document')
->setDescription('Test document for PhpSpreadsheet, generated using PHP classes.')
->setKeywords('office PhpSpreadsheet php')
->setCategory('Table');
// Create the worksheet
$helper->log('Add data');
$spreadsheet->setActiveSheetIndex(0);
$columnFormula = '=SUM(Sales_Data[[#This Row],[Q1]:[Q4]])';
$dataArray = [
['Year', 'Country', 'Q1', 'Q2', 'Q3', 'Q4', 'Sales'],
[2010, 'Belgium', 380, 390, 420, 460, $columnFormula],
[2010, 'France', 510, 490, 460, 590, $columnFormula],
[2010, 'Germany', 720, 680, 640, 660, $columnFormula],
[2010, 'Italy', 440, 410, 420, 450, $columnFormula],
[2010, 'Spain', 510, 490, 470, 420, $columnFormula],
[2010, 'UK', 690, 610, 620, 600, $columnFormula],
[2010, 'United States', 790, 730, 860, 850, $columnFormula],
[2011, 'Belgium', 400, 350, 450, 500, $columnFormula],
[2011, 'France', 620, 650, 415, 570, $columnFormula],
[2011, 'Germany', 680, 620, 710, 690, $columnFormula],
[2011, 'Italy', 430, 370, 350, 335, $columnFormula],
[2011, 'Spain', 460, 390, 430, 415, $columnFormula],
[2011, 'UK', 720, 650, 580, 510, $columnFormula],
[2011, 'United States', 800, 700, 900, 950, $columnFormula],
];
$spreadsheet->getActiveSheet()->fromArray($dataArray, null, 'A1');
// Create Table
$helper->log('Create Table');
$table = new Table('A1:G15', 'Sales_Data');
$table->setRange('A1:G15');
// Set Column Formula
$table->getColumn('G')->setColumnFormula($columnFormula);
// Add Table to Worksheet
$helper->log('Add Table to Worksheet');
$spreadsheet->getActiveSheet()->addTable($table);
// Save
$path = $helper->getFilename(__FILE__);
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
// Disable precalculation to add table's total row
$writer->setPreCalculateFormulas(false);
$callStartTime = microtime(true);
$writer->save($path);
$helper->logWrite($writer, $path, $callStartTime);
$helper->logEndingNotes();

View File

@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Style\Conditional;
use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class ReferenceHelper
@ -497,6 +498,9 @@ class ReferenceHelper
// Update worksheet: autofilter
$this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns);
// Update worksheet: table
$this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns);
// Update worksheet: freeze pane
if ($worksheet->getFreezePane()) {
$splitCell = $worksheet->getFreezePane() ?? '';
@ -1026,6 +1030,85 @@ class ReferenceHelper
} while ($startColID !== $endColID);
}
private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void
{
$tableCollection = $worksheet->getTableCollection();
foreach ($tableCollection as $table) {
$tableRange = $table->getRange();
if (!empty($tableRange)) {
if ($numberOfColumns !== 0) {
$tableColumns = $table->getColumns();
if (count($tableColumns) > 0) {
$column = '';
$row = 0;
sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row);
$columnIndex = Coordinate::columnIndexFromString($column);
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange);
if ($columnIndex <= $rangeEnd[0]) {
if ($numberOfColumns < 0) {
$this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table);
}
$startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0];
// Shuffle columns in table range
if ($numberOfColumns > 0) {
$this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table);
} else {
$this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table);
}
}
}
}
$table->setRange($this->updateCellReference($tableRange));
}
}
}
private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void
{
// If we're actually deleting any columns that fall within the table range,
// then we delete any rules for those columns
$deleteColumn = $columnIndex + $numberOfColumns - 1;
$deleteCount = abs($numberOfColumns);
for ($i = 1; $i <= $deleteCount; ++$i) {
$columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1);
if (isset($tableColumns[$columnName])) {
$table->clearColumn($columnName);
}
++$deleteColumn;
}
}
private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
{
$startColRef = $startCol;
$endColRef = $rangeEnd;
$toColRef = $rangeEnd + $numberOfColumns;
do {
$table->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef));
--$endColRef;
--$toColRef;
} while ($startColRef <= $endColRef);
}
private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void
{
// For delete, we shuffle from beginning to end to avoid overwriting
$startColID = Coordinate::stringFromColumnIndex($startCol);
$toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns);
$endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1);
do {
$table->shiftColumn($startColID, $toColID);
++$startColID;
++$toColID;
} while ($startColID !== $endColID);
}
private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void
{
$beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1);

View File

@ -0,0 +1,454 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Worksheet;
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
class Table
{
/**
* Table Name.
*
* @var string
*/
private $name = '';
/**
* Show Header Row.
*
* @var bool
*/
private $showHeaderRow = true;
/**
* Show Totals Row.
*
* @var bool
*/
private $showTotalsRow = false;
/**
* Table Range.
*
* @var string
*/
private $range = '';
/**
* Table Worksheet.
*
* @var null|Worksheet
*/
private $workSheet;
/**
* Table Column.
*
* @var Table\Column[]
*/
private $columns = [];
/**
* Table Style.
*
* @var TableStyle
*/
private $style;
/**
* Create a new Table.
*
* @param AddressRange|array<int>|string $range
* A simple string containing a Cell range like 'A1:E10' is permitted
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
* @param string $name (e.g. Table1)
*/
public function __construct($range = '', string $name = '')
{
$this->setRange($range);
$this->setName($name);
$this->style = new TableStyle();
}
/**
* Get Table name.
*/
public function getName(): string
{
return $this->name;
}
/**
* Set Table name.
*/
public function setName(string $name): self
{
$name = trim($name);
if (!empty($name)) {
if (strlen($name) === 1 && in_array($name, ['C', 'c', 'R', 'r'])) {
throw new PhpSpreadsheetException('The table name is invalid');
}
if (strlen($name) > 255) {
throw new PhpSpreadsheetException('The table name cannot be longer than 255 characters');
}
// Check for A1 or R1C1 cell reference notation
if (
preg_match(Coordinate::A1_COORDINATE_REGEX, $name) ||
preg_match('/^R\[?\-?[0-9]*\]?C\[?\-?[0-9]*\]?$/i', $name)
) {
throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference');
}
if (!preg_match('/^[\p{L}_\\\\]/iu', $name)) {
throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)');
}
if (!preg_match('/^[\p{L}_\\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) {
throw new PhpSpreadsheetException('The table name contains invalid characters');
}
}
$this->name = $name;
return $this;
}
/**
* Get show Header Row.
*/
public function getShowHeaderRow(): bool
{
return $this->showHeaderRow;
}
/**
* Set show Header Row.
*/
public function setShowHeaderRow(bool $showHeaderRow): self
{
$this->showHeaderRow = $showHeaderRow;
return $this;
}
/**
* Get show Totals Row.
*/
public function getShowTotalsRow(): bool
{
return $this->showTotalsRow;
}
/**
* Set show Totals Row.
*/
public function setShowTotalsRow(bool $showTotalsRow): self
{
$this->showTotalsRow = $showTotalsRow;
return $this;
}
/**
* Get Table Range.
*/
public function getRange(): string
{
return $this->range;
}
/**
* Set Table Cell Range.
*
* @param AddressRange|array<int>|string $range
* A simple string containing a Cell range like 'A1:E10' is permitted
* or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]),
* or an AddressRange object.
*/
public function setRange($range = ''): self
{
// extract coordinate
if ($range !== '') {
[, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true);
}
if (empty($range)) {
// Discard all column rules
$this->columns = [];
$this->range = '';
return $this;
}
if (strpos($range, ':') === false) {
throw new PhpSpreadsheetException('Table must be set on a range of cells.');
}
[$width, $height] = Coordinate::rangeDimension($range);
if ($width < 1 || $height < 2) {
throw new PhpSpreadsheetException('The table range must be at least 1 column and 2 rows');
}
$this->range = $range;
// Discard any column ruless that are no longer valid within this range
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
foreach ($this->columns as $key => $value) {
$colIndex = Coordinate::columnIndexFromString($key);
if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) {
unset($this->columns[$key]);
}
}
return $this;
}
/**
* Set Table Cell Range to max row.
*/
public function setRangeToMaxRow(): self
{
if ($this->workSheet !== null) {
$thisrange = $this->range;
$range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? '';
if ($range !== $thisrange) {
$this->setRange($range);
}
}
return $this;
}
/**
* Get Table's Worksheet.
*/
public function getWorksheet(): ?Worksheet
{
return $this->workSheet;
}
/**
* Set Table's Worksheet.
*/
public function setWorksheet(?Worksheet $worksheet = null): self
{
if ($this->name !== '' && $worksheet !== null) {
$spreadsheet = $worksheet->getParent();
$tableName = StringHelper::strToUpper($this->name);
foreach ($spreadsheet->getWorksheetIterator() as $sheet) {
foreach ($sheet->getTableCollection() as $table) {
if (StringHelper::strToUpper($table->getName()) === $tableName) {
throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'");
}
}
}
}
$this->workSheet = $worksheet;
return $this;
}
/**
* Get all Table Columns.
*
* @return Table\Column[]
*/
public function getColumns(): array
{
return $this->columns;
}
/**
* Validate that the specified column is in the Table range.
*
* @param string $column Column name (e.g. A)
*
* @return int The column offset within the table range
*/
public function isColumnInRange(string $column): int
{
if (empty($this->range)) {
throw new PhpSpreadsheetException('No table range is defined.');
}
$columnIndex = Coordinate::columnIndexFromString($column);
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) {
throw new PhpSpreadsheetException('Column is outside of current table range.');
}
return $columnIndex - $rangeStart[0];
}
/**
* Get a specified Table Column Offset within the defined Table range.
*
* @param string $column Column name (e.g. A)
*
* @return int The offset of the specified column within the table range
*/
public function getColumnOffset($column): int
{
return $this->isColumnInRange($column);
}
/**
* Get a specified Table Column.
*
* @param string $column Column name (e.g. A)
*/
public function getColumn($column): Table\Column
{
$this->isColumnInRange($column);
if (!isset($this->columns[$column])) {
$this->columns[$column] = new Table\Column($column, $this);
}
return $this->columns[$column];
}
/**
* Get a specified Table Column by it's offset.
*
* @param int $columnOffset Column offset within range (starting from 0)
*/
public function getColumnByOffset($columnOffset): Table\Column
{
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range);
$pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset);
return $this->getColumn($pColumn);
}
/**
* Set Table.
*
* @param string|Table\Column $columnObjectOrString
* A simple string containing a Column ID like 'A' is permitted
*/
public function setColumn($columnObjectOrString): self
{
if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) {
$column = $columnObjectOrString;
} elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof Table\Column)) {
$column = $columnObjectOrString->getColumnIndex();
} else {
throw new PhpSpreadsheetException('Column is not within the table range.');
}
$this->isColumnInRange($column);
if (is_string($columnObjectOrString)) {
$this->columns[$columnObjectOrString] = new Table\Column($columnObjectOrString, $this);
} else {
$columnObjectOrString->setTable($this);
$this->columns[$column] = $columnObjectOrString;
}
ksort($this->columns);
return $this;
}
/**
* Clear a specified Table Column.
*
* @param string $column Column name (e.g. A)
*/
public function clearColumn($column): self
{
$this->isColumnInRange($column);
if (isset($this->columns[$column])) {
unset($this->columns[$column]);
}
return $this;
}
/**
* Shift an Table Column Rule to a different column.
*
* Note: This method bypasses validation of the destination column to ensure it is within this Table range.
* Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value.
* Use with caution.
*
* @param string $fromColumn Column name (e.g. A)
* @param string $toColumn Column name (e.g. B)
*/
public function shiftColumn($fromColumn, $toColumn): self
{
$fromColumn = strtoupper($fromColumn);
$toColumn = strtoupper($toColumn);
if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) {
$this->columns[$fromColumn]->setTable();
$this->columns[$fromColumn]->setColumnIndex($toColumn);
$this->columns[$toColumn] = $this->columns[$fromColumn];
$this->columns[$toColumn]->setTable($this);
unset($this->columns[$fromColumn]);
ksort($this->columns);
}
return $this;
}
/**
* Get table Style.
*/
public function getStyle(): Table\TableStyle
{
return $this->style;
}
/**
* Set table Style.
*/
public function setStyle(TableStyle $style): self
{
$this->style = $style;
return $this;
}
/**
* Implement PHP __clone to create a deep clone, not just a shallow copy.
*/
public function __clone()
{
$vars = get_object_vars($this);
foreach ($vars as $key => $value) {
if (is_object($value)) {
if ($key === 'workSheet') {
// Detach from worksheet
$this->{$key} = null;
} else {
$this->{$key} = clone $value;
}
} elseif ((is_array($value)) && ($key === 'columns')) {
// The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table objects
$this->{$key} = [];
foreach ($value as $k => $v) {
$this->{$key}[$k] = clone $v;
// attach the new cloned Column to this new cloned Table object
$this->{$key}[$k]->setTable($this);
}
} else {
$this->{$key} = $value;
}
}
}
/**
* toString method replicates previous behavior by returning the range if object is
* referenced as a property of its worksheet.
*/
public function __toString()
{
return (string) $this->range;
}
}

View File

@ -0,0 +1,203 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
class Column
{
/**
* Table Column Index.
*
* @var string
*/
private $columnIndex = '';
/**
* Show Filter Button.
*
* @var bool
*/
private $showFilterButton = true;
/**
* Total Row Label.
*
* @var string
*/
private $totalsRowLabel;
/**
* Total Row Function.
*
* @var string
*/
private $totalsRowFunction;
/**
* Total Row Formula.
*
* @var string
*/
private $totalsRowFormula;
/**
* Column Formula.
*
* @var string
*/
private $columnFormula;
/**
* Table.
*
* @var null|Table
*/
private $table;
/**
* Create a new Column.
*
* @param string $column Column (e.g. A)
* @param Table $table Table for this column
*/
public function __construct($column, ?Table $table = null)
{
$this->columnIndex = $column;
$this->table = $table;
}
/**
* Get Table column index as string eg: 'A'.
*/
public function getColumnIndex(): string
{
return $this->columnIndex;
}
/**
* Set Table column index as string eg: 'A'.
*
* @param string $column Column (e.g. A)
*/
public function setColumnIndex($column): self
{
// Uppercase coordinate
$column = strtoupper($column);
if ($this->table !== null) {
$this->table->isColumnInRange($column);
}
$this->columnIndex = $column;
return $this;
}
/**
* Get show Filter Button.
*/
public function getShowFilterButton(): bool
{
return $this->showFilterButton;
}
/**
* Set show Filter Button.
*/
public function setShowFilterButton(bool $showFilterButton): self
{
$this->showFilterButton = $showFilterButton;
return $this;
}
/**
* Get total Row Label.
*/
public function getTotalsRowLabel(): ?string
{
return $this->totalsRowLabel;
}
/**
* Set total Row Label.
*/
public function setTotalsRowLabel(string $totalsRowLabel): self
{
$this->totalsRowLabel = $totalsRowLabel;
return $this;
}
/**
* Get total Row Function.
*/
public function getTotalsRowFunction(): ?string
{
return $this->totalsRowFunction;
}
/**
* Set total Row Function.
*/
public function setTotalsRowFunction(string $totalsRowFunction): self
{
$this->totalsRowFunction = $totalsRowFunction;
return $this;
}
/**
* Get total Row Formula.
*/
public function getTotalsRowFormula(): ?string
{
return $this->totalsRowFormula;
}
/**
* Set total Row Formula.
*/
public function setTotalsRowFormula(string $totalsRowFormula): self
{
$this->totalsRowFormula = $totalsRowFormula;
return $this;
}
/**
* Get column Formula.
*/
public function getColumnFormula(): ?string
{
return $this->columnFormula;
}
/**
* Set column Formula.
*/
public function setColumnFormula(string $columnFormula): self
{
$this->columnFormula = $columnFormula;
return $this;
}
/**
* Get this Column's Table.
*/
public function getTable(): ?Table
{
return $this->table;
}
/**
* Set this Column's Table.
*/
public function setTable(?Table $table = null): self
{
$this->table = $table;
return $this;
}
}

View File

@ -0,0 +1,230 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
class TableStyle
{
const TABLE_STYLE_NONE = '';
const TABLE_STYLE_LIGHT1 = 'TableStyleLight1';
const TABLE_STYLE_LIGHT2 = 'TableStyleLight2';
const TABLE_STYLE_LIGHT3 = 'TableStyleLight3';
const TABLE_STYLE_LIGHT4 = 'TableStyleLight4';
const TABLE_STYLE_LIGHT5 = 'TableStyleLight5';
const TABLE_STYLE_LIGHT6 = 'TableStyleLight6';
const TABLE_STYLE_LIGHT7 = 'TableStyleLight7';
const TABLE_STYLE_LIGHT8 = 'TableStyleLight8';
const TABLE_STYLE_LIGHT9 = 'TableStyleLight9';
const TABLE_STYLE_LIGHT10 = 'TableStyleLight10';
const TABLE_STYLE_LIGHT11 = 'TableStyleLight11';
const TABLE_STYLE_LIGHT12 = 'TableStyleLight12';
const TABLE_STYLE_LIGHT13 = 'TableStyleLight13';
const TABLE_STYLE_LIGHT14 = 'TableStyleLight14';
const TABLE_STYLE_LIGHT15 = 'TableStyleLight15';
const TABLE_STYLE_LIGHT16 = 'TableStyleLight16';
const TABLE_STYLE_LIGHT17 = 'TableStyleLight17';
const TABLE_STYLE_LIGHT18 = 'TableStyleLight18';
const TABLE_STYLE_LIGHT19 = 'TableStyleLight19';
const TABLE_STYLE_LIGHT20 = 'TableStyleLight20';
const TABLE_STYLE_LIGHT21 = 'TableStyleLight21';
const TABLE_STYLE_MEDIUM1 = 'TableStyleMedium1';
const TABLE_STYLE_MEDIUM2 = 'TableStyleMedium2';
const TABLE_STYLE_MEDIUM3 = 'TableStyleMedium3';
const TABLE_STYLE_MEDIUM4 = 'TableStyleMedium4';
const TABLE_STYLE_MEDIUM5 = 'TableStyleMedium5';
const TABLE_STYLE_MEDIUM6 = 'TableStyleMedium6';
const TABLE_STYLE_MEDIUM7 = 'TableStyleMedium7';
const TABLE_STYLE_MEDIUM8 = 'TableStyleMedium8';
const TABLE_STYLE_MEDIUM9 = 'TableStyleMedium9';
const TABLE_STYLE_MEDIUM10 = 'TableStyleMedium10';
const TABLE_STYLE_MEDIUM11 = 'TableStyleMedium11';
const TABLE_STYLE_MEDIUM12 = 'TableStyleMedium12';
const TABLE_STYLE_MEDIUM13 = 'TableStyleMedium13';
const TABLE_STYLE_MEDIUM14 = 'TableStyleMedium14';
const TABLE_STYLE_MEDIUM15 = 'TableStyleMedium15';
const TABLE_STYLE_MEDIUM16 = 'TableStyleMedium16';
const TABLE_STYLE_MEDIUM17 = 'TableStyleMedium17';
const TABLE_STYLE_MEDIUM18 = 'TableStyleMedium18';
const TABLE_STYLE_MEDIUM19 = 'TableStyleMedium19';
const TABLE_STYLE_MEDIUM20 = 'TableStyleMedium20';
const TABLE_STYLE_MEDIUM21 = 'TableStyleMedium21';
const TABLE_STYLE_MEDIUM22 = 'TableStyleMedium22';
const TABLE_STYLE_MEDIUM23 = 'TableStyleMedium23';
const TABLE_STYLE_MEDIUM24 = 'TableStyleMedium24';
const TABLE_STYLE_MEDIUM25 = 'TableStyleMedium25';
const TABLE_STYLE_MEDIUM26 = 'TableStyleMedium26';
const TABLE_STYLE_MEDIUM27 = 'TableStyleMedium27';
const TABLE_STYLE_MEDIUM28 = 'TableStyleMedium28';
const TABLE_STYLE_DARK1 = 'TableStyleDark1';
const TABLE_STYLE_DARK2 = 'TableStyleDark2';
const TABLE_STYLE_DARK3 = 'TableStyleDark3';
const TABLE_STYLE_DARK4 = 'TableStyleDark4';
const TABLE_STYLE_DARK5 = 'TableStyleDark5';
const TABLE_STYLE_DARK6 = 'TableStyleDark6';
const TABLE_STYLE_DARK7 = 'TableStyleDark7';
const TABLE_STYLE_DARK8 = 'TableStyleDark8';
const TABLE_STYLE_DARK9 = 'TableStyleDark9';
const TABLE_STYLE_DARK10 = 'TableStyleDark10';
const TABLE_STYLE_DARK11 = 'TableStyleDark11';
/**
* Theme.
*
* @var string
*/
private $theme;
/**
* Show First Column.
*
* @var bool
*/
private $showFirstColumn = false;
/**
* Show Last Column.
*
* @var bool
*/
private $showLastColumn = false;
/**
* Show Row Stripes.
*
* @var bool
*/
private $showRowStripes = false;
/**
* Show Column Stripes.
*
* @var bool
*/
private $showColumnStripes = false;
/**
* Table.
*
* @var null|Table
*/
private $table;
/**
* Create a new Table Style.
*
* @param string $theme (e.g. TableStyle::TABLE_STYLE_MEDIUM2)
*/
public function __construct(string $theme = self::TABLE_STYLE_MEDIUM2)
{
$this->theme = $theme;
}
/**
* Get theme.
*/
public function getTheme(): string
{
return $this->theme;
}
/**
* Set theme.
*/
public function setTheme(string $theme): self
{
$this->theme = $theme;
return $this;
}
/**
* Get show First Column.
*/
public function getShowFirstColumn(): bool
{
return $this->showFirstColumn;
}
/**
* Set show First Column.
*/
public function setShowFirstColumn(bool $showFirstColumn): self
{
$this->showFirstColumn = $showFirstColumn;
return $this;
}
/**
* Get show Last Column.
*/
public function getShowLastColumn(): bool
{
return $this->showLastColumn;
}
/**
* Set show Last Column.
*/
public function setShowLastColumn(bool $showLastColumn): self
{
$this->showLastColumn = $showLastColumn;
return $this;
}
/**
* Get show Row Stripes.
*/
public function getShowRowStripes(): bool
{
return $this->showRowStripes;
}
/**
* Set show Row Stripes.
*/
public function setShowRowStripes(bool $showRowStripes): self
{
$this->showRowStripes = $showRowStripes;
return $this;
}
/**
* Get show Column Stripes.
*/
public function getShowColumnStripes(): bool
{
return $this->showColumnStripes;
}
/**
* Set show Column Stripes.
*/
public function setShowColumnStripes(bool $showColumnStripes): self
{
$this->showColumnStripes = $showColumnStripes;
return $this;
}
/**
* Get this Style's Table.
*/
public function getTable(): ?Table
{
return $this->table;
}
/**
* Set this Style's Table.
*/
public function setTable(?Table $table = null): self
{
$this->table = $table;
return $this;
}
}

View File

@ -111,6 +111,13 @@ class Worksheet implements IComparable
*/
private $chartCollection;
/**
* Collection of Table objects.
*
* @var ArrayObject<int, Table>
*/
private $tableCollection;
/**
* Worksheet title.
*
@ -375,7 +382,10 @@ class Worksheet implements IComparable
$this->defaultRowDimension = new RowDimension(null);
// Default column dimension
$this->defaultColumnDimension = new ColumnDimension(null);
// AutoFilter
$this->autoFilter = new AutoFilter('', $this);
// Table collection
$this->tableCollection = new ArrayObject();
}
/**
@ -2095,6 +2105,58 @@ class Worksheet implements IComparable
return $this;
}
/**
* Get collection of Tables.
*
* @return ArrayObject<int, Table>
*/
public function getTableCollection()
{
return $this->tableCollection;
}
/**
* Add Table.
*
* @return $this
*/
public function addTable(Table $table): self
{
$table->setWorksheet($this);
$this->tableCollection[] = $table;
return $this;
}
/**
* Remove Table by name.
*
* @param string $name Table name
*
* @return $this
*/
public function removeTableByName(string $name): self
{
$name = Shared\StringHelper::strToUpper($name);
foreach ($this->tableCollection as $key => $table) {
if (Shared\StringHelper::strToUpper($table->getName()) === $name) {
unset($this->tableCollection[$key]);
}
}
return $this;
}
/**
* Remove collection of Tables.
*/
public function removeTableCollection(): self
{
$this->tableCollection = new ArrayObject();
return $this;
}
/**
* Get Freeze Pane.
*

View File

@ -25,6 +25,7 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsRibbon;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsVBA;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\StringTable;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Style;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Table;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Theme;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Workbook;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet;
@ -167,6 +168,11 @@ class Xlsx extends BaseWriter
*/
private $writerPartTheme;
/**
* @var Table
*/
private $writerPartTable;
/**
* @var Workbook
*/
@ -196,6 +202,7 @@ class Xlsx extends BaseWriter
$this->writerPartStringTable = new StringTable($this);
$this->writerPartStyle = new Style($this);
$this->writerPartTheme = new Theme($this);
$this->writerPartTable = new Table($this);
$this->writerPartWorkbook = new Workbook($this);
$this->writerPartWorksheet = new Worksheet($this);
@ -271,6 +278,11 @@ class Xlsx extends BaseWriter
return $this->writerPartTheme;
}
public function getWriterPartTable(): Table
{
return $this->writerPartTable;
}
public function getWriterPartWorkbook(): Workbook
{
return $this->writerPartWorkbook;
@ -389,10 +401,11 @@ class Xlsx extends BaseWriter
}
$chartRef1 = 0;
$tableRef1 = 1;
// Add worksheet relationships (drawings, ...)
for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) {
// Add relationships
$zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts);
$zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1);
// Add unparsedLoadedData
$sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName();
@ -478,6 +491,12 @@ class Xlsx extends BaseWriter
$zipContent['xl/media/' . $image->getIndexedFilename()] = file_get_contents($image->getPath());
}
}
// Add Table parts
$tables = $this->spreadSheet->getSheet($i)->getTableCollection();
foreach ($tables as $table) {
$zipContent['xl/tables/table' . $tableRef1 . '.xml'] = $this->getWriterPartTable()->writeTable($table, $tableRef1++);
}
}
// Add media

View File

@ -85,6 +85,16 @@ class ContentTypes extends WriterPart
// Shared strings
$this->writeOverrideContentType($objWriter, '/xl/sharedStrings.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml');
// Table
$table = 1;
for ($i = 0; $i < $sheetCount; ++$i) {
$tableCount = $spreadsheet->getSheet($i)->getTableCollection()->count();
for ($t = 1; $t <= $tableCount; ++$t) {
$this->writeOverrideContentType($objWriter, '/xl/tables/table' . $table++ . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml');
}
}
// Add worksheet relationship content types
$unparsedLoadedData = $spreadsheet->getUnparsedLoadedData();
$chart = 1;

View File

@ -163,10 +163,11 @@ class Rels extends WriterPart
*
* @param int $worksheetId
* @param bool $includeCharts Flag indicating if we should write charts
* @param int $tableRef Table ID
*
* @return string XML Output
*/
public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false)
public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1)
{
// Create XML writer
$objWriter = null;
@ -252,6 +253,17 @@ class Rels extends WriterPart
);
}
// Write Table
$tableCount = $worksheet->getTableCollection()->count();
for ($i = 1; $i <= $tableCount; ++$i) {
$this->writeRelationship(
$objWriter,
'_table_' . $i,
'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table',
'../tables/table' . $tableRef++ . '.xml'
);
}
// Write header/footer relationship?
$i = 1;
if (count($worksheet->getHeaderFooter()->getImages()) > 0) {

View File

@ -0,0 +1,111 @@
<?php
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
use PhpOffice\PhpSpreadsheet\Worksheet\Table as WorksheetTable;
class Table extends WriterPart
{
/**
* Write Table to XML format.
*
* @param int $tableRef Table ID
*
* @return string XML Output
*/
public function writeTable(WorksheetTable $table, $tableRef): string
{
// Create XML writer
$objWriter = null;
if ($this->getParentWriter()->getUseDiskCaching()) {
$objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
} else {
$objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
}
// XML header
$objWriter->startDocument('1.0', 'UTF-8', 'yes');
// Table
$name = 'Table' . $tableRef;
$range = $table->getRange();
$objWriter->startElement('table');
$objWriter->writeAttribute('xml:space', 'preserve');
$objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main');
$objWriter->writeAttribute('id', (string) $tableRef);
$objWriter->writeAttribute('name', $name);
$objWriter->writeAttribute('displayName', $table->getName() ?: $name);
$objWriter->writeAttribute('ref', $range);
$objWriter->writeAttribute('headerRowCount', $table->getShowHeaderRow() ? '1' : '0');
$objWriter->writeAttribute('totalsRowCount', $table->getShowTotalsRow() ? '1' : '0');
// Table Boundaries
[$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($table->getRange());
// Table Auto Filter
if ($table->getShowHeaderRow()) {
$objWriter->startElement('autoFilter');
$objWriter->writeAttribute('ref', $range);
foreach (range($rangeStart[0], $rangeEnd[0]) as $offset => $columnIndex) {
$column = $table->getColumnByOffset($offset);
if (!$column->getShowFilterButton()) {
$objWriter->startElement('filterColumn');
$objWriter->writeAttribute('colId', (string) $offset);
$objWriter->writeAttribute('hiddenButton', '1');
$objWriter->endElement();
}
}
$objWriter->endElement();
}
// Table Columns
$objWriter->startElement('tableColumns');
$objWriter->writeAttribute('count', (string) ($rangeEnd[0] - $rangeStart[0] + 1));
foreach (range($rangeStart[0], $rangeEnd[0]) as $offset => $columnIndex) {
$worksheet = $table->getWorksheet();
if (!$worksheet) {
continue;
}
$column = $table->getColumnByOffset($offset);
$cell = $worksheet->getCellByColumnAndRow($columnIndex, $rangeStart[1]);
$objWriter->startElement('tableColumn');
$objWriter->writeAttribute('id', (string) ($offset + 1));
$objWriter->writeAttribute('name', $table->getShowHeaderRow() ? $cell->getValue() : 'Column' . ($offset + 1));
if ($table->getShowTotalsRow()) {
if ($column->getTotalsRowLabel()) {
$objWriter->writeAttribute('totalsRowLabel', $column->getTotalsRowLabel());
}
if ($column->getTotalsRowFunction()) {
$objWriter->writeAttribute('totalsRowFunction', $column->getTotalsRowFunction());
}
}
if ($column->getColumnFormula()) {
$objWriter->writeElement('calculatedColumnFormula', $column->getColumnFormula());
}
$objWriter->endElement();
}
$objWriter->endElement();
// Table Styles
$objWriter->startElement('tableStyleInfo');
$objWriter->writeAttribute('name', $table->getStyle()->getTheme());
$objWriter->writeAttribute('showFirstColumn', $table->getStyle()->getShowFirstColumn() ? '1' : '0');
$objWriter->writeAttribute('showLastColumn', $table->getStyle()->getShowLastColumn() ? '1' : '0');
$objWriter->writeAttribute('showRowStripes', $table->getStyle()->getShowRowStripes() ? '1' : '0');
$objWriter->writeAttribute('showColumnStripes', $table->getStyle()->getShowColumnStripes() ? '1' : '0');
$objWriter->endElement();
$objWriter->endElement();
// Return
return $objWriter->getData();
}
}

View File

@ -120,6 +120,9 @@ class Worksheet extends WriterPart
// AlternateContent
$this->writeAlternateContent($objWriter, $worksheet);
// Table
$this->writeTable($objWriter, $worksheet);
// ConditionalFormattingRuleExtensionList
// (Must be inserted last. Not insert last, an Excel parse error will occur)
$this->writeExtLst($objWriter, $worksheet);
@ -993,6 +996,25 @@ class Worksheet extends WriterPart
}
}
/**
* Write Table.
*/
private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void
{
$tableCount = $worksheet->getTableCollection()->count();
$objWriter->startElement('tableParts');
$objWriter->writeAttribute('count', (string) $tableCount);
for ($t = 1; $t <= $tableCount; ++$t) {
$objWriter->startElement('tablePart');
$objWriter->writeAttribute('r:id', 'rId_table_' . $t);
$objWriter->endElement();
}
$objWriter->endElement();
}
/**
* Write PageSetup.
*/

View File

@ -0,0 +1,91 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\Column;
class ColumnTest extends SetupTeardown
{
protected function initTable(): Table
{
$sheet = $this->getSheet();
$sheet->getCell('G1')->setValue('Heading');
$sheet->getCell('G2')->setValue(2);
$sheet->getCell('G3')->setValue(3);
$sheet->getCell('G4')->setValue(4);
$sheet->getCell('H1')->setValue('Heading2');
$sheet->getCell('H2')->setValue(1);
$sheet->getCell('H3')->setValue(2);
$sheet->getCell('H4')->setValue(3);
$this->maxRow = $maxRow = 4;
$table = new Table();
$table->setRange("G1:H$maxRow");
return $table;
}
public function testVariousGets(): void
{
$table = $this->initTable();
$column = $table->getColumn('H');
$result = $column->getColumnIndex();
self::assertEquals('H', $result);
}
public function testGetBadColumnIndex(): void
{
$this->expectException(PhpSpreadsheetException::class);
$this->expectExceptionMessage('Column is outside of current table range.');
$table = $this->initTable();
$table->getColumn('B');
}
public function testSetColumnIndex(): void
{
$table = $this->initTable();
$column = $table->getColumn('H');
$column->setShowFilterButton(false);
$expectedResult = 'G';
$result = $column->setColumnIndex($expectedResult);
self::assertInstanceOf(Column::class, $result);
$result = $result->getColumnIndex();
self::assertEquals($expectedResult, $result);
}
public function testVariousSets(): void
{
$table = $this->initTable();
$column = $table->getColumn('H');
$result = $column->setShowFilterButton(false);
self::assertInstanceOf(Column::class, $result);
self::assertFalse($column->getShowFilterButton());
$label = 'Total';
$result = $column->setTotalsRowLabel($label);
self::assertInstanceOf(Column::class, $result);
self::assertEquals($label, $column->getTotalsRowLabel());
$function = 'sum';
$result = $column->setTotalsRowFunction($function);
self::assertInstanceOf(Column::class, $result);
self::assertEquals($function, $column->getTotalsRowFunction());
$formula = '=SUM(Sales_Data[[#This Row],[Q1]:[Q4]])';
$result = $column->setColumnFormula($formula);
self::assertInstanceOf(Column::class, $result);
self::assertEquals($formula, $column->getColumnFormula());
}
public function testTable(): void
{
$table = $this->initTable();
$column = new Column('H');
$column->setTable($table);
self::assertEquals($table, $column->getTable());
}
}

View File

@ -0,0 +1,38 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
class RemoveTableTest extends SetupTeardown
{
private const INITIAL_RANGE = 'H2:O256';
public function testRemoveTable(): void
{
$sheet = $this->getSheet();
$table = new Table(self::INITIAL_RANGE);
$table->setName('Table1');
$sheet->addTable($table);
self::assertEquals(1, $sheet->getTableCollection()->count());
$sheet->removeTableByName('table1'); // case insensitive
self::assertEquals(0, $sheet->getTableCollection()->count());
}
public function testRemoveCollection(): void
{
$sheet = $this->getSheet();
$table = new Table(self::INITIAL_RANGE);
$table->setName('Table1');
$sheet->addTable($table);
self::assertEquals(1, $sheet->getTableCollection()->count());
$sheet->removeTableCollection();
self::assertEquals(0, $sheet->getTableCollection()->count());
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use PHPUnit\Framework\TestCase;
class SetupTeardown extends TestCase
{
/**
* @var ?Spreadsheet
*/
private $spreadsheet;
/**
* @var ?Worksheet
*/
private $sheet;
/**
* @var int
*/
protected $maxRow = 4;
protected function tearDown(): void
{
$this->sheet = null;
if ($this->spreadsheet !== null) {
$this->spreadsheet->disconnectWorksheets();
$this->spreadsheet = null;
}
}
protected function getSpreadsheet(): Spreadsheet
{
if ($this->spreadsheet !== null) {
return $this->spreadsheet;
}
$this->spreadsheet = new Spreadsheet();
return $this->spreadsheet;
}
protected function getSheet(): Worksheet
{
if ($this->sheet !== null) {
return $this->sheet;
}
$this->sheet = $this->getSpreadsheet()->getActiveSheet();
return $this->sheet;
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle;
class TableStyleTest extends SetupTeardown
{
private const INITIAL_RANGE = 'H2:O256';
public function testVariousSets(): void
{
$table = new Table(self::INITIAL_RANGE);
$style = $table->getStyle();
$result = $style->setTheme(TableStyle::TABLE_STYLE_DARK1);
self::assertInstanceOf(TableStyle::class, $result);
self::assertEquals(TableStyle::TABLE_STYLE_DARK1, $style->getTheme());
$result = $style->setShowFirstColumn(true);
self::assertInstanceOf(TableStyle::class, $result);
self::assertTrue($style->getShowFirstColumn());
$result = $style->setShowLastColumn(true);
self::assertInstanceOf(TableStyle::class, $result);
self::assertTrue($style->getShowLastColumn());
$result = $style->setShowRowStripes(true);
self::assertInstanceOf(TableStyle::class, $result);
self::assertTrue($style->getShowRowStripes());
$result = $style->setShowColumnStripes(true);
self::assertInstanceOf(TableStyle::class, $result);
self::assertTrue($style->getShowColumnStripes());
}
public function testTable(): void
{
$table = new Table(self::INITIAL_RANGE);
$style = new TableStyle();
$style->setTable($table);
self::assertEquals($table, $style->getTable());
}
}

View File

@ -0,0 +1,558 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Cell\CellAddress;
use PhpOffice\PhpSpreadsheet\Cell\CellRange;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
use PhpOffice\PhpSpreadsheet\Worksheet\Table\Column;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
class TableTest extends SetupTeardown
{
private const INITIAL_RANGE = 'H2:O256';
public function testToString(): void
{
$expectedResult = self::INITIAL_RANGE;
$table = new Table(self::INITIAL_RANGE);
// magic __toString should return the active table range
$result = (string) $table;
self::assertEquals($expectedResult, $result);
}
/**
* @dataProvider validTableNamesProvider
*/
public function testValidTableNames(string $name, string $expected): void
{
$table = new Table(self::INITIAL_RANGE);
$result = $table->setName($name);
self::assertInstanceOf(Table::class, $result);
self::assertEquals($expected, $table->getName());
}
public function validTableNamesProvider(): array
{
return [
['', ''],
['Table_1', 'Table_1'],
['_table_2', '_table_2'],
['\table_3', '\table_3'],
[" Table_4 \n", 'Table_4'],
['table.5', 'table.5'],
['தமிழ்', 'தமிழ்'], // UTF-8 letters with combined character
];
}
/**
* @dataProvider invalidTableNamesProvider
*/
public function testInvalidTableNames(string $name): void
{
$table = new Table(self::INITIAL_RANGE);
$this->expectException(PhpSpreadsheetException::class);
$table->setName($name);
}
public function invalidTableNamesProvider(): array
{
return [
['C'],
['c'],
['R'],
['r'],
['Z100'],
['Z$100'],
['R1C1'],
['R1C'],
['R11C11'],
['123'],
['=Table'],
['ிக'], // starting with UTF-8 combined character
[bin2hex(random_bytes(255))], // random string with length greater than 255
];
}
public function testUniqueTableName(): void
{
$this->expectException(PhpSpreadsheetException::class);
$sheet = $this->getSheet();
$table1 = new Table();
$table1->setName('Table_1');
$sheet->addTable($table1);
$table2 = new Table();
$table2->setName('table_1'); // case insensitive
$sheet->addTable($table2);
}
public function testVariousSets(): void
{
$table = new Table(self::INITIAL_RANGE);
$result = $table->setShowHeaderRow(false);
self::assertInstanceOf(Table::class, $result);
self::assertFalse($table->getShowHeaderRow());
$result = $table->setShowTotalsRow(true);
self::assertInstanceOf(Table::class, $result);
self::assertTrue($table->getShowTotalsRow());
}
public function testGetWorksheet(): void
{
$sheet = $this->getSheet();
$table = new Table(self::INITIAL_RANGE);
$sheet->addTable($table);
$result = $table->getWorksheet();
self::assertSame($sheet, $result);
}
public function testSetWorksheet(): void
{
$table = new Table(self::INITIAL_RANGE);
$spreadsheet = $this->getSpreadsheet();
$sheet2 = $spreadsheet->createSheet();
// Setters return the instance to implement the fluent interface
$result = $table->setWorksheet($sheet2);
self::assertInstanceOf(Table::class, $result);
}
public function testGetRange(): void
{
$expectedResult = self::INITIAL_RANGE;
$table = new Table(self::INITIAL_RANGE);
// Result should be the active table range
$result = $table->getRange();
self::assertEquals($expectedResult, $result);
}
/**
* @dataProvider validTableRangeProvider
*
* @param AddressRange|array<int>|string $fullRange
* @param string $fullRange
*/
public function testSetRangeValidRange($fullRange, string $actualRange): void
{
$table = new Table(self::INITIAL_RANGE);
$result = $table->setRange($fullRange);
self::assertInstanceOf(Table::class, $result);
self::assertEquals($actualRange, $table->getRange());
}
public function validTableRangeProvider(): array
{
$sheet = $this->getSheet();
$title = $sheet->getTitle();
return [
["$title!G1:J512", 'G1:J512'],
['K1:N20', 'K1:N20'],
[[3, 5, 6, 8], 'C5:F8'],
[new CellRange(new CellAddress('C5', $sheet), new CellAddress('F8', $sheet)), 'C5:F8'],
];
}
public function testClearRange(): void
{
$expectedResult = '';
$table = new Table(self::INITIAL_RANGE);
// Setters return the instance to implement the fluent interface
$result = $table->setRange('');
self::assertInstanceOf(Table::class, $result);
// Result should be a clear range
$result = $table->getRange();
self::assertEquals($expectedResult, $result);
}
/**
* @dataProvider invalidTableRangeProvider
*/
public function testSetRangeInvalidRange(string $range): void
{
$this->expectException(PhpSpreadsheetException::class);
new Table($range);
}
public function invalidTableRangeProvider(): array
{
return [
['A1'],
['A1:A1'],
['B1:A4'],
['A1:D1'],
['D1:A1'],
];
}
public function testGetColumnsEmpty(): void
{
// There should be no columns yet defined
$table = new Table(self::INITIAL_RANGE);
$result = $table->getColumns();
self::assertIsArray($result);
self::assertCount(0, $result);
}
public function testGetColumnOffset(): void
{
$columnIndexes = [
'H' => 0,
'K' => 3,
'M' => 5,
];
$table = new Table(self::INITIAL_RANGE);
// If we request a specific column by its column ID, we should get an
// integer returned representing the column offset within the range
foreach ($columnIndexes as $columnIndex => $columnOffset) {
$result = $table->getColumnOffset($columnIndex);
self::assertEquals($columnOffset, $result);
}
}
public function testRemoveColumns(): void
{
$sheet = $this->getSheet();
$sheet->fromArray(range('H', 'O'), null, 'H2');
$table = new Table(self::INITIAL_RANGE);
$table->getColumn('L')->setShowFilterButton(false);
$sheet->addTable($table);
$sheet->removeColumn('K', 2);
$result = $table->getRange();
self::assertEquals('H2:M256', $result);
// Check that the prop that was set for column L is no longer set
self::assertTrue($table->getColumn('L')->getShowFilterButton());
}
public function testRemoveRows(): void
{
$sheet = $this->getSheet();
$sheet->fromArray(range('H', 'O'), null, 'H2');
$table = new Table(self::INITIAL_RANGE);
$sheet->addTable($table);
$sheet->removeRow(42, 128);
$result = $table->getRange();
self::assertEquals('H2:O128', $result);
}
public function testInsertColumns(): void
{
$sheet = $this->getSheet();
$sheet->fromArray(range('H', 'O'), null, 'H2');
$table = new Table(self::INITIAL_RANGE);
$table->getColumn('N')->setShowFilterButton(false);
$sheet->addTable($table);
$sheet->insertNewColumnBefore('N', 3);
$result = $table->getRange();
self::assertEquals('H2:R256', $result);
// Check that column N no longer has a prop
self::assertTrue($table->getColumn('N')->getShowFilterButton());
// Check that the prop originally set in column N has been moved to column Q
self::assertFalse($table->getColumn('Q')->getShowFilterButton());
}
public function testInsertRows(): void
{
$sheet = $this->getSheet();
$sheet->fromArray(range('H', 'O'), null, 'H2');
$table = new Table(self::INITIAL_RANGE);
$sheet->addTable($table);
$sheet->insertNewRowBefore(3, 4);
$result = $table->getRange();
self::assertEquals('H2:O260', $result);
}
public function testGetInvalidColumnOffset(): void
{
$this->expectException(PhpSpreadsheetException::class);
$invalidColumn = 'G';
$sheet = $this->getSheet();
$table = new Table();
$table->setWorksheet($sheet);
$table->getColumnOffset($invalidColumn);
}
public function testSetColumnWithString(): void
{
$expectedResult = 'L';
$table = new Table(self::INITIAL_RANGE);
// Setters return the instance to implement the fluent interface
$result = $table->setColumn($expectedResult);
self::assertInstanceOf(Table::class, $result);
$result = $table->getColumns();
// Result should be an array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table\Column
// objects for each column we set indexed by the column ID
self::assertIsArray($result);
self::assertCount(1, $result);
self::assertArrayHasKey($expectedResult, $result);
self::assertInstanceOf(Column::class, $result[$expectedResult]);
}
public function testSetInvalidColumnWithString(): void
{
$this->expectException(PhpSpreadsheetException::class);
$table = new Table(self::INITIAL_RANGE);
$invalidColumn = 'A';
$table->setColumn($invalidColumn);
}
public function testSetColumnWithColumnObject(): void
{
$expectedResult = 'M';
$columnObject = new Column($expectedResult);
$table = new Table(self::INITIAL_RANGE);
// Setters return the instance to implement the fluent interface
$result = $table->setColumn($columnObject);
self::assertInstanceOf(Table::class, $result);
$result = $table->getColumns();
// Result should be an array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table\Column
// objects for each column we set indexed by the column ID
self::assertIsArray($result);
self::assertCount(1, $result);
self::assertArrayHasKey($expectedResult, $result);
self::assertInstanceOf(Column::class, $result[$expectedResult]);
}
public function testSetInvalidColumnWithObject(): void
{
$this->expectException(PhpSpreadsheetException::class);
$invalidColumn = 'E';
$table = new Table(self::INITIAL_RANGE);
$table->setColumn($invalidColumn);
}
public function testSetColumnWithInvalidDataType(): void
{
$this->expectException(PhpSpreadsheetException::class);
$table = new Table(self::INITIAL_RANGE);
$invalidColumn = 123.456;
// @phpstan-ignore-next-line
$table->setColumn($invalidColumn);
}
public function testGetColumns(): void
{
$table = new Table(self::INITIAL_RANGE);
$columnIndexes = ['L', 'M'];
foreach ($columnIndexes as $columnIndex) {
$table->setColumn($columnIndex);
}
$result = $table->getColumns();
// Result should be an array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table\Column
// objects for each column we set indexed by the column ID
self::assertIsArray($result);
self::assertCount(count($columnIndexes), $result);
foreach ($columnIndexes as $columnIndex) {
self::assertArrayHasKey($columnIndex, $result);
self::assertInstanceOf(Column::class, $result[$columnIndex]);
}
$table->setRange('');
self::assertCount(0, $table->getColumns());
self::assertSame('', $table->getRange());
}
public function testGetColumn(): void
{
$table = new Table(self::INITIAL_RANGE);
$columnIndexes = ['L', 'M'];
foreach ($columnIndexes as $columnIndex) {
$table->setColumn($columnIndex);
}
// If we request a specific column by its column ID, we should
// get a \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table\Column object returned
foreach ($columnIndexes as $columnIndex) {
$result = $table->getColumn($columnIndex);
self::assertInstanceOf(Column::class, $result);
}
}
public function testGetColumnByOffset(): void
{
$table = new Table(self::INITIAL_RANGE);
$columnIndexes = [
0 => 'H',
3 => 'K',
5 => 'M',
];
// If we request a specific column by its offset, we should
// get a \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table\Column object returned
foreach ($columnIndexes as $columnIndex => $columnID) {
$result = $table->getColumnByOffset($columnIndex);
self::assertInstanceOf(Column::class, $result);
self::assertEquals($result->getColumnIndex(), $columnID);
}
}
public function testGetColumnIfNotSet(): void
{
$table = new Table(self::INITIAL_RANGE);
// If we request a specific column by its column ID, we should
// get a \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table\Column object returned
$result = $table->getColumn('K');
self::assertInstanceOf(Column::class, $result);
}
public function testGetColumnWithoutRangeSet(): void
{
$this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class);
$table = new Table(self::INITIAL_RANGE);
// Clear the range
$table->setRange('');
$table->getColumn('A');
}
public function testClearRangeWithExistingColumns(): void
{
$table = new Table(self::INITIAL_RANGE);
$expectedResult = '';
$columnIndexes = ['L', 'M', 'N'];
foreach ($columnIndexes as $columnIndex) {
$table->setColumn($columnIndex);
}
// Setters return the instance to implement the fluent interface
$result = $table->setRange('');
self::assertInstanceOf(Table::class, $result);
// Range should be cleared
$result = $table->getRange();
self::assertEquals($expectedResult, $result);
// Column array should be cleared
$result = $table->getColumns();
self::assertIsArray($result);
self::assertCount(0, $result);
}
public function testSetRangeWithExistingColumns(): void
{
$table = new Table(self::INITIAL_RANGE);
$expectedResult = 'G1:J512';
// These columns should be retained
$columnIndexes1 = ['I', 'J'];
foreach ($columnIndexes1 as $columnIndex) {
$table->setColumn($columnIndex);
}
// These columns should be discarded
$columnIndexes2 = ['K', 'L', 'M'];
foreach ($columnIndexes2 as $columnIndex) {
$table->setColumn($columnIndex);
}
// Setters return the instance to implement the fluent interface
$result = $table->setRange($expectedResult);
self::assertInstanceOf(Table::class, $result);
// Range should be correctly set
$result = $table->getRange();
self::assertEquals($expectedResult, $result);
// Only columns that existed in the original range and that
// still fall within the new range should be retained
$result = $table->getColumns();
self::assertIsArray($result);
self::assertCount(count($columnIndexes1), $result);
}
public function testClone(): void
{
$sheet = $this->getSheet();
$table = new Table(self::INITIAL_RANGE);
$sheet->addTable($table);
$columnIndexes = ['L', 'M'];
foreach ($columnIndexes as $columnIndex) {
$table->setColumn($columnIndex);
}
$result = clone $table;
self::assertInstanceOf(Table::class, $result);
self::assertSame($table->getRange(), $result->getRange());
self::assertNull($result->getWorksheet());
self::assertNotNull($table->getWorksheet());
self::assertInstanceOf(Worksheet::class, $table->getWorksheet());
$tableColumns = $table->getColumns();
$resultColumns = $result->getColumns();
self::assertIsArray($tableColumns);
self::assertIsArray($resultColumns);
self::assertCount(2, $tableColumns);
self::assertCount(2, $resultColumns);
self::assertArrayHasKey('L', $tableColumns);
self::assertArrayHasKey('L', $resultColumns);
self::assertArrayHasKey('M', $tableColumns);
self::assertArrayHasKey('M', $resultColumns);
self::assertInstanceOf(Column::class, $tableColumns['L']);
self::assertInstanceOf(Column::class, $resultColumns['L']);
self::assertInstanceOf(Column::class, $tableColumns['M']);
self::assertInstanceOf(Column::class, $resultColumns['M']);
}
public function testNoWorksheet(): void
{
$table = new Table();
self::assertNull($table->getWorksheet());
}
public function testClearColumn(): void
{
$table = new Table(self::INITIAL_RANGE);
$columnIndexes = ['J', 'K', 'L', 'M'];
foreach ($columnIndexes as $columnIndex) {
$table->setColumn($columnIndex);
}
$columns = $table->getColumns();
self::assertCount(4, $columns);
self::assertArrayHasKey('J', $columns);
self::assertArrayHasKey('K', $columns);
self::assertArrayHasKey('L', $columns);
self::assertArrayHasKey('M', $columns);
$table->clearColumn('K');
$columns = $table->getColumns();
self::assertCount(3, $columns);
self::assertArrayHasKey('J', $columns);
self::assertArrayHasKey('L', $columns);
self::assertArrayHasKey('M', $columns);
}
}