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 ea6a7b89ca..8e91ec8787 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 0000000000..405c07aed6 --- /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 0000000000..26113cb792 --- /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 0000000000..30d659f11e --- /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 0000000000..d9b87d8088 --- /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 0000000000..ccf13729c8 --- /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 362f20f08a..38e77abc4b 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 @@ public function __construct(?Spreadsheet $parent = null, $title = 'Worksheet') $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 @@ public function removeAutoFilter(): self 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 4f50607052..1558b25199 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -25,6 +25,7 @@ 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 @@ public function __construct(Spreadsheet $spreadsheet) $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 @@ public function getWriterPartTheme(): Theme return $this->writerPartTheme; } + public function getWriterPartTable(): Table + { + return $this->writerPartTable; + } + public function getWriterPartWorkbook(): Workbook { return $this->writerPartWorkbook; @@ -389,10 +401,11 @@ public function save($filename, int $flags = 0): void } $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 @@ public function save($filename, int $flags = 0): void $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 f62c14af70..acb85b5788 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -85,6 +85,16 @@ public function writeContentTypes(Spreadsheet $spreadsheet, $includeCharts = fal // 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 5aa878760a..238fb5bf6e 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -163,10 +163,11 @@ public function writeWorkbookRelationships(Spreadsheet $spreadsheet) * * @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 @@ public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\ ); } + // 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 0000000000..e7adfd3cbf --- /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 2cc35a28cb..eba4c9270d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -120,6 +120,9 @@ public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable // 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 @@ private function writeAutoFilter(XMLWriter $objWriter, PhpspreadsheetWorksheet $ } } + /** + * 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 0000000000..195d6e4144 --- /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 0000000000..76e914f7db --- /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 0000000000..e3cdaa7c3b --- /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 0000000000..c4365f7ade --- /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 405c07aed6..c8301680a6 100644 --- a/samples/Table/01_Table.php +++ b/samples/Table/01_Table.php @@ -53,7 +53,7 @@ // 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 30d659f11e..950fe74cd1 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -88,7 +88,29 @@ public function getName() */ 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 @@ public function getWorksheet() */ 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 c4365f7ade..106955a88f 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -22,15 +22,78 @@ public function testToString(): void 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 08423356c3..e696e0d8b9 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2057,6 +2057,24 @@ public function addTableByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row 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 0000000000..b33e6c7966 --- /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 665b2e182a..378a9a5afe 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -7,6 +7,7 @@ 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 @@ function ($coordinate) use ($allCoordinates) { // 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 @@ private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int } 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 950fe74cd1..04a34e6f1d 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -380,6 +380,36 @@ public function clearColumn($column) return $this; } + /** + * Shift an Table Column Rule to a different column. + * + * Note: This method bypasses validation of the destination column to ensure it is within this Table range. + * Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value. + * Use with caution. + * + * @param string $fromColumn Column name (e.g. A) + * @param string $toColumn Column name (e.g. B) + * + * @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 106955a88f..1b1edb83dc 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -208,6 +208,64 @@ public function testGetColumnOffset(): void } } + 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 0000000000..e8ae5d9994 --- /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 d9b87d8088..06284c67ff 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 @@ public function setTotalsRowFormula(string $totalsRowFormula) 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 e7adfd3cbf..be9f518361 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Table.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Table.php @@ -86,6 +86,10 @@ public function writeTable(WorksheetTable $table, $tableRef) $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 195d6e4144..c7edd86a0c 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/ColumnTest.php @@ -74,6 +74,11 @@ public function testVariousSets(): void $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 e696e0d8b9..971f2a9866 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2066,7 +2066,7 @@ public function addTableByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row */ 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 04a34e6f1d..cecd224a5a 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -103,10 +103,10 @@ public function setName(string $name) ) { 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 1b1edb83dc..c4e5f579c5 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -42,6 +42,8 @@ public function validTableNamesProvider(): array ['_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 @@ public function invalidTableNamesProvider(): array ['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 cecd224a5a..cfb8edfdc4 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -90,7 +90,7 @@ public function setName(string $name) { $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 @@ public function getWorksheet() */ 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 @@ public function __clone() } 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 cfb8edfdc4..ee664aa6be 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -272,7 +272,7 @@ public function getColumns() * * @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 @@ public function testColumnInRange($column) */ public function getColumnOffset($column) { - return $this->testColumnInRange($column); + return $this->isColumnInRange($column); } /** @@ -308,7 +308,7 @@ public function getColumnOffset($column) */ 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 @@ public function setColumn($columnObjectOrString) } 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 @@ public function setColumn($columnObjectOrString) */ 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 06284c67ff..385ba17170 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -89,7 +89,7 @@ public function setColumnIndex($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 ee664aa6be..0198dd2ef3 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -190,6 +190,11 @@ public function setRange(string $range): self 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 c4e5f579c5..9602f27dcb 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -173,14 +173,26 @@ public function testClearRange(): void 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 0198dd2ef3..b2d29ce7fe 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -73,20 +73,16 @@ public function __construct(string $range = '', ?Worksheet $worksheet = null) /** * 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 @@ public function setName(string $name) /** * 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 @@ public function setShowHeaderRow(bool $showHeaderRow) /** * 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 @@ public function setShowTotalsRow(bool $showTotalsRow) /** * 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 @@ public function setRange(string $range): self /** * Set Table Cell Range to max row. - * - * @return $this */ public function setRangeToMaxRow(): self { @@ -228,20 +210,16 @@ public function setRangeToMaxRow(): self /** * 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 @@ public function setWorksheet(?Worksheet $worksheet = null) * * @return Table\Column[] */ - public function getColumns() + public function getColumns(): array { return $this->columns; } @@ -277,7 +255,7 @@ public function getColumns() * * @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 @@ public function isColumnInRange($column) * * @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 @@ public function getColumnOffset($column) * 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 @@ public function getColumn($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) + public function getColumnByOffset($columnOffset): Table\Column { [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset); @@ -342,10 +316,8 @@ public function getColumnByOffset($columnOffset) * * @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 @@ public function setColumn($columnObjectOrString) * 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 @@ public function clearColumn($column) * * @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 @@ public function shiftColumn($fromColumn, $toColumn) /** * 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 385ba17170..a7c445f530 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/Column.php +++ b/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -69,10 +69,8 @@ public function __construct($column, ?Table $table = null) /** * Get Table column index as string eg: 'A'. - * - * @return string */ - public function getColumnIndex() + public function getColumnIndex(): string { return $this->columnIndex; } @@ -81,10 +79,8 @@ public function getColumnIndex() * 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 @@ public function setColumnIndex($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 @@ public function setShowFilterButton(bool $showFilterButton) /** * 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 @@ public function setTotalsRowLabel(string $totalsRowLabel) /** * 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 @@ public function setTotalsRowFunction(string $totalsRowFunction) /** * 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 @@ public function setTotalsRowFormula(string $totalsRowFormula) /** * 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 @@ public function setColumnFormula(string $columnFormula) /** * 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 ccf13729c8..78643c729e 100644 --- a/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php +++ b/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php @@ -122,20 +122,16 @@ public function __construct(string $theme = self::TABLE_STYLE_MEDIUM2) /** * 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 @@ public function setTheme(string $theme) /** * 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 @@ public function setShowFirstColumn(bool $showFirstColumn) /** * 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 @@ public function setShowLastColumn(bool $showLastColumn) /** * 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 @@ public function setShowRowStripes(bool $showRowStripes) /** * 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 @@ public function setShowColumnStripes(bool $showColumnStripes) /** * 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 be9f518361..67dbd19bd8 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 c8301680a6..476f187cc3 100644 --- a/samples/Table/01_Table.php +++ b/samples/Table/01_Table.php @@ -52,9 +52,7 @@ // 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 e8ae5d9994..b431af34fd 100644 --- a/samples/Table/03_Column_Formula.php +++ b/samples/Table/03_Column_Formula.php @@ -49,8 +49,7 @@ // 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 b2d29ce7fe..35966d6f9c 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 @@ public function setName(string $name): self { $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 2facf172b7..6f04d2baf9 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2212,7 +2212,11 @@ public function addTableByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row { $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 b33e6c7966..0495b11cdd 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php @@ -12,7 +12,7 @@ public function testRemoveTable(): void { $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 @@ public function testRemoveCollection(): void { $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 e3cdaa7c3b..830cc87b82 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 @@ public function testVariousSets(): void 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 9602f27dcb..af205196f7 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 @@ public function testToString(): void */ 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 @@ public function testValidTableNames(string $name, string $expected): void public function validTableNamesProvider(): array { return [ + ['', ''], ['Table_1', 'Table_1'], ['_table_2', '_table_2'], ['\table_3', '\table_3'], @@ -52,8 +51,7 @@ public function validTableNamesProvider(): array */ 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 @@ public function testUniqueTableName(): void 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 @@ public function testVariousSets(): void 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 @@ public function testSetWorksheet(): void 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 @@ public function testSetRange(): void { $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 @@ public function testSetRange(): void 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 @@ public function testSetRangeInvalidRange(string $range): void { $this->expectException(PhpSpreadsheetException::class); - $sheet = $this->getSheet(); - new Table($range, $sheet); + new Table($range); } public function invalidTableRangeProvider(): array @@ -198,8 +192,7 @@ public function invalidTableRangeProvider(): array 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 @@ public function testGetColumnOffset(): void '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 @@ public function testGetInvalidColumnOffset(): void 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 @@ public function testSetColumnWithString(): void 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 @@ public function testSetColumnWithColumnObject(): void { $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 @@ public function testSetInvalidColumnWithObject(): void $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 @@ public function testSetColumnWithInvalidDataType(): void { $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 @@ public function testSetColumnWithInvalidDataType(): void 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 @@ public function testGetColumns(): void 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 @@ public function testGetColumn(): void 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 @@ public function testGetColumnByOffset(): void 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 @@ public function testGetColumnIfNotSet(): void 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 @@ public function testGetColumnWithoutRangeSet(): void 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 @@ public function testClearRangeWithExistingColumns(): void 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 @@ public function testSetRangeWithExistingColumns(): void 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 @@ public function testNoWorksheet(): void 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 35966d6f9c..4ec4cbfca4 100644 --- a/src/PhpSpreadsheet/Worksheet/Table.php +++ b/src/PhpSpreadsheet/Worksheet/Table.php @@ -4,6 +4,7 @@ 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 @@ public function setWorksheet(?Worksheet $worksheet = null): self { if ($this->name !== '' && $worksheet !== null) { $spreadsheet = $worksheet->getParent(); + $tableName = StringHelper::strToUpper($this->name); foreach ($spreadsheet->getWorksheetIterator() as $sheet) { foreach ($sheet->getTableCollection() as $table) { - if ($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 80f51b4460..6631508351 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2158,8 +2158,9 @@ public function addTableByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row */ 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 0495b11cdd..fb2bb93979 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/RemoveTableTest.php @@ -18,7 +18,7 @@ public function testRemoveTable(): void 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 af205196f7..dd49aae1e1 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/Table/TableTest.php @@ -87,7 +87,7 @@ public function testUniqueTableName(): void $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 4ec4cbfca4..66839d4150 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 @@ public function getRange(): string /** * 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 6631508351..0baf6ecd24 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2128,27 +2128,6 @@ public function addTable(Table $table): self 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 dd49aae1e1..c7cbee25fe 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 @@ public function testGetRange(): void 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 { - $sheet = $this->getSheet(); - $title = $sheet->getTitle(); $table = new Table(self::INITIAL_RANGE); - $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 = $table->setRange($fullRange); + self::assertInstanceOf(Table::class, $result); + self::assertEquals($actualRange, $table->getRange()); + } + + public function validTableRangeProvider(): array + { + $sheet = $this->getSheet(); + $title = $sheet->getTitle(); - // Result should be the new table range - $result = $table->getRange(); - self::assertEquals($actualRange, $result); - } + return [ + ["$title!G1:J512", 'G1:J512'], + ['K1:N20', 'K1:N20'], + [[3, 5, 6, 8], 'C5:F8'], + [new CellRange(new CellAddress('C5', $sheet), new CellAddress('F8', $sheet)), 'C5:F8'], + ]; } public function testClearRange(): void