Skip to content

Commit 2fd3ac5

Browse files
committed
Option to Create Blank Sheet If LoadSheetsOnly Doesn't Find Any
In PhpSpreadsheet Release 1, if the LoadSheetsOnly option was specified, and no sheets matched, a new blank sheet was created. This behavior changed in PhpSpreadsheet Release 2, so that an exception wound up being thrown instead. Although the Release 2 approach seems more sensible to me, it was actually collateral damage from a different change, and was not an intentional result. The difference in behavior is causing a problem for Laravel-Excel. In particular, a PR which would move their supported PhpSpreadsheet release from 1 to 5, is delayed because this change in behavior breaks part of their test suite. See SpartnerNL/Laravel-Excel#4302. We would very much like them to get off release 1. I volunteered to add a compatibility option to the Readers which would emulate the release 1 behavior. The result is this PR. Usage: ```php $reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx(); $reader->setLoadSheetsOnly([list of sheet names]); if (method_exists($reader, 'setCreateBlankSheetIfNoneRead')) { $reader->setCreateBlankSheetIfNoneRead(true); } ``` In addition to Xlsx Reader, the method is available for Xls, Ods, Xml, and Gnumeric.
1 parent b79b77c commit 2fd3ac5

File tree

8 files changed

+110
-0
lines changed

8 files changed

+110
-0
lines changed

src/PhpSpreadsheet/Reader/BaseReader.php

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

57+
/**
58+
* Create a blank sheet if none are read,
59+
* possibly due to a typo when using LoadSheetsOnly.
60+
*/
61+
protected bool $createBlankSheetIfNoneRead = false;
62+
5763
/**
5864
* IReadFilter instance.
5965
*/
@@ -173,6 +179,17 @@ public function getAllowExternalImages(): bool
173179
return $this->allowExternalImages;
174180
}
175181

182+
/**
183+
* Create a blank sheet if none are read,
184+
* possibly due to a typo when using LoadSheetsOnly.
185+
*/
186+
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self
187+
{
188+
$this->createBlankSheetIfNoneRead = $createBlankSheetIfNoneRead;
189+
190+
return $this;
191+
}
192+
176193
public function getSecurityScanner(): ?XmlScanner
177194
{
178195
return $this->securityScanner;
@@ -207,6 +224,9 @@ protected function processFlags(int $flags): void
207224
if (((bool) ($flags & self::DONT_ALLOW_EXTERNAL_IMAGES)) === true) {
208225
$this->setAllowExternalImages(false);
209226
}
227+
if (((bool) ($flags & self::CREATE_BLANK_SHEET_IF_NONE_READ)) === true) {
228+
$this->setCreateBlankSheetIfNoneRead(true);
229+
}
210230
}
211231

212232
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
@@ -269,6 +269,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
269269
(new Properties($this->spreadsheet))->readProperties($xml, $gnmXML);
270270

271271
$worksheetID = 0;
272+
$sheetCreated = false;
272273
foreach ($gnmXML->Sheets->Sheet as $sheetOrNull) {
273274
$sheet = self::testSimpleXml($sheetOrNull);
274275
$worksheetName = (string) $sheet->Name;
@@ -280,6 +281,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
280281

281282
// Create new Worksheet
282283
$this->spreadsheet->createSheet();
284+
$sheetCreated = true;
283285
$this->spreadsheet->setActiveSheetIndex($worksheetID);
284286
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
285287
// cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
@@ -329,6 +331,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
329331
$this->setSelectedCells($sheet);
330332
++$worksheetID;
331333
}
334+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
335+
$this->spreadsheet->createSheet();
336+
}
332337

333338
$this->processDefinedNames($gnmXML);
334339

src/PhpSpreadsheet/Reader/IReader.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ interface IReader
4141
public const ALLOW_EXTERNAL_IMAGES = 16;
4242
public const DONT_ALLOW_EXTERNAL_IMAGES = 32;
4343

44+
public const CREATE_BLANK_SHEET_IF_NONE_READ = 64;
45+
4446
public function __construct();
4547

