From 92a50d134f3fd765ec353d3398e43de2e178c68f Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Thu, 10 Mar 2022 19:54:45 +0530 Subject: [PATCH 01/14] Format as Table for Xlsx Initial implementation of Excel's tables feature (i.e. Select Home > Format as Table in Excel App). Tables are similar to AutoFilter but tables have other advantages like named ranges, easy formatting, totals row and header row with filter. Tables can also be converted to charts and pivot tables easily. Usage: $table = new Table(); $table->setName('Sales_Data'); $table->setRange('A1:D17'); $spreadsheet->getActiveSheet()->addTable($table); In this Commit: - Added Table API with initial support for header and totals row. - Added complete styling options for Table. - Added Xlsx Writer for Table. - Added samples. - Covered with unit tests. To be done: - Filter expressions similar to AutoFilter. - Precalucate formulas for totals row (Check sample 2). - Table named ranges in formulas and calculation. --- phpstan-baseline.neon | 2 +- samples/Table/01_Table.php | 77 ++++ samples/Table/02_Table_Total.php | 84 ++++ src/PhpSpreadsheet/Worksheet/Table.php | 407 ++++++++++++++++ src/PhpSpreadsheet/Worksheet/Table/Column.php | 202 ++++++++ .../Worksheet/Table/TableStyle.php | 254 ++++++++++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 60 +++ src/PhpSpreadsheet/Writer/Xlsx.php | 21 +- .../Writer/Xlsx/ContentTypes.php | 10 + src/PhpSpreadsheet/Writer/Xlsx/Rels.php | 14 +- src/PhpSpreadsheet/Writer/Xlsx/Table.php | 107 +++++ src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 22 + .../Worksheet/Table/ColumnTest.php | 86 ++++ .../Worksheet/Table/SetupTeardown.php | 54 +++ .../Worksheet/Table/TableStyleTest.php | 47 ++ .../Worksheet/Table/TableTest.php | 433 ++++++++++++++++++ 16 files changed, 1877 insertions(+), 3 deletions(-) create mode 100644 samples/Table/01_Table.php create mode 100644 samples/Table/02_Table_Total.php create mode 100644 src/PhpSpreadsheet/Worksheet/Table.php create mode 100644 src/PhpSpreadsheet/Worksheet/Table/Column.php create mode 100644 src/PhpSpreadsheet/Worksheet/Table/TableStyle.php create mode 100644 src/PhpSpreadsheet/Writer/Xlsx/Table.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/Table/SetupTeardown.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php create mode 100644 tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ea6a7b89..8e91ec87 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5527,7 +5527,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 - diff --git a/samples/Table/01_Table.php b/samples/Table/01_Table.php new file mode 100644 index 00000000..405c07ae --- /dev/null +++ b/samples/Table/01_Table.php @@ -0,0 +1,77 @@ +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(); +$table->setName('Sales Data'); +$table->setRange('A1:D17'); + +// 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']); diff --git a/samples/Table/02_Table_Total.php b/samples/Table/02_Table_Total.php new file mode 100644 index 00000000..26113cb7 --- /dev/null +++ b/samples/Table/02_Table_Total.php @@ -0,0 +1,84 @@ +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(); diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php new file mode 100644 index 00000000..30d659f1 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -0,0 +1,407 @@ +setRange($range); + $this->setWorksheet($worksheet); + $this->style = new TableStyle(); + } + + /** + * Get Table name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set Table name. + * + * @return $this + */ + public function setName(string $name) + { + $this->name = preg_replace('/\s+/', '_', trim($name)) ?? ''; + + return $this; + } + + /** + * Get show Header Row. + * + * @return bool + */ + public function getShowHeaderRow() + { + return $this->showHeaderRow; + } + + /** + * Set show Header Row. + * + * @return $this + */ + public function setShowHeaderRow(bool $showHeaderRow) + { + $this->showHeaderRow = $showHeaderRow; + + return $this; + } + + /** + * Get show Totals Row. + * + * @return bool + */ + public function getShowTotalsRow() + { + return $this->showTotalsRow; + } + + /** + * Set show Totals Row. + * + * @return $this + */ + public function setShowTotalsRow(bool $showTotalsRow) + { + $this->showTotalsRow = $showTotalsRow; + + return $this; + } + + /** + * Get Table Range. + * + * @return string + */ + public function getRange() + { + return $this->range; + } + + /** + * Set Table Cell Range. + * + * @return $this + */ + public function setRange(string $range): self + { + // extract coordinate + [$worksheet, $range] = Worksheet::extractSheetTitle($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.'); + } + + $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. + * + * @return $this + */ + 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. + * + * @return null|Worksheet + */ + public function getWorksheet() + { + return $this->workSheet; + } + + /** + * Set Table's Worksheet. + * + * @return $this + */ + public function setWorksheet(?Worksheet $worksheet = null) + { + $this->workSheet = $worksheet; + + return $this; + } + + /** + * Get all Table Columns. + * + * @return Table\Column[] + */ + public function getColumns() + { + 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 testColumnInRange($column) + { + 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) + { + return $this->testColumnInRange($column); + } + + /** + * Get a specified Table Column. + * + * @param string $column Column name (e.g. A) + * + * @return Table\Column + */ + public function getColumn($column) + { + $this->testColumnInRange($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) + * + * @return Table\Column + */ + public function getColumnByOffset($columnOffset) + { + [$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 + * + * @return $this + */ + public function setColumn($columnObjectOrString) + { + 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->testColumnInRange($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) + * + * @return $this + */ + public function clearColumn($column) + { + $this->testColumnInRange($column); + + if (isset($this->columns[$column])) { + unset($this->columns[$column]); + } + + return $this; + } + + /** + * Get table Style. + * + * @return TableStyle + */ + public function getStyle() + { + return $this->style; + } + + /** + * Set table Style. + * + * @return $this + */ + public function setStyle(TableStyle $style) + { + $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; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Table/Column.php b/src/PhpSpreadsheet/Worksheet/Table/Column.php new file mode 100644 index 00000000..d9b87d80 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -0,0 +1,202 @@ +columnIndex = $column; + $this->table = $table; + } + + /** + * Get Table column index as string eg: 'A'. + * + * @return string + */ + public function getColumnIndex() + { + return $this->columnIndex; + } + + /** + * Set Table column index as string eg: 'A'. + * + * @param string $column Column (e.g. A) + * + * @return $this + */ + public function setColumnIndex($column) + { + // Uppercase coordinate + $column = strtoupper($column); + if ($this->table !== null) { + $this->table->testColumnInRange($column); + } + + $this->columnIndex = $column; + + return $this; + } + + /** + * Get show Filter Button. + * + * @return bool + */ + public function getShowFilterButton() + { + return $this->showFilterButton; + } + + /** + * Set show Filter Button. + * + * @return $this + */ + public function setShowFilterButton(bool $showFilterButton) + { + $this->showFilterButton = $showFilterButton; + + return $this; + } + + /** + * Get total Row Label. + * + * @return string + */ + public function getTotalsRowLabel() + { + return $this->totalsRowLabel; + } + + /** + * Set total Row Label. + * + * @return $this + */ + public function setTotalsRowLabel(string $totalsRowLabel) + { + $this->totalsRowLabel = $totalsRowLabel; + + return $this; + } + + /** + * Get total Row Function. + * + * @return string + */ + public function getTotalsRowFunction() + { + return $this->totalsRowFunction; + } + + /** + * Set total Row Function. + * + * @return $this + */ + public function setTotalsRowFunction(string $totalsRowFunction) + { + $this->totalsRowFunction = $totalsRowFunction; + + return $this; + } + + /** + * Get total Row Formula. + * + * @return string + */ + public function getTotalsRowFormula() + { + return $this->totalsRowFormula; + } + + /** + * Set total Row Formula. + * + * @return $this + */ + public function setTotalsRowFormula(string $totalsRowFormula) + { + $this->totalsRowFormula = $totalsRowFormula; + + return $this; + } + + /** + * Get this Column's Table. + * + * @return null|Table + */ + public function getTable() + { + return $this->table; + } + + /** + * Set this Column's Table. + * + * @return $this + */ + public function setTable(?Table $table = null) + { + $this->table = $table; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php new file mode 100644 index 00000000..ccf13729 --- /dev/null +++ b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php @@ -0,0 +1,254 @@ +theme = $theme; + } + + /** + * Get theme. + * + * @return string + */ + public function getTheme() + { + return $this->theme; + } + + /** + * Set theme. + * + * @return $this + */ + public function setTheme(string $theme) + { + $this->theme = $theme; + + return $this; + } + + /** + * Get show First Column. + * + * @return bool + */ + public function getShowFirstColumn() + { + return $this->showFirstColumn; + } + + /** + * Set show First Column. + * + * @return $this + */ + public function setShowFirstColumn(bool $showFirstColumn) + { + $this->showFirstColumn = $showFirstColumn; + + return $this; + } + + /** + * Get show Last Column. + * + * @return bool + */ + public function getShowLastColumn() + { + return $this->showLastColumn; + } + + /** + * Set show Last Column. + * + * @return $this + */ + public function setShowLastColumn(bool $showLastColumn) + { + $this->showLastColumn = $showLastColumn; + + return $this; + } + + /** + * Get show Row Stripes. + * + * @return bool + */ + public function getShowRowStripes() + { + return $this->showRowStripes; + } + + /** + * Set show Row Stripes. + * + * @return $this + */ + public function setShowRowStripes(bool $showRowStripes) + { + $this->showRowStripes = $showRowStripes; + + return $this; + } + + /** + * Get show Column Stripes. + * + * @return bool + */ + public function getShowColumnStripes() + { + return $this->showColumnStripes; + } + + /** + * Set show Column Stripes. + * + * @return $this + */ + public function setShowColumnStripes(bool $showColumnStripes) + { + $this->showColumnStripes = $showColumnStripes; + + return $this; + } + + /** + * Get this Style's Table. + * + * @return null|Table + */ + public function getTable() + { + return $this->table; + } + + /** + * Set this Style's Table. + * + * @return $this + */ + public function setTable(?Table $table = null) + { + $this->table = $table; + + return $this; + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 362f20f0..38e77abc 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -107,6 +107,13 @@ class Worksheet implements IComparable */ private $chartCollection; + /** + * Collection of Table objects. + * + * @var ArrayObject + */ + private $tableCollection; + /** * Worksheet title. * @@ -371,7 +378,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(); } /** @@ -2005,6 +2015,56 @@ class Worksheet implements IComparable return $this; } + /** + * Get collection of Tables. + * + * @return ArrayObject + */ + public function getTableCollection() + { + return $this->tableCollection; + } + + /** + * Add Table. + * + * @return $this + */ + public function addTable(Table $table): self + { + $table->setWorksheet($this); + $this->tableCollection[] = $table; + + return $this; + } + + /** + * Add Table Range by using numeric cell coordinates. + * + * @param int $columnIndex1 Numeric column coordinate of the first cell + * @param int $row1 Numeric row coordinate of the first cell + * @param int $columnIndex2 Numeric column coordinate of the second cell + * @param int $row2 Numeric row coordinate of the second cell + * + * @return $this + */ + public function addTableByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2): self + { + $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + + return $this->addTable(new Table($cellRange, $this)); + } + + /** + * Remove collection of Tables. + */ + public function removeTableCollection(): self + { + $this->tableCollection = new ArrayObject(); + + return $this; + } + /** * Get Freeze Pane. * diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 4f506070..1558b251 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -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 diff --git a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php index f62c14af..acb85b57 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -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; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 5aa87876..238fb5bf 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -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) { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Table.php b/src/PhpSpreadsheet/Writer/Xlsx/Table.php new file mode 100644 index 00000000..e7adfd3c --- /dev/null +++ b/src/PhpSpreadsheet/Writer/Xlsx/Table.php @@ -0,0 +1,107 @@ +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()); + } + } + $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(); + } +} diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 2cc35a28..eba4c927 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -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. */ diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php new file mode 100644 index 00000000..195d6e41 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php @@ -0,0 +1,86 @@ +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()); + } + + public function testTable(): void + { + $table = $this->initTable(); + $column = new Column('H'); + $column->setTable($table); + self::assertEquals($table, $column->getTable()); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/SetupTeardown.php b/tests/PhpSpreadsheetTests/Worksheet/Table/SetupTeardown.php new file mode 100644 index 00000000..76e914f7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/SetupTeardown.php @@ -0,0 +1,54 @@ +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; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php new file mode 100644 index 00000000..e3cdaa7c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php @@ -0,0 +1,47 @@ +getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $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 + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $style = new TableStyle(); + $style->setTable($table); + self::assertEquals($table, $style->getTable()); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php new file mode 100644 index 00000000..c4365f7a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -0,0 +1,433 @@ +getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + // magic __toString should return the active table range + $result = (string) $table; + self::assertEquals($expectedResult, $result); + } + + public function testVariousSets(): void + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + $result = $table->setName('Table 1'); + self::assertInstanceOf(Table::class, $result); + // Spaces will be converted to underscore + self::assertEquals('Table_1', $table->getName()); + + $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); + $result = $table->getWorksheet(); + self::assertSame($sheet, $result); + } + + public function testSetWorksheet(): void + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $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; + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + // Result should be the active table range + $result = $table->getRange(); + self::assertEquals($expectedResult, $result); + } + + public function testSetRange(): void + { + $sheet = $this->getSheet(); + $title = $sheet->getTitle(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $ranges = [ + 'G1:J512' => "$title!G1:J512", + 'K1:N20' => 'K1:N20', + ]; + + foreach ($ranges as $actualRange => $fullRange) { + // Setters return the instance to implement the fluent interface + $result = $table->setRange($fullRange); + self::assertInstanceOf(Table::class, $result); + + // Result should be the new table range + $result = $table->getRange(); + self::assertEquals($actualRange, $result); + } + } + + public function testClearRange(): void + { + $expectedResult = ''; + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + // 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); + } + + public function testSetRangeInvalidRange(): void + { + $this->expectException(PhpSpreadsheetException::class); + + $expectedResult = 'A1'; + + $sheet = $this->getSheet(); + $table = new Table($expectedResult, $sheet); + } + + public function testGetColumnsEmpty(): void + { + // There should be no columns yet defined + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $result = $table->getColumns(); + self::assertIsArray($result); + self::assertCount(0, $result); + } + + public function testGetColumnOffset(): void + { + $columnIndexes = [ + 'H' => 0, + 'K' => 3, + 'M' => 5, + ]; + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + // 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 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'; + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + // 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); + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + $invalidColumn = 'A'; + $table->setColumn($invalidColumn); + } + + public function testSetColumnWithColumnObject(): void + { + $expectedResult = 'M'; + $columnObject = new Column($expectedResult); + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + // 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'; + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $table->setColumn($invalidColumn); + } + + public function testSetColumnWithInvalidDataType(): void + { + $this->expectException(PhpSpreadsheetException::class); + + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $invalidColumn = 123.456; + // @phpstan-ignore-next-line + $table->setColumn($invalidColumn); + } + + public function testGetColumns(): void + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + $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 + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + $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 + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + $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 + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + // 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); + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + // Clear the range + $table->setRange(''); + $table->getColumn('A'); + } + + public function testClearRangeWithExistingColumns(): void + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $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 + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $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); + $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 + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + $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); + } +} From 3c3d949a5d1899521a6b45aecc5dc721ff5524d5 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 3 Apr 2022 18:23:13 +0530 Subject: [PATCH 02/14] Added table name validation Validation added for - invalid characters - invalid names ("C", "c", "R", or "r") - cell references - space separate words - maxlength of 255 characters - unique table names across worksheet --- samples/Table/01_Table.php | 2 +- src/PhpSpreadsheet/Worksheet/Table.php | 36 +++++++++- .../Worksheet/Table/TableTest.php | 71 +++++++++++++++++-- 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/samples/Table/01_Table.php b/samples/Table/01_Table.php index 405c07ae..c8301680 100644 --- a/samples/Table/01_Table.php +++ b/samples/Table/01_Table.php @@ -53,7 +53,7 @@ $spreadsheet->getActiveSheet()->fromArray($dataArray, null, 'A2'); // Create Table $helper->log('Create Table'); $table = new Table(); -$table->setName('Sales Data'); +$table->setName('Sales_Data'); $table->setRange('A1:D17'); // Create Columns diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 30d659f1..950fe74c 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -88,7 +88,29 @@ class Table */ public function setName(string $name) { - $this->name = preg_replace('/\s+/', '_', trim($name)) ?? ''; + $name = trim($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('/^[A-Z_\\\\]/i', $name)) { + throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)'); + } + if (!preg_match('/^[A-Z_\\\\][A-Z0-9\._]+$/i', $name)) { + throw new PhpSpreadsheetException('The table name contains invalid characters'); + } + + $this->name = $name; return $this; } @@ -216,6 +238,18 @@ class Table */ public function setWorksheet(?Worksheet $worksheet = null) { + if ($this->name != '' && $worksheet != null) { + $spreadsheet = $worksheet->getParent(); + + foreach ($spreadsheet->getWorksheetIterator() as $sheet) { + foreach ($sheet->getTableCollection() as $table) { + if ($table->getName() == $this->name) { + throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'"); + } + } + } + } + $this->workSheet = $worksheet; return $this; diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index c4365f7a..106955a8 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -22,15 +22,78 @@ class TableTest extends SetupTeardown self::assertEquals($expectedResult, $result); } - public function testVariousSets(): void + /** + * @dataProvider validTableNamesProvider + */ + public function testValidTableNames(string $name, string $expected): void { $sheet = $this->getSheet(); $table = new Table(self::INITIAL_RANGE, $sheet); - $result = $table->setName('Table 1'); + $result = $table->setName($name); self::assertInstanceOf(Table::class, $result); - // Spaces will be converted to underscore - self::assertEquals('Table_1', $table->getName()); + 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'], + ]; + } + + /** + * @dataProvider invalidTableNamesProvider + */ + public function testInvalidTableNames(string $name): void + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); + + $this->expectException(PhpSpreadsheetException::class); + + $table->setName($name); + } + + public function invalidTableNamesProvider(): array + { + return [ + ['C'], + ['c'], + ['R'], + ['r'], + ['Z100'], + ['Z$100'], + ['R1C1'], + ['R1C'], + ['R11C11'], + ['123'], + ['=Table'], + [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'); + $sheet->addTable($table2); + } + + public function testVariousSets(): void + { + $sheet = $this->getSheet(); + $table = new Table(self::INITIAL_RANGE, $sheet); $result = $table->setShowHeaderRow(false); self::assertInstanceOf(Table::class, $result); From 50b91e8ede3f25a4b9277405df56a58211445087 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 3 Apr 2022 18:24:38 +0530 Subject: [PATCH 03/14] Remove table By name Option to remove the table from table collection of worksheet --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 18 +++++++++ .../Worksheet/Table/RemoveTableTest.php | 38 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 08423356..e696e0d8 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2057,6 +2057,24 @@ class Worksheet implements IComparable return $this->addTable(new Table($cellRange, $this)); } + /** + * Remove Table by name. + * + * @param string $name Table name + * + * @return $this + */ + public function removeTableByName(string $name): self + { + foreach($this->tableCollection as $key => $table) { + if ($table->getName() === $name) { + unset($this->tableCollection[$key]); + } + } + + return $this; + } + /** * Remove collection of Tables. */ diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php new file mode 100644 index 00000000..b33e6c79 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php @@ -0,0 +1,38 @@ +getSheet(); + + $table = new Table(self::INITIAL_RANGE, $sheet); + $table->setName('Table1'); + $sheet->addTable($table); + + self::assertEquals(1, $sheet->getTableCollection()->count()); + + $sheet->removeTableByName('Table1'); + self::assertEquals(0, $sheet->getTableCollection()->count()); + } + + public function testRemoveCollection(): void + { + $sheet = $this->getSheet(); + + $table = new Table(self::INITIAL_RANGE, $sheet); + $table->setName('Table1'); + $sheet->addTable($table); + + self::assertEquals(1, $sheet->getTableCollection()->count()); + + $sheet->removeTableCollection(); + self::assertEquals(0, $sheet->getTableCollection()->count()); + } +} From bc6ec1932a4681fa880a8e25e4901be001a57be1 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 3 Apr 2022 18:27:00 +0530 Subject: [PATCH 04/14] Auto adjust table range using ReferenceHelper Automatically adjusts table range on insertion and deletion of rows and columns within table range --- src/PhpSpreadsheet/ReferenceHelper.php | 83 +++++++++++++++++++ src/PhpSpreadsheet/Worksheet/Table.php | 30 +++++++ .../Worksheet/Table/TableTest.php | 58 +++++++++++++ 3 files changed, 171 insertions(+) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 665b2e18..378a9a5a 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -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); diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 950fe74c..04a34e6f 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -380,6 +380,36 @@ class Table 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) + * + * @return $this + */ + public function shiftColumn($fromColumn, $toColumn) + { + $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. * diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index 106955a8..1b1edb83 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -208,6 +208,64 @@ class TableTest extends SetupTeardown } } + 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); From feffb76944e631475f986785a1d320a146d5a3dd Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 3 Apr 2022 18:27:38 +0530 Subject: [PATCH 05/14] Added Column Formula Option to add column formula that applied automatically for any new rows added to the table range --- samples/Table/03_Column_Formula.php | 72 +++++++++++++++++++ src/PhpSpreadsheet/Worksheet/Table/Column.php | 29 ++++++++ src/PhpSpreadsheet/Writer/Xlsx/Table.php | 4 ++ .../Worksheet/Table/ColumnTest.php | 5 ++ 4 files changed, 110 insertions(+) create mode 100644 samples/Table/03_Column_Formula.php diff --git a/samples/Table/03_Column_Formula.php b/samples/Table/03_Column_Formula.php new file mode 100644 index 00000000..e8ae5d99 --- /dev/null +++ b/samples/Table/03_Column_Formula.php @@ -0,0 +1,72 @@ +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(); +$table->setName('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(); diff --git a/src/PhpSpreadsheet/Worksheet/Table/Column.php b/src/PhpSpreadsheet/Worksheet/Table/Column.php index d9b87d80..06284c67 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -41,6 +41,13 @@ class Column */ private $totalsRowFormula; + /** + * Column Formula. + * + * @var string + */ + private $columnFormula; + /** * Table. * @@ -178,6 +185,28 @@ class Column return $this; } + /** + * Get column Formula. + * + * @return string + */ + public function getColumnFormula() + { + return $this->columnFormula; + } + + /** + * Set column Formula. + * + * @return $this + */ + public function setColumnFormula(string $columnFormula) + { + $this->columnFormula = $columnFormula; + + return $this; + } + /** * Get this Column's Table. * diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Table.php b/src/PhpSpreadsheet/Writer/Xlsx/Table.php index e7adfd3c..be9f5183 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Table.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Table.php @@ -86,6 +86,10 @@ class Table extends WriterPart $objWriter->writeAttribute('totalsRowFunction', $column->getTotalsRowFunction()); } } + if ($column->getColumnFormula()) { + $objWriter->writeElement('calculatedColumnFormula', $column->getColumnFormula()); + } + $objWriter->endElement(); } $objWriter->endElement(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php index 195d6e41..c7edd86a 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php @@ -74,6 +74,11 @@ class ColumnTest extends SetupTeardown $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 From 4db82032b4c8afebb2cf1178f2dd4a4b69785ff8 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 3 Apr 2022 18:51:06 +0530 Subject: [PATCH 06/14] Remove table by name cs fix --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index e696e0d8..971f2a98 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2066,7 +2066,7 @@ class Worksheet implements IComparable */ public function removeTableByName(string $name): self { - foreach($this->tableCollection as $key => $table) { + foreach ($this->tableCollection as $key => $table) { if ($table->getName() === $name) { unset($this->tableCollection[$key]); } From 8889ecf0446f2e996de55a5247a32d58462ea1fd Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 17 Apr 2022 17:41:49 +0530 Subject: [PATCH 07/14] Support UTF-8 Table names Added support for UTF-8 Table names (including combined character) --- src/PhpSpreadsheet/Worksheet/Table.php | 4 ++-- tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 04a34e6f..cecd224a 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -103,10 +103,10 @@ class Table ) { throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference'); } - if (!preg_match('/^[A-Z_\\\\]/i', $name)) { + 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('/^[A-Z_\\\\][A-Z0-9\._]+$/i', $name)) { + if (!preg_match('/^[\p{L}_\\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) { throw new PhpSpreadsheetException('The table name contains invalid characters'); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index 1b1edb83..c4e5f579 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -42,6 +42,8 @@ class TableTest extends SetupTeardown ['_table_2', '_table_2'], ['\table_3', '\table_3'], [" Table_4 \n", 'Table_4'], + ['table.5', 'table.5'], + ['தமிழ்', 'தமிழ்'], // UTF-8 letters with combined character ]; } @@ -72,6 +74,7 @@ class TableTest extends SetupTeardown ['R11C11'], ['123'], ['=Table'], + ['ிக'], // starting with UTF-8 combined character [bin2hex(random_bytes(255))], // random string with length greater than 255 ]; } From 3a6ebc0ce6de43c6f37c6331177767e12245b4e5 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 17 Apr 2022 17:42:35 +0530 Subject: [PATCH 08/14] Fixed coding standard with strict comparisons --- src/PhpSpreadsheet/Worksheet/Table.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index cecd224a..cfb8edfd 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -90,7 +90,7 @@ class Table { $name = trim($name); - if (strlen($name) == 1 && in_array($name, ['C', 'c', 'R', 'r'])) { + if (strlen($name) === 1 && in_array($name, ['C', 'c', 'R', 'r'])) { throw new PhpSpreadsheetException('The table name is invalid'); } if (strlen($name) > 255) { @@ -238,12 +238,12 @@ class Table */ public function setWorksheet(?Worksheet $worksheet = null) { - if ($this->name != '' && $worksheet != null) { + if ($this->name !== '' && $worksheet !== null) { $spreadsheet = $worksheet->getParent(); foreach ($spreadsheet->getWorksheetIterator() as $sheet) { foreach ($sheet->getTableCollection() as $table) { - if ($table->getName() == $this->name) { + if ($table->getName() === $this->name) { throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'"); } } @@ -446,7 +446,7 @@ class Table } else { $this->{$key} = clone $value; } - } elseif ((is_array($value)) && ($key == 'columns')) { + } elseif ((is_array($value)) && ($key === 'columns')) { // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table objects $this->{$key} = []; foreach ($value as $k => $v) { From 3c4a51acb5b50a2d04817db03d063d34b8a5302f Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 17 Apr 2022 17:43:32 +0530 Subject: [PATCH 09/14] Minor refactoring work testColumnInRange method renamed to isColumnInRange --- src/PhpSpreadsheet/Worksheet/Table.php | 10 +++++----- src/PhpSpreadsheet/Worksheet/Table/Column.php | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index cfb8edfd..ee664aa6 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -272,7 +272,7 @@ class Table * * @return int The column offset within the table range */ - public function testColumnInRange($column) + public function isColumnInRange($column) { if (empty($this->range)) { throw new PhpSpreadsheetException('No table range is defined.'); @@ -296,7 +296,7 @@ class Table */ public function getColumnOffset($column) { - return $this->testColumnInRange($column); + return $this->isColumnInRange($column); } /** @@ -308,7 +308,7 @@ class Table */ public function getColumn($column) { - $this->testColumnInRange($column); + $this->isColumnInRange($column); if (!isset($this->columns[$column])) { $this->columns[$column] = new Table\Column($column, $this); @@ -349,7 +349,7 @@ class Table } else { throw new PhpSpreadsheetException('Column is not within the table range.'); } - $this->testColumnInRange($column); + $this->isColumnInRange($column); if (is_string($columnObjectOrString)) { $this->columns[$columnObjectOrString] = new Table\Column($columnObjectOrString, $this); @@ -371,7 +371,7 @@ class Table */ public function clearColumn($column) { - $this->testColumnInRange($column); + $this->isColumnInRange($column); if (isset($this->columns[$column])) { unset($this->columns[$column]); diff --git a/src/PhpSpreadsheet/Worksheet/Table/Column.php b/src/PhpSpreadsheet/Worksheet/Table/Column.php index 06284c67..385ba171 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -89,7 +89,7 @@ class Column // Uppercase coordinate $column = strtoupper($column); if ($this->table !== null) { - $this->table->testColumnInRange($column); + $this->table->isColumnInRange($column); } $this->columnIndex = $column; From ea3263650bb10d8646df91a847ac536376de6e46 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 17 Apr 2022 17:45:17 +0530 Subject: [PATCH 10/14] Minimum Table range validation Range must be at least 1 column and 2 rows --- src/PhpSpreadsheet/Worksheet/Table.php | 5 +++++ .../Worksheet/Table/TableTest.php | 20 +++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index ee664aa6..0198dd2e 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -190,6 +190,11 @@ class Table 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); diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index c4e5f579..9602f27d 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -173,14 +173,26 @@ class TableTest extends SetupTeardown self::assertEquals($expectedResult, $result); } - public function testSetRangeInvalidRange(): void + /** + * @dataProvider invalidTableRangeProvider + */ + public function testSetRangeInvalidRange(string $range): void { $this->expectException(PhpSpreadsheetException::class); - $expectedResult = 'A1'; - $sheet = $this->getSheet(); - $table = new Table($expectedResult, $sheet); + new Table($range, $sheet); + } + + public function invalidTableRangeProvider(): array + { + return [ + ['A1'], + ['A1:A1'], + ['B1:A4'], + ['A1:D1'], + ['D1:A1'], + ]; } public function testGetColumnsEmpty(): void From 44d63f027aedc865342e1f87e30234c91a5df359 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 17 Apr 2022 17:46:59 +0530 Subject: [PATCH 11/14] Fixed coding standard with return typehints --- src/PhpSpreadsheet/Worksheet/Table.php | 74 +++++-------------- src/PhpSpreadsheet/Worksheet/Table/Column.php | 56 ++++---------- .../Worksheet/Table/TableStyle.php | 48 +++--------- src/PhpSpreadsheet/Writer/Xlsx/Table.php | 2 +- 4 files changed, 46 insertions(+), 134 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 0198dd2e..b2d29ce7 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -73,20 +73,16 @@ class Table /** * Get Table name. - * - * @return string */ - public function getName() + public function getName(): string { return $this->name; } /** * Set Table name. - * - * @return $this */ - public function setName(string $name) + public function setName(string $name): self { $name = trim($name); @@ -117,20 +113,16 @@ class Table /** * Get show Header Row. - * - * @return bool */ - public function getShowHeaderRow() + public function getShowHeaderRow(): bool { return $this->showHeaderRow; } /** * Set show Header Row. - * - * @return $this */ - public function setShowHeaderRow(bool $showHeaderRow) + public function setShowHeaderRow(bool $showHeaderRow): self { $this->showHeaderRow = $showHeaderRow; @@ -139,20 +131,16 @@ class Table /** * Get show Totals Row. - * - * @return bool */ - public function getShowTotalsRow() + public function getShowTotalsRow(): bool { return $this->showTotalsRow; } /** * Set show Totals Row. - * - * @return $this */ - public function setShowTotalsRow(bool $showTotalsRow) + public function setShowTotalsRow(bool $showTotalsRow): self { $this->showTotalsRow = $showTotalsRow; @@ -161,18 +149,14 @@ class Table /** * Get Table Range. - * - * @return string */ - public function getRange() + public function getRange(): string { return $this->range; } /** * Set Table Cell Range. - * - * @return $this */ public function setRange(string $range): self { @@ -210,8 +194,6 @@ class Table /** * Set Table Cell Range to max row. - * - * @return $this */ public function setRangeToMaxRow(): self { @@ -228,20 +210,16 @@ class Table /** * Get Table's Worksheet. - * - * @return null|Worksheet */ - public function getWorksheet() + public function getWorksheet(): ?Worksheet { return $this->workSheet; } /** * Set Table's Worksheet. - * - * @return $this */ - public function setWorksheet(?Worksheet $worksheet = null) + public function setWorksheet(?Worksheet $worksheet = null): self { if ($this->name !== '' && $worksheet !== null) { $spreadsheet = $worksheet->getParent(); @@ -265,7 +243,7 @@ class Table * * @return Table\Column[] */ - public function getColumns() + public function getColumns(): array { return $this->columns; } @@ -277,7 +255,7 @@ class Table * * @return int The column offset within the table range */ - public function isColumnInRange($column) + public function isColumnInRange(string $column): int { if (empty($this->range)) { throw new PhpSpreadsheetException('No table range is defined.'); @@ -299,7 +277,7 @@ class Table * * @return int The offset of the specified column within the table range */ - public function getColumnOffset($column) + public function getColumnOffset($column): int { return $this->isColumnInRange($column); } @@ -308,10 +286,8 @@ class Table * Get a specified Table Column. * * @param string $column Column name (e.g. A) - * - * @return Table\Column */ - public function getColumn($column) + public function getColumn($column): Table\Column { $this->isColumnInRange($column); @@ -326,10 +302,8 @@ class Table * Get a specified Table Column by it's offset. * * @param int $columnOffset Column offset within range (starting from 0) - * - * @return Table\Column */ - public function getColumnByOffset($columnOffset) + public function getColumnByOffset($columnOffset): Table\Column { [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset); @@ -342,10 +316,8 @@ class Table * * @param string|Table\Column $columnObjectOrString * A simple string containing a Column ID like 'A' is permitted - * - * @return $this */ - public function setColumn($columnObjectOrString) + public function setColumn($columnObjectOrString): self { if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) { $column = $columnObjectOrString; @@ -371,10 +343,8 @@ class Table * Clear a specified Table Column. * * @param string $column Column name (e.g. A) - * - * @return $this */ - public function clearColumn($column) + public function clearColumn($column): self { $this->isColumnInRange($column); @@ -394,10 +364,8 @@ class Table * * @param string $fromColumn Column name (e.g. A) * @param string $toColumn Column name (e.g. B) - * - * @return $this */ - public function shiftColumn($fromColumn, $toColumn) + public function shiftColumn($fromColumn, $toColumn): self { $fromColumn = strtoupper($fromColumn); $toColumn = strtoupper($toColumn); @@ -417,20 +385,16 @@ class Table /** * Get table Style. - * - * @return TableStyle */ - public function getStyle() + public function getStyle(): Table\TableStyle { return $this->style; } /** * Set table Style. - * - * @return $this */ - public function setStyle(TableStyle $style) + public function setStyle(TableStyle $style): self { $this->style = $style; diff --git a/src/PhpSpreadsheet/Worksheet/Table/Column.php b/src/PhpSpreadsheet/Worksheet/Table/Column.php index 385ba171..a7c445f5 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -69,10 +69,8 @@ class Column /** * Get Table column index as string eg: 'A'. - * - * @return string */ - public function getColumnIndex() + public function getColumnIndex(): string { return $this->columnIndex; } @@ -81,10 +79,8 @@ class Column * Set Table column index as string eg: 'A'. * * @param string $column Column (e.g. A) - * - * @return $this */ - public function setColumnIndex($column) + public function setColumnIndex($column): self { // Uppercase coordinate $column = strtoupper($column); @@ -99,20 +95,16 @@ class Column /** * Get show Filter Button. - * - * @return bool */ - public function getShowFilterButton() + public function getShowFilterButton(): bool { return $this->showFilterButton; } /** * Set show Filter Button. - * - * @return $this */ - public function setShowFilterButton(bool $showFilterButton) + public function setShowFilterButton(bool $showFilterButton): self { $this->showFilterButton = $showFilterButton; @@ -121,20 +113,16 @@ class Column /** * Get total Row Label. - * - * @return string */ - public function getTotalsRowLabel() + public function getTotalsRowLabel(): ?string { return $this->totalsRowLabel; } /** * Set total Row Label. - * - * @return $this */ - public function setTotalsRowLabel(string $totalsRowLabel) + public function setTotalsRowLabel(string $totalsRowLabel): self { $this->totalsRowLabel = $totalsRowLabel; @@ -143,20 +131,16 @@ class Column /** * Get total Row Function. - * - * @return string */ - public function getTotalsRowFunction() + public function getTotalsRowFunction(): ?string { return $this->totalsRowFunction; } /** * Set total Row Function. - * - * @return $this */ - public function setTotalsRowFunction(string $totalsRowFunction) + public function setTotalsRowFunction(string $totalsRowFunction): self { $this->totalsRowFunction = $totalsRowFunction; @@ -165,20 +149,16 @@ class Column /** * Get total Row Formula. - * - * @return string */ - public function getTotalsRowFormula() + public function getTotalsRowFormula(): ?string { return $this->totalsRowFormula; } /** * Set total Row Formula. - * - * @return $this */ - public function setTotalsRowFormula(string $totalsRowFormula) + public function setTotalsRowFormula(string $totalsRowFormula): self { $this->totalsRowFormula = $totalsRowFormula; @@ -187,20 +167,16 @@ class Column /** * Get column Formula. - * - * @return string */ - public function getColumnFormula() + public function getColumnFormula(): ?string { return $this->columnFormula; } /** * Set column Formula. - * - * @return $this */ - public function setColumnFormula(string $columnFormula) + public function setColumnFormula(string $columnFormula): self { $this->columnFormula = $columnFormula; @@ -209,20 +185,16 @@ class Column /** * Get this Column's Table. - * - * @return null|Table */ - public function getTable() + public function getTable(): ?Table { return $this->table; } /** * Set this Column's Table. - * - * @return $this */ - public function setTable(?Table $table = null) + public function setTable(?Table $table = null): self { $this->table = $table; diff --git a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php index ccf13729..78643c72 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php +++ b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php @@ -122,20 +122,16 @@ class TableStyle /** * Get theme. - * - * @return string */ - public function getTheme() + public function getTheme(): string { return $this->theme; } /** * Set theme. - * - * @return $this */ - public function setTheme(string $theme) + public function setTheme(string $theme): self { $this->theme = $theme; @@ -144,20 +140,16 @@ class TableStyle /** * Get show First Column. - * - * @return bool */ - public function getShowFirstColumn() + public function getShowFirstColumn(): bool { return $this->showFirstColumn; } /** * Set show First Column. - * - * @return $this */ - public function setShowFirstColumn(bool $showFirstColumn) + public function setShowFirstColumn(bool $showFirstColumn): self { $this->showFirstColumn = $showFirstColumn; @@ -166,20 +158,16 @@ class TableStyle /** * Get show Last Column. - * - * @return bool */ - public function getShowLastColumn() + public function getShowLastColumn(): bool { return $this->showLastColumn; } /** * Set show Last Column. - * - * @return $this */ - public function setShowLastColumn(bool $showLastColumn) + public function setShowLastColumn(bool $showLastColumn): self { $this->showLastColumn = $showLastColumn; @@ -188,20 +176,16 @@ class TableStyle /** * Get show Row Stripes. - * - * @return bool */ - public function getShowRowStripes() + public function getShowRowStripes(): bool { return $this->showRowStripes; } /** * Set show Row Stripes. - * - * @return $this */ - public function setShowRowStripes(bool $showRowStripes) + public function setShowRowStripes(bool $showRowStripes): self { $this->showRowStripes = $showRowStripes; @@ -210,20 +194,16 @@ class TableStyle /** * Get show Column Stripes. - * - * @return bool */ - public function getShowColumnStripes() + public function getShowColumnStripes(): bool { return $this->showColumnStripes; } /** * Set show Column Stripes. - * - * @return $this */ - public function setShowColumnStripes(bool $showColumnStripes) + public function setShowColumnStripes(bool $showColumnStripes): self { $this->showColumnStripes = $showColumnStripes; @@ -232,20 +212,16 @@ class TableStyle /** * Get this Style's Table. - * - * @return null|Table */ - public function getTable() + public function getTable(): ?Table { return $this->table; } /** * Set this Style's Table. - * - * @return $this */ - public function setTable(?Table $table = null) + public function setTable(?Table $table = null): self { $this->table = $table; diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Table.php b/src/PhpSpreadsheet/Writer/Xlsx/Table.php index be9f5183..67dbd19b 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Table.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Table.php @@ -15,7 +15,7 @@ class Table extends WriterPart * * @return string XML Output */ - public function writeTable(WorksheetTable $table, $tableRef) + public function writeTable(WorksheetTable $table, $tableRef): string { // Create XML writer $objWriter = null; From 530e6642bf26522176504fccc81385d7358936d2 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sun, 17 Apr 2022 17:48:38 +0530 Subject: [PATCH 12/14] Table name as an constructor argument Replaced worksheet argument with table name --- samples/Table/01_Table.php | 4 +- samples/Table/03_Column_Formula.php | 3 +- src/PhpSpreadsheet/Worksheet/Table.php | 43 +++++----- src/PhpSpreadsheet/Worksheet/Worksheet.php | 6 +- .../Worksheet/Table/RemoveTableTest.php | 4 +- .../Worksheet/Table/TableStyleTest.php | 6 +- .../Worksheet/Table/TableTest.php | 78 +++++++------------ 7 files changed, 63 insertions(+), 81 deletions(-) diff --git a/samples/Table/01_Table.php b/samples/Table/01_Table.php index c8301680..476f187c 100644 --- a/samples/Table/01_Table.php +++ b/samples/Table/01_Table.php @@ -52,9 +52,7 @@ $spreadsheet->getActiveSheet()->fromArray($dataArray, null, 'A2'); // Create Table $helper->log('Create Table'); -$table = new Table(); -$table->setName('Sales_Data'); -$table->setRange('A1:D17'); +$table = new Table('A1:D17', 'Sales_Data'); // Create Columns $table->getColumn('D')->setShowFilterButton(false); diff --git a/samples/Table/03_Column_Formula.php b/samples/Table/03_Column_Formula.php index e8ae5d99..b431af34 100644 --- a/samples/Table/03_Column_Formula.php +++ b/samples/Table/03_Column_Formula.php @@ -49,8 +49,7 @@ $spreadsheet->getActiveSheet()->fromArray($dataArray, null, 'A1'); // Create Table $helper->log('Create Table'); -$table = new Table(); -$table->setName('Sales_Data'); +$table = new Table('A1:G15', 'Sales_Data'); $table->setRange('A1:G15'); // Set Column Formula diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index b2d29ce7..35966d6f 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -61,13 +61,14 @@ class Table * Create a new Table. * * @param string $range (e.g. A1:D4) + * @param string $name (e.g. Table1) * * @return $this */ - public function __construct(string $range = '', ?Worksheet $worksheet = null) + public function __construct(string $range = '', string $name = '') { $this->setRange($range); - $this->setWorksheet($worksheet); + $this->setName($name); $this->style = new TableStyle(); } @@ -86,24 +87,26 @@ class Table { $name = trim($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'); + 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; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 2facf172..6f04d2ba 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2212,7 +2212,11 @@ class Worksheet implements IComparable { $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; - return $this->addTable(new Table($cellRange, $this)); + $table = new Table($cellRange); + $table->setWorksheet($this); + $this->addTable($table); + + return $this; } /** diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php index b33e6c79..0495b11c 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php @@ -12,7 +12,7 @@ class RemoveTableTest extends SetupTeardown { $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $table->setName('Table1'); $sheet->addTable($table); @@ -26,7 +26,7 @@ class RemoveTableTest extends SetupTeardown { $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $table->setName('Table1'); $sheet->addTable($table); diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php index e3cdaa7c..830cc87b 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableStyleTest.php @@ -11,8 +11,7 @@ class TableStyleTest extends SetupTeardown public function testVariousSets(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $style = $table->getStyle(); $result = $style->setTheme(TableStyle::TABLE_STYLE_DARK1); @@ -38,8 +37,7 @@ class TableStyleTest extends SetupTeardown public function testTable(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $style = new TableStyle(); $style->setTable($table); self::assertEquals($table, $style->getTable()); diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index 9602f27d..af205196 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -14,8 +14,7 @@ class TableTest extends SetupTeardown public function testToString(): void { $expectedResult = self::INITIAL_RANGE; - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); // magic __toString should return the active table range $result = (string) $table; @@ -27,8 +26,7 @@ class TableTest extends SetupTeardown */ public function testValidTableNames(string $name, string $expected): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $result = $table->setName($name); self::assertInstanceOf(Table::class, $result); @@ -38,6 +36,7 @@ class TableTest extends SetupTeardown public function validTableNamesProvider(): array { return [ + ['', ''], ['Table_1', 'Table_1'], ['_table_2', '_table_2'], ['\table_3', '\table_3'], @@ -52,8 +51,7 @@ class TableTest extends SetupTeardown */ public function testInvalidTableNames(string $name): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $this->expectException(PhpSpreadsheetException::class); @@ -95,8 +93,7 @@ class TableTest extends SetupTeardown public function testVariousSets(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $result = $table->setShowHeaderRow(false); self::assertInstanceOf(Table::class, $result); @@ -110,15 +107,15 @@ class TableTest extends SetupTeardown public function testGetWorksheet(): void { $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); + $sheet->addTable($table); $result = $table->getWorksheet(); self::assertSame($sheet, $result); } public function testSetWorksheet(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $spreadsheet = $this->getSpreadsheet(); $sheet2 = $spreadsheet->createSheet(); // Setters return the instance to implement the fluent interface @@ -129,8 +126,7 @@ class TableTest extends SetupTeardown public function testGetRange(): void { $expectedResult = self::INITIAL_RANGE; - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); // Result should be the active table range $result = $table->getRange(); @@ -141,7 +137,7 @@ class TableTest extends SetupTeardown { $sheet = $this->getSheet(); $title = $sheet->getTitle(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $ranges = [ 'G1:J512' => "$title!G1:J512", 'K1:N20' => 'K1:N20', @@ -161,8 +157,7 @@ class TableTest extends SetupTeardown public function testClearRange(): void { $expectedResult = ''; - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); // Setters return the instance to implement the fluent interface $result = $table->setRange(''); @@ -180,8 +175,7 @@ class TableTest extends SetupTeardown { $this->expectException(PhpSpreadsheetException::class); - $sheet = $this->getSheet(); - new Table($range, $sheet); + new Table($range); } public function invalidTableRangeProvider(): array @@ -198,8 +192,7 @@ class TableTest extends SetupTeardown public function testGetColumnsEmpty(): void { // There should be no columns yet defined - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $result = $table->getColumns(); self::assertIsArray($result); self::assertCount(0, $result); @@ -212,8 +205,7 @@ class TableTest extends SetupTeardown 'K' => 3, 'M' => 5, ]; - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $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 @@ -296,8 +288,7 @@ class TableTest extends SetupTeardown public function testSetColumnWithString(): void { $expectedResult = 'L'; - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); // Setters return the instance to implement the fluent interface $result = $table->setColumn($expectedResult); @@ -315,8 +306,7 @@ class TableTest extends SetupTeardown public function testSetInvalidColumnWithString(): void { $this->expectException(PhpSpreadsheetException::class); - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $invalidColumn = 'A'; $table->setColumn($invalidColumn); @@ -326,8 +316,7 @@ class TableTest extends SetupTeardown { $expectedResult = 'M'; $columnObject = new Column($expectedResult); - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); // Setters return the instance to implement the fluent interface $result = $table->setColumn($columnObject); @@ -347,8 +336,7 @@ class TableTest extends SetupTeardown $this->expectException(PhpSpreadsheetException::class); $invalidColumn = 'E'; - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $table->setColumn($invalidColumn); } @@ -356,8 +344,7 @@ class TableTest extends SetupTeardown { $this->expectException(PhpSpreadsheetException::class); - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $invalidColumn = 123.456; // @phpstan-ignore-next-line $table->setColumn($invalidColumn); @@ -365,8 +352,7 @@ class TableTest extends SetupTeardown public function testGetColumns(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $columnIndexes = ['L', 'M']; @@ -391,8 +377,7 @@ class TableTest extends SetupTeardown public function testGetColumn(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $columnIndexes = ['L', 'M']; @@ -410,8 +395,7 @@ class TableTest extends SetupTeardown public function testGetColumnByOffset(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $columnIndexes = [ 0 => 'H', @@ -430,8 +414,7 @@ class TableTest extends SetupTeardown public function testGetColumnIfNotSet(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $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'); @@ -441,8 +424,7 @@ class TableTest extends SetupTeardown public function testGetColumnWithoutRangeSet(): void { $this->expectException(\PhpOffice\PhpSpreadsheet\Exception::class); - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); // Clear the range $table->setRange(''); @@ -451,8 +433,7 @@ class TableTest extends SetupTeardown public function testClearRangeWithExistingColumns(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $expectedResult = ''; $columnIndexes = ['L', 'M', 'N']; @@ -476,8 +457,7 @@ class TableTest extends SetupTeardown public function testSetRangeWithExistingColumns(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $expectedResult = 'G1:J512'; // These columns should be retained @@ -509,7 +489,8 @@ class TableTest extends SetupTeardown public function testClone(): void { $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); + $sheet->addTable($table); $columnIndexes = ['L', 'M']; foreach ($columnIndexes as $columnIndex) { @@ -546,8 +527,7 @@ class TableTest extends SetupTeardown public function testClearColumn(): void { - $sheet = $this->getSheet(); - $table = new Table(self::INITIAL_RANGE, $sheet); + $table = new Table(self::INITIAL_RANGE); $columnIndexes = ['J', 'K', 'L', 'M']; foreach ($columnIndexes as $columnIndex) { From d414f139f144bd3f387c7c5f223a8d024913c6ff Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sat, 23 Apr 2022 18:42:11 +0530 Subject: [PATCH 13/14] Table name is now case insensitive Table name comparison changed to UTF-8 aware and case insensitive --- src/PhpSpreadsheet/Worksheet/Table.php | 4 +++- src/PhpSpreadsheet/Worksheet/Worksheet.php | 3 ++- tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php | 2 +- tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 35966d6f..4ec4cbfc 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Worksheet\Table\TableStyle; class Table @@ -226,10 +227,11 @@ class Table { if ($this->name !== '' && $worksheet !== null) { $spreadsheet = $worksheet->getParent(); + $tableName = StringHelper::strToUpper($this->name); foreach ($spreadsheet->getWorksheetIterator() as $sheet) { foreach ($sheet->getTableCollection() as $table) { - if ($table->getName() === $this->name) { + if (StringHelper::strToUpper($table->getName()) === $tableName) { throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'"); } } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 80f51b44..66315083 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2158,8 +2158,9 @@ class Worksheet implements IComparable */ public function removeTableByName(string $name): self { + $name = Shared\StringHelper::strToUpper($name); foreach ($this->tableCollection as $key => $table) { - if ($table->getName() === $name) { + if (Shared\StringHelper::strToUpper($table->getName()) === $name) { unset($this->tableCollection[$key]); } } diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php index 0495b11c..fb2bb939 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php @@ -18,7 +18,7 @@ class RemoveTableTest extends SetupTeardown self::assertEquals(1, $sheet->getTableCollection()->count()); - $sheet->removeTableByName('Table1'); + $sheet->removeTableByName('table1'); // case insensitive self::assertEquals(0, $sheet->getTableCollection()->count()); } diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index af205196..dd49aae1 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -87,7 +87,7 @@ class TableTest extends SetupTeardown $sheet->addTable($table1); $table2 = new Table(); - $table2->setName('Table_1'); + $table2->setName('table_1'); // case insensitive $sheet->addTable($table2); } From 534cbc04c040079c26c8855670ba8ed94566c176 Mon Sep 17 00:00:00 2001 From: aswinkumar863 Date: Sat, 23 Apr 2022 18:43:38 +0530 Subject: [PATCH 14/14] Accept table range as AddressRange and array Table constructor now accepts AddressRange and array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] --- src/PhpSpreadsheet/Worksheet/Table.php | 21 +++++++--- src/PhpSpreadsheet/Worksheet/Worksheet.php | 21 ---------- .../Worksheet/Table/TableTest.php | 39 ++++++++++++------- 3 files changed, 39 insertions(+), 42 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Table.php b/src/PhpSpreadsheet/Worksheet/Table.php index 4ec4cbfc..66839d41 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -2,6 +2,7 @@ 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; @@ -61,12 +62,13 @@ class Table /** * Create a new Table. * - * @param string $range (e.g. A1:D4) + * @param AddressRange|array|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) - * - * @return $this */ - public function __construct(string $range = '', string $name = '') + public function __construct($range = '', string $name = '') { $this->setRange($range); $this->setName($name); @@ -161,11 +163,18 @@ class Table /** * Set Table Cell Range. + * + * @param AddressRange|array|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(string $range): self + public function setRange($range = ''): self { // extract coordinate - [$worksheet, $range] = Worksheet::extractSheetTitle($range, true); + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } if (empty($range)) { // Discard all column rules $this->columns = []; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 66315083..0baf6ecd 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2128,27 +2128,6 @@ class Worksheet implements IComparable return $this; } - /** - * Add Table Range by using numeric cell coordinates. - * - * @param int $columnIndex1 Numeric column coordinate of the first cell - * @param int $row1 Numeric row coordinate of the first cell - * @param int $columnIndex2 Numeric column coordinate of the second cell - * @param int $row2 Numeric row coordinate of the second cell - * - * @return $this - */ - public function addTableByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2): self - { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; - - $table = new Table($cellRange); - $table->setWorksheet($this); - $this->addTable($table); - - return $this; - } - /** * Remove Table by name. * diff --git a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php index dd49aae1..c7cbee25 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -2,6 +2,8 @@ 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; @@ -133,25 +135,32 @@ class TableTest extends SetupTeardown self::assertEquals($expectedResult, $result); } - public function testSetRange(): void + /** + * @dataProvider validTableRangeProvider + * + * @param AddressRange|array|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(); - $table = new Table(self::INITIAL_RANGE); - $ranges = [ - 'G1:J512' => "$title!G1:J512", - 'K1:N20' => 'K1:N20', + + 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'], ]; - - foreach ($ranges as $actualRange => $fullRange) { - // Setters return the instance to implement the fluent interface - $result = $table->setRange($fullRange); - self::assertInstanceOf(Table::class, $result); - - // Result should be the new table range - $result = $table->getRange(); - self::assertEquals($actualRange, $result); - } } public function testClearRange(): void