Skip to content

Commit 67245f8

Browse files
authored
Option for Readers to create a new blank sheet if none match LoadSheetsOnly list. (#4622)
* Option for Readers to create a new blank sheet if none match LoadSheetsOnly list. Backport of PR #4618. * Update CHANGELOG.md
1 parent 50eeec0 commit 67245f8

File tree

9 files changed

+133
-0
lines changed

9 files changed

+133
-0
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com)
66
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.
77

8+
## 2025-09-03 - 2.4.1
9+
10+
### Added
11+
12+
- 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).
13+
14+
### Fixed
15+
16+
- 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)
17+
818
## 2025-08-10 - 2.4.0
919

1020
### Breaking Changes

src/PhpSpreadsheet/Reader/BaseReader.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ abstract class BaseReader implements IReader
5353
*/
5454
protected bool $allowExternalImages = false;
5555

56+
/**
57+
* Create a blank sheet if none are read,
58+
* possibly due to a typo when using LoadSheetsOnly.
59+
*/
60+
protected bool $createBlankSheetIfNoneRead = false;
61+
5662
/**
5763
* IReadFilter instance.
5864
*/
@@ -168,6 +174,17 @@ public function getAllowExternalImages(): bool
168174
return $this->allowExternalImages;
169175
}
170176

177+
/**
178+
* Create a blank sheet if none are read,
179+
* possibly due to a typo when using LoadSheetsOnly.
180+
*/
181+
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self
182+
{
183+
$this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;
184+
185+
return $this;
186+
}
187+
171188
public function getSecurityScanner(): ?XmlScanner
172189
{
173190
return $this->securityScanner;
@@ -202,6 +219,9 @@ protected function processFlags(int $flags): void
202219
if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
203220
$this->setAllowExternalImages(false);
204221
}
222+
if (((bool) ($flags & self::CREATE_BLANK_SHEET_IF_NONE_READ)) === true) {
223+
$this->setCreateBlankSheetIfNoneRead(true);
224+
}
205225
}
206226

207227
protected function loadSpreadsheetFromFile(string $filename): Spreadsheet

src/PhpSpreadsheet/Reader/Gnumeric.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
253253
(new Properties($this->spreadsheet))->readProperties($xml, $gnmXML);
254254

255255
$worksheetID = 0;
256+
$sheetCreated = false;
256257
foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) {
257258
$sheet = self::testSimpleXml($sheetOrNull);
258259
$worksheetName = (string) $sheet->Name;
@@ -264,6 +265,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
264265

265266
// Create new Worksheet
266267
$this->spreadsheet->createSheet();
268+
$sheetCreated = true;
267269
$this->spreadsheet->setActiveSheetIndex($worksheetID);
268270
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
269271
// 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
315317
$this->setSelectedCells($sheet);
316318
++$worksheetID;
317319
}
320+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
321+
$this->spreadsheet->createSheet();
322+
}
318323

319324
$this->processDefinedNames($gnmXML);
320325

src/PhpSpreadsheet/Reader/IReader.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ interface IReader
2323
public const ALLOW_EXTERNAL_IMAGES = 16;
2424
public const DONT_ALLOW_EXTERNAL_IMAGES = 32;
2525

26+
public const CREATE_BLANK_SHEET_IF_NONE_READ = 64;
27+
2628
public function __construct();
2729

2830
/**
@@ -129,6 +131,12 @@ public function setAllowExternalImages(bool $allowExternalImages): self;
129131

130132
public function getAllowExternalImages(): bool;
131133

134+
/**
135+
* Create a blank sheet if none are read,
136+
* possibly due to a typo when using LoadSheetsOnly.
137+
*/
138+
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self;
139+
132140
/**
133141
* Loads PhpSpreadsheet from file.
134142
*
@@ -141,6 +149,7 @@ public function getAllowExternalImages(): bool;
141149
* self::IGNORE_ROWS_WITH_NO_CELLS Don't load any rows that contain no cells.
142150
* self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet.
143151
* self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet.
152+
* self::CREATE_BLANK_SHEET_IF_NONE_READ If no sheets are read, create a blank one.
144153
*/
145154
public function load(string $filename, int $flags = 0): Spreadsheet;
146155
}

src/PhpSpreadsheet/Reader/Ods.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
320320
$tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
321321

322322
$worksheetID = 0;
323+
$sheetCreated = false;
323324
foreach ($tables as $worksheetDataSet) {
324325
/** @var DOMElement $worksheetDataSet */
325326
$worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
@@ -337,6 +338,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
337338

338339
// Create sheet
339340
$spreadsheet->createSheet();
341+
$sheetCreated = true;
340342
$spreadsheet->setActiveSheetIndex($worksheetID);
341343

342344
if ($worksheetName || is_numeric($worksheetName)) {
@@ -658,6 +660,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
658660
$pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName);
659661
++$worksheetID;
660662
}
663+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
664+
$spreadsheet->createSheet();
665+
}
661666

662667
$autoFilterReader->read($workbookData);
663668
$definedNameReader->read($workbookData);

src/PhpSpreadsheet/Reader/Xls.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,6 +700,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
700700