4648
/**
@@ -149,6 +151,12 @@ public function setAllowExternalImages(bool $allowExternalImages): self;
149151

150152
public function getAllowExternalImages(): bool;
151153

154+
/**
155+
* Create a blank sheet if none are read,
156+
* possibly due to a typo when using LoadSheetsOnly.
157+
*/
158+
public function setCreateBlankSheetIfNoneRead(bool $createBlankSheetIfNoneRead): self;
159+
152160
/**
153161
* Loads PhpSpreadsheet from file.
154162
*
@@ -161,6 +169,7 @@ public function getAllowExternalImages(): bool;
161169
* self::IGNORE_ROWS_WITH_NO_CELLS Don't load any rows that contain no cells.
162170
* self::ALLOW_EXTERNAL_IMAGES Attempt to fetch images stored outside the spreadsheet.
163171
* self::DONT_ALLOW_EXTERNAL_IMAGES Don't attempt to fetch images stored outside the spreadsheet.
172+
* self::CREATE_BLANK_SHEET_IF_NONE_READ If no sheets are read, create a blank one.
164173
*/
165174
public function load(string $filename, int $flags = 0): Spreadsheet;
166175
}

src/PhpSpreadsheet/Reader/Ods.php

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

320320
$worksheetID = 0;
321+
$sheetCreated = false;
321322
foreach ($tables as $worksheetDataSet) {
322323
/** @var DOMElement $worksheetDataSet */
323324
$worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
@@ -335,6 +336,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
335336

336337
// Create sheet
337338
$spreadsheet->createSheet();
339+
$sheetCreated = true;
338340
$spreadsheet->setActiveSheetIndex($worksheetID);
339341

340342
if ($worksheetName || is_numeric($worksheetName)) {
@@ -441,6 +443,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Sp
441443
);
442444
++$worksheetID;
443445
}
446+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
447+
$spreadsheet->createSheet();
448+
}
444449

445450
$autoFilterReader->read($workbookData);
446451
$definedNameReader->read($workbookData);

src/PhpSpreadsheet/Reader/Xls/LoadSpreadsheet.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ protected function loadSpreadsheetFromFile2(string $filename, Xls $xls): Spreads
153153

154154
// Parse the individual sheets
155155
$xls->activeSheetSet = false;
156+
$sheetCreated = false;
156157
foreach ($xls->sheets as $sheet) {
157158
$selectedCells = '';
158159
if ($sheet['sheetType'] != 0x00) {
@@ -167,6 +168,7 @@ protected function loadSpreadsheetFromFile2(string $filename, Xls $xls): Spreads
167168

168169
// add sheet to PhpSpreadsheet object
169170
$xls->phpSheet = $xls->spreadsheet->createSheet();
171+
$sheetCreated = true;
170172
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in formula
171173
// cells... during the load, all formulae should be correct, and we're simply bringing the worksheet
172174
// name in line with the formula, not the reverse
@@ -582,6 +584,9 @@ protected function loadSpreadsheetFromFile2(string $filename, Xls $xls): Spreads
582584
$xls->phpSheet->setSelectedCells($selectedCells);
583585
}
584586
}
587+
if ($xls->createBlankSheetIfNoneRead && !$sheetCreated) {
588+
$xls->spreadsheet->createSheet();
589+
}
585590
if ($xls->activeSheetSet === false) {
586591
$xls->spreadsheet->setActiveSheetIndex(0);
587592
}

src/PhpSpreadsheet/Reader/Xlsx.php

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

785785
$charts = $chartDetails = [];
786786

787+
$sheetCreated = false;
787788
if ($xmlWorkbookNS->sheets) {
788789
foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
789790
$eleSheetAttr = self::getAttributes($eleSheet);
@@ -810,6 +811,7 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
810811

811812
// Load sheet
812813
$docSheet = $excel->createSheet();
814+
$sheetCreated = true;
813815
// Use false for $updateFormulaCellReferences to prevent adjustment of worksheet
814816
// references in formula cells... during the load, all formulae should be correct,
815817
// and we're simply bringing the worksheet name in line with the formula, not the
@@ -1897,6 +1899,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
18971899
}
18981900
}
18991901
}
1902+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
1903+
$excel->createSheet();
1904+
}
19001905

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

src/PhpSpreadsheet/Reader/Xml.php

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

310+
$sheetCreated = false;
310311
/** @var null|SimpleXMLElement $worksheetx */
311312
foreach ($xml_ss->Worksheet as $worksheetx) {
312313
$worksheet = $worksheetx ?? new SimpleXMLElement('<xml></xml>');
@@ -321,6 +322,7 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
321322

322323
// Create new Worksheet
323324
$spreadsheet->createSheet();
325+
$sheetCreated = true;
324326
$spreadsheet->setActiveSheetIndex($worksheetID);
325327
$worksheetName = '';
326328
if (isset($worksheet_ss['Name'])) {
@@ -668,6 +670,9 @@ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet, boo
668670
}
669671
++$worksheetID;
670672
}
673+
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
674+
$spreadsheet->createSheet();
675+
}
671676

672677
// Globally scoped defined names
673678
$activeSheetIndex = 0;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
}

0 commit comments

Comments
 (0)