diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 74087f44..3989e2cf 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2395,11 +2395,6 @@ parameters: count: 2 path: src/PhpSpreadsheet/Reader/Xlsx.php - - - message: "#^Comparison operation \"\\>\" between SimpleXMLElement\\|null and 0 results in an error\\.$#" - count: 1 - path: src/PhpSpreadsheet/Reader/Xlsx.php - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Reader\\\\Xlsx\\:\\:boolean\\(\\) has no return type specified\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index a6e7fe03..166d9576 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -19,6 +19,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Properties as PropertyReader; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx\TableReader; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView; use PhpOffice\PhpSpreadsheet\ReferenceHelper; @@ -874,7 +875,8 @@ class Xlsx extends BaseReader } if ($this->readDataOnly === false) { - $this->readAutoFilterTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip); + $this->readAutoFilter($xmlSheet, $docSheet); + $this->readTables($xmlSheet, $docSheet, $dir, $fileWorksheet, $zip); } if ($xmlSheet && $xmlSheet->mergeCells && $xmlSheet->mergeCells->mergeCell && !$this->readDataOnly) { @@ -1983,23 +1985,28 @@ class Xlsx extends BaseReader } } - private function readAutoFilterTables( + private function readAutoFilter( + SimpleXMLElement $xmlSheet, + Worksheet $docSheet + ): void { + if ($xmlSheet && $xmlSheet->autoFilter) { + (new AutoFilter($docSheet, $xmlSheet))->load(); + } + } + + private function readTables( SimpleXMLElement $xmlSheet, Worksheet $docSheet, string $dir, string $fileWorksheet, ZipArchive $zip ): void { - if ($xmlSheet && $xmlSheet->autoFilter) { - // In older files, autofilter structure is defined in the worksheet file - (new AutoFilter($docSheet, $xmlSheet))->load(); - } elseif ($xmlSheet && $xmlSheet->tableParts && $xmlSheet->tableParts['count'] > 0) { - // But for Office365, MS decided to make it all just a bit more complicated - $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); + if ($xmlSheet && $xmlSheet->tableParts && (int) $xmlSheet->tableParts['count'] > 0) { + $this->readTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); } } - private function readAutoFilterTablesInTablesFile( + private function readTablesInTablesFile( SimpleXMLElement $xmlSheet, string $dir, string $fileWorksheet, @@ -2022,8 +2029,8 @@ class Xlsx extends BaseReader $relationshipFilePath = File::realpath($relationshipFilePath); if ($this->fileExistsInArchive($this->zip, $relationshipFilePath)) { - $autoFilter = $this->loadZip($relationshipFilePath); - (new AutoFilter($docSheet, $autoFilter))->load(); + $tableXml = $this->loadZip($relationshipFilePath); + (new TableReader($docSheet, $tableXml))->load(); } } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php new file mode 100644 index 00000000..e2c06da0 --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Xlsx/TableReader.php @@ -0,0 +1,105 @@ +worksheet = $workSheet; + $this->tableXml = $tableXml; + } + + /** + * Loads Table into the Worksheet. + */ + public function load(): void + { + // Remove all "$" in the table range + $tableRange = (string) preg_replace('/\$/', '', $this->tableXml['ref'] ?? ''); + if (strpos($tableRange, ':') !== false) { + $this->readTable($tableRange, $this->tableXml); + } + } + + /** + * Read Table from xml. + */ + private function readTable(string $tableRange, SimpleXMLElement $tableXml): void + { + $table = new Table($tableRange); + $table->setName((string) $tableXml['displayName']); + $table->setShowHeaderRow((string) $tableXml['headerRowCount'] !== '0'); + $table->setShowTotalsRow((string) $tableXml['totalsRowCount'] === '1'); + + $this->readTableAutoFilter($table, $tableXml->autoFilter); + $this->readTableColumns($table, $tableXml->tableColumns); + $this->readTableStyle($table, $tableXml->tableStyleInfo); + + $this->worksheet->addTable($table); + } + + /** + * Reads TableAutoFilter from xml. + */ + private function readTableAutoFilter(Table $table, SimpleXMLElement $autoFilterXml): void + { + foreach ($autoFilterXml->filterColumn as $filterColumn) { + $column = $table->getColumnByOffset((int) $filterColumn['colId']); + $column->setShowFilterButton((string) $filterColumn['hiddenButton'] !== '1'); + } + } + + /** + * Reads TableColumns from xml. + */ + private function readTableColumns(Table $table, SimpleXMLElement $tableColumnsXml): void + { + foreach ($tableColumnsXml->tableColumn as $tableColumn) { + $column = $table->getColumnByOffset((int) $tableColumn['id'] - 1); + + if ($table->getShowTotalsRow()) { + if ($tableColumn['totalsRowLabel']) { + $column->setTotalsRowLabel((string) $tableColumn['totalsRowLabel']); + } + + if ($tableColumn['totalsRowFunction']) { + $column->setTotalsRowFunction((string) $tableColumn['totalsRowFunction']); + } + } + + if ($tableColumn->calculatedColumnFormula) { + $column->setColumnFormula((string) $tableColumn->calculatedColumnFormula); + } + } + } + + /** + * Reads TableStyle from xml. + */ + private function readTableStyle(Table $table, SimpleXMLElement $tableStyleInfoXml): void + { + $tableStyle = new TableStyle(); + $tableStyle->setTheme((string) $tableStyleInfoXml['name']); + $tableStyle->setShowRowStripes((string) $tableStyleInfoXml['showRowStripes'] === '1'); + $tableStyle->setShowColumnStripes((string) $tableStyleInfoXml['showColumnStripes'] === '1'); + $tableStyle->setShowFirstColumn((string) $tableStyleInfoXml['showFirstColumn'] === '1'); + $tableStyle->setShowLastColumn((string) $tableStyleInfoXml['showLastColumn'] === '1'); + $table->setStyle($tableStyle); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/TableTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/TableTest.php new file mode 100644 index 00000000..73e8d111 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/TableTest.php @@ -0,0 +1,41 @@ +load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + + $tables = $worksheet->getTableCollection(); + self::assertCount(1, $tables); + + $table = $tables->offsetGet(0); + self::assertInstanceOf(Table::class, $table); + self::assertEquals('SalesData', $table->getName()); + self::assertEquals('A1:G16', $table->getRange()); + self::assertTrue($table->getShowHeaderRow(), 'ShowHeaderRow'); + self::assertTrue($table->getShowTotalsRow(), 'ShowTotalsRow'); + + self::assertEquals('Total', $table->getColumn('B')->getTotalsRowLabel()); + self::assertEquals('sum', $table->getColumn('G')->getTotalsRowFunction()); + self::assertEquals('SUM(SalesData[[#This Row],[Q1]:[Q4]])', $table->getColumn('G')->getColumnFormula()); + + $tableStyle = $table->getStyle(); + self::assertEquals(TableStyle::TABLE_STYLE_MEDIUM4, $tableStyle->getTheme()); + self::assertTrue($tableStyle->getShowRowStripes(), 'ShowRowStripes'); + self::assertFalse($tableStyle->getShowColumnStripes(), 'ShowColumnStripes'); + self::assertFalse($tableStyle->getShowFirstColumn(), 'ShowFirstColumn'); + self::assertTrue($tableStyle->getShowLastColumn(), 'ShowLastColumn'); + } +} diff --git a/tests/data/Reader/XLSX/tableTest.xlsx b/tests/data/Reader/XLSX/tableTest.xlsx new file mode 100644 index 00000000..c707c9e7 Binary files /dev/null and b/tests/data/Reader/XLSX/tableTest.xlsx differ