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 https://github.com/SpartnerNL/Laravel-Excel/pull/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.
This commit is contained in:
oleibman
2025-08-28 13:44:32 -07:00
parent b79b77cf0f
commit 2fd3ac5552
8 changed files with 110 additions and 0 deletions
+20
View File
@@ -54,6 +54,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.
*/
@@ -173,6 +179,17 @@ abstract class BaseReader implements IReader
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;
@@ -207,6 +224,9 @@ abstract class BaseReader implements IReader
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
+5
View File
@@ -269,6 +269,7 @@ class Gnumeric extends BaseReader
(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;
@@ -280,6 +281,7 @@ class Gnumeric extends BaseReader
// 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
@@ -329,6 +331,9 @@ class Gnumeric extends BaseReader
$this->setSelectedCells($sheet);
++$worksheetID;
}
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
$this->spreadsheet->createSheet();
}
$this->processDefinedNames($gnmXML);
+9
View File
@@ -41,6 +41,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();
/**
@@ -149,6 +151,12 @@ interface IReader
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.
*
@@ -161,6 +169,7 @@ interface IReader
* 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;
}
+5
View File
@@ -318,6 +318,7 @@ class Ods extends BaseReader
$tables = $workbookData->getElementsByTagNameNS($tableNs, 'table');
$worksheetID = 0;
$sheetCreated = false;
foreach ($tables as $worksheetDataSet) {
/** @var DOMElement $worksheetDataSet */
$worksheetName = $worksheetDataSet->getAttributeNS($tableNs, 'name');
@@ -335,6 +336,7 @@ class Ods extends BaseReader
// Create sheet
$spreadsheet->createSheet();
$sheetCreated = true;
$spreadsheet->setActiveSheetIndex($worksheetID);
if ($worksheetName || is_numeric($worksheetName)) {
@@ -441,6 +443,9 @@ class Ods extends BaseReader
);
++$worksheetID;
}
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
$spreadsheet->createSheet();
}
$autoFilterReader->read($workbookData);
$definedNameReader->read($workbookData);
@@ -153,6 +153,7 @@ class LoadSpreadsheet extends Xls
// Parse the individual sheets
$xls->activeSheetSet = false;
$sheetCreated = false;
foreach ($xls->sheets as $sheet) {
$selectedCells = '';
if ($sheet['sheetType'] != 0x00) {
@@ -167,6 +168,7 @@ class LoadSpreadsheet extends Xls
// add sheet to PhpSpreadsheet object
$xls->phpSheet = $xls->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
@@ -582,6 +584,9 @@ class LoadSpreadsheet extends Xls
$xls->phpSheet->setSelectedCells($selectedCells);
}
}
if ($xls->createBlankSheetIfNoneRead && !$sheetCreated) {
$xls->spreadsheet->createSheet();
}
if ($xls->activeSheetSet === false) {
$xls->spreadsheet->setActiveSheetIndex(0);
}
+5
View File
@@ -784,6 +784,7 @@ class Xlsx extends BaseReader
$charts = $chartDetails = [];
$sheetCreated = false;
if ($xmlWorkbookNS->sheets) {
foreach ($xmlWorkbookNS->sheets->sheet as $eleSheet) {
$eleSheetAttr = self::getAttributes($eleSheet);
@@ -810,6 +811,7 @@ class Xlsx extends BaseReader
// 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
@@ -1897,6 +1899,9 @@ class Xlsx extends BaseReader
}
}
}
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
$excel->createSheet();
}
(new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly);
+5
View File
@@ -307,6 +307,7 @@ class Xml extends BaseReader
$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('<xml></xml>');
@@ -321,6 +322,7 @@ class Xml extends BaseReader
// Create new Worksheet
$spreadsheet->createSheet();
$sheetCreated = true;
$spreadsheet->setActiveSheetIndex($worksheetID);
$worksheetName = '';
if (isset($worksheet_ss['Name'])) {
@@ -668,6 +670,9 @@ class Xml extends BaseReader
}
++$worksheetID;
}
if ($this->createBlankSheetIfNoneRead && !$sheetCreated) {
$spreadsheet->createSheet();
}
// Globally scoped defined names
$activeSheetIndex = 0;
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace PhpOffice\PhpSpreadsheetTests\Reader;
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Reader;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
class CreateBlankSheetIfNoneReadTest extends TestCase
{
#[DataProvider('providerIdentify')]
public function testExceptionIfNoSheet(string $file, string $expectedName, string $expectedClass): void
{
$this->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],
];
}
}