From 50fac315f65669c890633cee4e98a582ffe69e4c Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 31 Aug 2025 23:53:06 -0700 Subject: [PATCH 1/2] Option for Readers to create a new blank sheet if none match LoadSheetsOnly list. Backport of PR #4618. --- CHANGELOG.md | 10 +++ src/PhpSpreadsheet/Reader/BaseReader.php | 20 ++++++ src/PhpSpreadsheet/Reader/Gnumeric.php | 5 ++ src/PhpSpreadsheet/Reader/IReader.php | 9 +++ src/PhpSpreadsheet/Reader/Ods.php | 5 ++ src/PhpSpreadsheet/Reader/Xls.php | 5 ++ src/PhpSpreadsheet/Reader/Xlsx.php | 5 ++ src/PhpSpreadsheet/Reader/Xml.php | 5 ++ .../Reader/CreateBlankSheetIfNoneReadTest.php | 69 +++++++++++++++++++ 9 files changed, 133 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Reader/CreateBlankSheetIfNoneReadTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 23e61ad78e..030a43c472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). This is always true of the master branch. Some earlier branches, including the branch from which you are reading this file, remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version. +## 2025-09-03 - 2.4.1 + +### Added + +- Option for Readers to create a new blank sheet if none match LoadSheetsOnly list. Backport of PR #4618. + +### Fixed + + - Compatibility changes for Php 8.5. [Issue #4600](https://github.com/PHPOffice/PhpSpreadsheet/issues/4600) [PR #4614](https://github.com/PHPOffice/PhpSpreadsheet/pull/4614) [PR #4594](https://github.com/PHPOffice/PhpSpreadsheet/pull/4594) [PR #4588](https://github.com/PHPOffice/PhpSpreadsheet/pull/4588) + ## 2025-08-10 - 2.4.0 ### Breaking Changes diff --git a/src/PhpSpreadsheet/Reader/BaseReader.php b/src/PhpSpreadsheet/Reader/BaseReader.php index 3d83ec4b9c..9217f7d8ed 100644 --- a/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/src/PhpSpreadsheet/Reader/BaseReader.php @@ -53,6 +53,12 @@ abstract class BaseReader implements IReader */ protected bool $allowExternalImages = false; + /** + * Create a blank sheet if none are read, + * possibly due to a typo when using LoadSheetsOnly. + */ + protected bool $createBlankSheetIfNoneRead = false; + /** * IReadFilter instance. */ @@ -168,6 +174,17 @@ public function getAllowExternalImages(): bool return $this->allowExternalImages; } + /** + * Create a blank sheet if none are read, + * possibly due to a typo when using LoadSheetsOnly. + */ + public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self + { + $this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead; + + return $this; + } + public function getSecurityScanner(): ?XmlScanner { return $this->securityScanner; @@ -202,6 +219,9 @@ protected function processFlags(int $flags): void if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) { $this->setAllowExternalImages(false); } + if (((bool) ($flags & self::CREATE_BLANK_SHEET_IF_NONE_READ)) === true) { + $this->setCreateBlankSheetIfNoneRead(true); + } } protected function loadSpreadsheetFromFile(string $filename): Spreadsheet diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index e57a07f492..8ecde3ff86 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -253,6 +253,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp (new Properties($this->spreadsheet))->readProperties($xml, $gnmXML); $worksheetID = 0; + $sheetCreated = false; foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) { $sheet = self::testSimpleXml($sheetOrNull); $worksheetName = (string) $sheet->Name; @@ -264,6 +265,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp // Create new Worksheet $this->spreadsheet->createSheet(); + $sheetCreated = true; $this->spreadsheet->setActiveSheetIndex($worksheetID); // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet @@ -315,6 +317,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp $this->setSelectedCells($sheet); ++$worksheetID; } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $this->spreadsheet->createSheet(); + } $this->processDefinedNames($gnmXML); diff --git a/src/PhpSpreadsheet/Reader/IReader.php b/src/PhpSpreadsheet/Reader/IReader.php index 5fc62a5bb6..5b24f53adf 100644 --- a/src/PhpSpreadsheet/Reader/IReader.php +++ b/src/PhpSpreadsheet/Reader/IReader.php @@ -23,6 +23,8 @@ interface IReader public const ALLOW_EXTERNAL_IMAGES = 16; public const DONT_ALLOW_EXTERNAL_IMAGES = 32; + public const CREATE_BLANK_SHEET_IF_NONE_READ = 64; + public function __construct(); /** @@ -129,6 +131,12 @@ public function setAllowExternalImages(bool $allowExternalImages): self; public function getAllowExternalImages(): bool; + /** + * Create a blank sheet if none are read, + * possibly due to a typo when using LoadSheetsOnly. + */ + public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self; + /** * Loads PhpSpreadsheet from file. * @@ -141,6 +149,7 @@ public function getAllowExternalImages(): bool; * self::IGNORE_ROWS_WITH_NO_CELLS Don't load any rows that contain no cells. * self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet. * self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet. + * self::CREATE_BLANK_SHEET_IF_NONE_READ If no sheets are read, create a blank one. */ public function load(string $filename, int $flags = 0): Spreadsheet; } diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 3625641ade..55d483612c 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -320,6 +320,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp $tables = $workbookData->getElementsByTagNameNS($tableNs, 'table'); $worksheetID = 0; + $sheetCreated = false; foreach ($tables as $worksheetDataSet) { /** @var DOMElement $worksheetDataSet */ $worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name'); @@ -337,6 +338,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp // Create sheet $spreadsheet->createSheet(); + $sheetCreated = true; $spreadsheet->setActiveSheetIndex($worksheetID); if ($worksheetName || is_numeric($worksheetName)) { @@ -658,6 +660,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); ++$worksheetID; } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $spreadsheet->createSheet(); + } $autoFilterReader->read($workbookData); $definedNameReader->read($workbookData); diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index b8a5b7ab33..bd39f0fa72 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -700,6 +700,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet // Parse the individual sheets $this->activeSheetSet = false; + $sheetCreated = false; foreach ($this->sheets as $sheet) { $selectedCells = ''; if ($sheet['sheetType'] != 0x00) { @@ -714,6 +715,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet // add sheet to PhpSpreadsheet object $this->phpSheet = $this->spreadsheet->createSheet(); + $sheetCreated = true; // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula // cells... during the load, all formulae should be correct, and we're simply bringing the worksheet // name in line with the formula, not the reverse @@ -1115,6 +1117,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $this->phpSheet->setSelectedCells($selectedCells); } } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $this->spreadsheet->createSheet(); + } if ($this->activeSheetSet === false) { $this->spreadsheet->setActiveSheetIndex(0); } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index e5d67be187..c43cf06e98 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -750,6 +750,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet $charts = $chartDetails = []; + $sheetCreated = false; if ($xmlWorkbookNS->sheets) { /** @var SimpleXMLElement $eleSheet */ foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) { @@ -777,6 +778,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet // Load sheet $docSheet = $excel->createSheet(); + $sheetCreated = true; // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet // references in formula cells... during the load, all formulae should be correct, // and we're simply bringing the worksheet name in line with the formula, not the @@ -1821,6 +1823,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet } } } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $excel->createSheet(); + } (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly); diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 28a9d63e51..999ea2ba24 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -299,6 +299,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo $worksheetID = 0; $xml_ss = $xml->children(self::NAMESPACES_SS); + $sheetCreated = false; /** @var null|SimpleXMLElement $worksheetx */ foreach ($xml_ss->Worksheet as $worksheetx) { $worksheet = $worksheetx ?? new SimpleXMLElement(''); @@ -313,6 +314,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo // Create new Worksheet $spreadsheet->createSheet(); + $sheetCreated = true; $spreadsheet->setActiveSheetIndex($worksheetID); $worksheetName = ''; if (isset($worksheet_ss['Name'])) { @@ -658,6 +660,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo } ++$worksheetID; } + if ($this->createBlankSheetIfNoneRead && !$sheetCreated) { + $spreadsheet->createSheet(); + } // Globally scoped defined names $activeSheetIndex = 0; diff --git a/tests/PhpSpreadsheetTests/Reader/CreateBlankSheetIfNoneReadTest.php b/tests/PhpSpreadsheetTests/Reader/CreateBlankSheetIfNoneReadTest.php new file mode 100644 index 0000000000..a5f1ac6dd0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/CreateBlankSheetIfNoneReadTest.php @@ -0,0 +1,69 @@ +expectException(SpreadsheetException::class); + $this->expectExceptionMessage('out of bounds index: 0'); + $actual = IOFactory::identify($file); + self::assertSame($expectedName, $actual); + $reader = IOFactory::createReaderForFile($file); + self::assertSame($expectedClass, $reader::class); + $sheetlist = ['Unknown sheetname']; + $reader->setLoadSheetsOnly($sheetlist); + $reader->load($file); + } + + #[DataProvider('providerIdentify')] + public function testCreateSheetIfNoSheet(string $file, string $expectedName, string $expectedClass): void + { + $actual = IOFactory::identify($file); + self::assertSame($expectedName, $actual); + $reader = IOFactory::createReaderForFile($file); + self::assertSame($expectedClass, $reader::class); + $reader->setCreateBlankSheetIfNoneRead(true); + $sheetlist = ['Unknown sheetname']; + $reader->setLoadSheetsOnly($sheetlist); + $spreadsheet = $reader->load($file); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + self::assertCount(1, $spreadsheet->getAllSheets()); + $spreadsheet->disconnectWorksheets(); + } + + public static function providerIdentify(): array + { + return [ + ['samples/templates/26template.xlsx', 'Xlsx', Reader\Xlsx::class], + ['samples/templates/GnumericTest.gnumeric', 'Gnumeric', Reader\Gnumeric::class], + ['samples/templates/30template.xls', 'Xls', Reader\Xls::class], + ['samples/templates/OOCalcTest.ods', 'Ods', Reader\Ods::class], + ['samples/templates/excel2003.xml', 'Xml', Reader\Xml::class], + ]; + } + + public function testUsingFlage(): void + { + $file = 'samples/templates/26template.xlsx'; + $reader = IOFactory::createReaderForFile($file); + $sheetlist = ['Unknown sheetname']; + $reader->setLoadSheetsOnly($sheetlist); + $spreadsheet = $reader->load($file, Reader\BaseReader::CREATE_BLANK_SHEET_IF_NONE_READ); + $sheet = $spreadsheet->getActiveSheet(); + self::assertSame('Worksheet', $sheet->getTitle()); + self::assertCount(1, $spreadsheet->getAllSheets()); + $spreadsheet->disconnectWorksheets(); + } +} From 16d2319dfc4fb7974fe535d1618a100b8a7aade2 Mon Sep 17 00:00:00 2001 From: oleibman <10341515+oleibman@users.noreply.github.com> Date: Sun, 31 Aug 2025 23:59:24 -0700 Subject: [PATCH 2/2] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 030a43c472..fff982780a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). This is a ### Added -- Option for Readers to create a new blank sheet if none match LoadSheetsOnly list. Backport of PR #4618. +- Option for Readers to create a new blank sheet if none match LoadSheetsOnly list. [PR #4622](https://github.com/PHPOffice/PhpSpreadsheet/pull/4622) Backport of [PR #4618](https://github.com/PHPOffice/PhpSpreadsheet/pull/4618). ### Fixed