701701
// Parse the individual sheets
702702
$this->activeSheetSet = false;
703+
$sheetCreated = false;
703704
foreach ($this->sheets as $sheet) {
704705
$selectedCells = '';
705706
if ($sheet['sheetType'] != 0x00) {
@@ -714,6 +715,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
714715

715716
// add sheet to PhpSpreadsheet object
716717
$this->phpSheet = $this->spreadsheet->createSheet();
718+
$sheetCreated = true;
717719
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
718720
// cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
719721
// name in line with the formula, not the reverse
@@ -1115,6 +1117,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
11151117
$this->phpSheet->setSelectedCells($selectedCells);
11161118
}
11171119
}
1120+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
1121+
$this->spreadsheet->createSheet();
1122+
}
11181123
if ($this->activeSheetSet === false) {
11191124
$this->spreadsheet->setActiveSheetIndex(0);
11201125
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
750750

751751
$charts = $chartDetails = [];
752752

753+
$sheetCreated = false;
753754
if ($xmlWorkbookNS->sheets) {
754755
/** @var SimpleXMLElement $eleSheet */
755756
foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
@@ -777,6 +778,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
777778

778779
// Load sheet
779780
$docSheet = $excel->createSheet();
781+
$sheetCreated = true;
780782
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
781783
// references in formula cells... during the load, all formulae should be correct,
782784
// 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
18211823
}
18221824
}
18231825
}
1826+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
1827+
$excel->createSheet();
1828+
}
18241829

18251830
(new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly);
18261831

src/PhpSpreadsheet/Reader/Xml.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
299299
$worksheetID = 0;
300300
$xml_ss = $xml->children(self::NAMESPACES_SS);
301301

302+
$sheetCreated = false;
302303
/** @var null|SimpleXMLElement $worksheetx */
303304
foreach ($xml_ss->Worksheet as $worksheetx) {
304305
$worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>');
@@ -313,6 +314,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
313314

314315
// Create new Worksheet
315316
$spreadsheet->createSheet();
317+
$sheetCreated = true;
316318
$spreadsheet->setActiveSheetIndex($worksheetID);
317319
$worksheetName = '';
318320
if (isset($worksheet_ss['Name'])) {
@@ -658,6 +660,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
658660
}
659661
++$worksheetID;
660662
}
663+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
664+
$spreadsheet->createSheet();
665+
}
661666

662667
// Globally scoped defined names
663668
$activeSheetIndex = 0;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader;
6+
7+
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
8+
use PhpOffice\PhpSpreadsheet\IOFactory;
9+
use PhpOffice\PhpSpreadsheet\Reader;
10+
use PHPUnit\Framework\Attributes\DataProvider;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class CreateBlankSheetIfNoneReadTest extends TestCase
14+
{
15+
#[DataProvider('providerIdentify')]
16+
public function testExceptionIfNoSheet(string $file, string $expectedName, string $expectedClass): void
17+
{
18+
$this->expectException(SpreadsheetException::class);
19+
$this->expectExceptionMessage('out of bounds index: 0');
20+
$actual = IOFactory::identify($file);
21+
self::assertSame($expectedName, $actual);
22+
$reader = IOFactory::createReaderForFile($file);
23+
self::assertSame($expectedClass, $reader::class);
24+
$sheetlist = ['Unknown sheetname'];
25+
$reader->setLoadSheetsOnly($sheetlist);
26+
$reader->load($file);
27+
}
28+
29+
#[DataProvider('providerIdentify')]
30+
public function testCreateSheetIfNoSheet(string $file, string $expectedName, string $expectedClass): void
31+
{
32+
$actual = IOFactory::identify($file);
33+
self::assertSame($expectedName, $actual);
34+
$reader = IOFactory::createReaderForFile($file);
35+
self::assertSame($expectedClass, $reader::class);
36+
$reader->setCreateBlankSheetIfNoneRead(true);
37+
$sheetlist = ['Unknown sheetname'];
38+
$reader->setLoadSheetsOnly($sheetlist);
39+
$spreadsheet = $reader->load($file);
40+
$sheet = $spreadsheet->getActiveSheet();
41+
self::assertSame('Worksheet', $sheet->getTitle());
42+
self::assertCount(1, $spreadsheet->getAllSheets());
43+
$spreadsheet->disconnectWorksheets();
44+
}
45+
46+
public static function providerIdentify(): array
47+
{
48+
return [
49+
['samples/templates/26template.xlsx', 'Xlsx', Reader\Xlsx::class],
50+
['samples/templates/GnumericTest.gnumeric', 'Gnumeric', Reader\Gnumeric::class],
51+
['samples/templates/30template.xls', 'Xls', Reader\Xls::class],
52+
['samples/templates/OOCalcTest.ods', 'Ods', Reader\Ods::class],
53+
['samples/templates/excel2003.xml', 'Xml', Reader\Xml::class],
54+
];
55+
}
56+
57+
public function testUsingFlage(): void
58+
{
59+
$file = 'samples/templates/26template.xlsx';
60+
$reader = IOFactory::createReaderForFile($file);
61+
$sheetlist = ['Unknown sheetname'];
62+
$reader->setLoadSheetsOnly($sheetlist);
63+
$spreadsheet = $reader->load($file, Reader\BaseReader::CREATE_BLANK_SHEET_IF_NONE_READ);
64+
$sheet = $spreadsheet->getActiveSheet();
65+
self::assertSame('Worksheet', $sheet->getTitle());
66+
self::assertCount(1, $spreadsheet->getAllSheets());
67+
$spreadsheet->disconnectWorksheets();
68+
}
69+
}

0 commit comments

Comments
 (0)