Skip to content

Commit 964eb27

Browse files
authored
Merge pull request #3314 from PHPOffice/CellIterator-Option-to-create-new-cell-or-return-null
Provide an option for Cell Iterators to return a null or create a new cell when cell doesn't exist
2 parents 5419eee + 8ad39f7 commit 964eb27

File tree

7 files changed

+160
-22
lines changed

7 files changed

+160
-22
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
99

1010
### Added
1111

12+
- Option for Cell Iterator to return a null value or create and return a new cell when accessing a cell that doesn't exist [PR #3314](https://github.com/PHPOffice/PhpSpreadsheet/pull/3314)
1213
- Support for Structured References in the Calculation Engine [PR #3261](https://github.com/PHPOffice/PhpSpreadsheet/pull/3261)
1314
- Limited Support for Form Controls [PR #3130](https://github.com/PHPOffice/PhpSpreadsheet/pull/3130) [Issue #2396](https://github.com/PHPOffice/PhpSpreadsheet/issues/2396) [Issue #1770](https://github.com/PHPOffice/PhpSpreadsheet/issues/1770) [Issue #2388](https://github.com/PHPOffice/PhpSpreadsheet/issues/2388) [Issue #2904](https://github.com/PHPOffice/PhpSpreadsheet/issues/2904) [Issue #2661](https://github.com/PHPOffice/PhpSpreadsheet/issues/2661)
1415

docs/topics/accessing-cells.md

+21-12
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ particularly when working with large spreadsheets. One technique used to
4646
reduce this memory overhead is cell caching, so cells are actually
4747
maintained in a collection that may or may not be held in memory while you
4848
are working with the spreadsheet. Because of this, a call to `getCell()`
49-
(or any similar method) returns the cell data, and a pointer to the collection.
49+
(or any similar method) returns the cell data, and sets a cell pointer to that cell in the collection.
5050
While this is not normally an issue, it can become significant
5151
if you assign the result of a call to `getCell()` to a variable. Any
52-
subsequent calls to retrieve other cells will unset that pointer, although
52+
subsequent calls to retrieve other cells will change that pointer, although
5353
the cell object will still retain its data values.
5454

5555
What does this mean? Consider the following code:
@@ -441,16 +441,25 @@ foreach ($worksheet->getRowIterator() as $row) {
441441
echo '</table>' . PHP_EOL;
442442
```
443443

444-
Note that we have set the cell iterator's
445-
`setIterateOnlyExistingCells()` to FALSE. This makes the iterator loop
446-
all cells within the worksheet range, even if they have not been set.
447-
448-
The cell iterator will return a `null` as the cell value if it is not
449-
set in the worksheet. Setting the cell iterator's
450-
`setIterateOnlyExistingCells()` to `false` will loop all cells in the
451-
worksheet that can be available at that moment. This will create new
452-
cells if required and increase memory usage! Only use it if it is
453-
intended to loop all cells that are possibly available.
444+
Note that we have set the cell iterator's `setIterateOnlyExistingCells()`
445+
to FALSE. This makes the iterator loop all cells within the worksheet
446+
range, even if they have not been set.
447+
448+
The cell iterator will create a new empty cell in the worksheet if it
449+
doesn't exist; return a `null` as the cell value if it is not set in
450+
the worksheet; although we can also tell it to return a null value
451+
rather than returning a new empty cell.
452+
Setting the cell iterator's `setIterateOnlyExistingCells()` to `false`
453+
will loop all cells in the worksheet that can be available at that
454+
moment. If this is then set to create new cells if required, then it
455+
will likely increase memory usage!
456+
Only use it if it is intended to loop all cells that are possibly
457+
available; otherwise use the option to return a null value if a cell
458+
doesn't exist, or iterate only the cells that already exist.
459+
460+
It is also possible to call the Row object's `isEmpty()` method to
461+
determine whether you need to instantiate the Cell Iterator for that
462+
Row.
454463

455464
### Looping through cells using indexes
456465

src/PhpSpreadsheet/Worksheet/CellIterator.php

+22
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ abstract class CellIterator implements NativeIterator
1717

1818
public const TREAT_EMPTY_STRING_AS_EMPTY_CELL = 2;
1919

20+
public const IF_NOT_EXISTS_RETURN_NULL = false;
21+
22+
public const IF_NOT_EXISTS_CREATE_NEW = true;
23+
2024
/**
2125
* Worksheet to iterate.
2226
*
@@ -38,6 +42,14 @@ abstract class CellIterator implements NativeIterator
3842
*/
3943
protected $onlyExistingCells = false;
4044

45+
/**
46+
* If iterating all cells, and a cell doesn't exist, identifies whether a new cell should be created,
47+
* or if the iterator should return a null value.
48+
*
49+
* @var bool
50+
*/
51+
protected $ifNotExists = self::IF_NOT_EXISTS_CREATE_NEW;
52+
4153
/**
4254
* Destructor.
4355
*/
@@ -47,6 +59,16 @@ public function __destruct()
4759
$this->worksheet = $this->cellCollection = null;
4860
}
4961

62+
public function getIfNotExists(): bool
63+
{
64+
return $this->ifNotExists;
65+
}
66+
67+
public function setIfNotExists(bool $ifNotExists = self::IF_NOT_EXISTS_CREATE_NEW): void
68+
{
69+
$this->ifNotExists = $ifNotExists;
70+
}
71+
5072
/**
5173
* Get loop only existing cells.
5274
*/

src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ public function current(): ?Cell
128128

129129
return $this->cellCollection->has($cellAddress)
130130
? $this->cellCollection->get($cellAddress)
131-
: $this->worksheet->createNewCell($cellAddress);
131+
: (
132+
$this->ifNotExists === self::IF_NOT_EXISTS_CREATE_NEW
133+
? $this->worksheet->createNewCell($cellAddress)
134+
: null
135+
);
132136
}
133137

134138
/**

src/PhpSpreadsheet/Worksheet/RowCellIterator.php

+5-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ public function current(): ?Cell
127127

128128
return $this->cellCollection->has($cellAddress)
129129
? $this->cellCollection->get($cellAddress)
130-
: $this->worksheet->createNewCell($cellAddress);
130+
: (
131+
$this->ifNotExists === self::IF_NOT_EXISTS_CREATE_NEW
132+
? $this->worksheet->createNewCell($cellAddress)
133+
: null
134+
);
131135
}
132136

133137
/**

tests/PhpSpreadsheetTests/Worksheet/ColumnCellIterator2Test.php

+53-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
44

55
use PhpOffice\PhpSpreadsheet\Spreadsheet;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\CellIterator;
67
use PhpOffice\PhpSpreadsheet\Worksheet\ColumnCellIterator;
78
use PHPUnit\Framework\TestCase;
89

@@ -32,8 +33,8 @@ public function testEndRange(?bool $existing, string $expectedResultFirst, strin
3233
}
3334
}
3435
}
35-
self::assertEquals($expectedResultFirst, $firstCoordinate);
36-
self::assertEquals($expectedResultLast, $lastCoordinate);
36+
self::assertSame($expectedResultFirst, $firstCoordinate);
37+
self::assertSame($expectedResultLast, $lastCoordinate);
3738
}
3839

3940
public function providerExistingCell(): array
@@ -63,15 +64,63 @@ public function testEmptyColumn(?bool $existing, int $expectedResult): void
6364
foreach ($iterator as $cell) {
6465
++$numCells;
6566
}
66-
self::assertEquals($expectedResult, $numCells);
67+
self::assertSame($expectedResult, $numCells);
6768
}
6869

6970
public function providerEmptyColumn(): array
7071
{
7172
return [
72-
[null, 6],
73+
[null, 6], // Default behaviour
7374
[false, 6],
7475
[true, 0],
7576
];
7677
}
78+
79+
/**
80+
* @dataProvider providerNullOrCreate
81+
*/
82+
public function testNullOrCreateOption(?bool $existingBehaviour, int $expectedCreatedResult): void
83+
{
84+
$spreadsheet = new Spreadsheet();
85+
$sheet = $spreadsheet->getActiveSheet();
86+
$iterator = new ColumnCellIterator($sheet, 'F');
87+
if (isset($existingBehaviour)) {
88+
$iterator->setIfNotExists($existingBehaviour);
89+
}
90+
$notExistsBehaviour = $iterator->getIfNotExists();
91+
self::assertSame($expectedCreatedResult > 0, $notExistsBehaviour);
92+
}
93+
94+
/**
95+
* @dataProvider providerNullOrCreate
96+
*/
97+
public function testNullOrCreate(?bool $existing, int $expectedCreatedResult, int $expectedNullResult): void
98+
{
99+
$spreadsheet = new Spreadsheet();
100+
$sheet = $spreadsheet->getActiveSheet();
101+
$sheet->getCell('B2')->setValue('cellb2');
102+
$sheet->getCell('F4')->setValue('cellf2');
103+
104+
$iterator = new ColumnCellIterator($sheet, 'F');
105+
if (isset($existing)) {
106+
$iterator->setIfNotExists($existing);
107+
}
108+
$numCreatedCells = $numEmptyCells = 0;
109+
foreach ($iterator as $cell) {
110+
$numCreatedCells += (int) ($cell !== null && $cell->getValue() === null);
111+
// @phpstan-ignore-next-line
112+
$numEmptyCells += (int) ($cell === null);
113+
}
114+
self::assertSame($expectedCreatedResult, $numCreatedCells);
115+
self::assertSame($expectedNullResult, $numEmptyCells);
116+
}
117+
118+
public function providerNullOrCreate(): array
119+
{
120+
return [
121+
[null, 3, 0], // Default behaviour
122+
[CellIterator::IF_NOT_EXISTS_CREATE_NEW, 3, 0],
123+
[CellIterator::IF_NOT_EXISTS_RETURN_NULL, 0, 3],
124+
];
125+
}
77126
}

tests/PhpSpreadsheetTests/Worksheet/RowCellIterator2Test.php

+53-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
44

55
use PhpOffice\PhpSpreadsheet\Spreadsheet;
6+
use PhpOffice\PhpSpreadsheet\Worksheet\CellIterator;
67
use PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator;
78
use PHPUnit\Framework\TestCase;
89

@@ -32,8 +33,8 @@ public function testEndRangeTrue(?bool $existing, string $expectedResultFirst, s
3233
}
3334
}
3435
}
35-
self::assertEquals($expectedResultFirst, $firstCoordinate);
36-
self::assertEquals($expectedResultLast, $lastCoordinate);
36+
self::assertSame($expectedResultFirst, $firstCoordinate);
37+
self::assertSame($expectedResultLast, $lastCoordinate);
3738
}
3839

3940
public function providerExistingCell(): array
@@ -63,15 +64,63 @@ public function testEmptyRow(?bool $existing, int $expectedResult): void
6364
foreach ($iterator as $cell) {
6465
++$numCells;
6566
}
66-
self::assertEquals($expectedResult, $numCells);
67+
self::assertSame($expectedResult, $numCells);
6768
}
6869

6970
public function providerEmptyRow(): array
7071
{
7172
return [
72-
[null, 6],
73+
[null, 6], // Default behaviour
7374
[false, 6],
7475
[true, 0],
7576
];
7677
}
78+
79+
/**
80+
* @dataProvider providerNullOrCreate
81+
*/
82+
public function testNullOrCreateOption(?bool $existingBehaviour, int $expectedCreatedResult): void
83+
{
84+
$spreadsheet = new Spreadsheet();
85+
$sheet = $spreadsheet->getActiveSheet();
86+
$iterator = new RowCellIterator($sheet, 2);
87+
if (isset($existingBehaviour)) {
88+
$iterator->setIfNotExists($existingBehaviour);
89+
}
90+
$notExistsBehaviour = $iterator->getIfNotExists();
91+
self::assertSame($expectedCreatedResult > 0, $notExistsBehaviour);
92+
}
93+
94+
/**
95+
* @dataProvider providerNullOrCreate
96+
*/
97+
public function testNullOrCreate(?bool $existing, int $expectedCreatedResult, int $expectedNullResult): void
98+
{
99+
$spreadsheet = new Spreadsheet();
100+
$sheet = $spreadsheet->getActiveSheet();
101+
$sheet->getCell('B2')->setValue('cellb2');
102+
$sheet->getCell('F2')->setValue('cellf2');
103+
104+
$iterator = new RowCellIterator($sheet, 2);
105+
if (isset($existing)) {
106+
$iterator->setIfNotExists($existing);
107+
}
108+
$numCreatedCells = $numEmptyCells = 0;
109+
foreach ($iterator as $cell) {
110+
$numCreatedCells += (int) ($cell !== null && $cell->getValue() === null);
111+
// @phpstan-ignore-next-line
112+
$numEmptyCells += (int) ($cell === null);
113+
}
114+
self::assertSame($expectedCreatedResult, $numCreatedCells);
115+
self::assertSame($expectedNullResult, $numEmptyCells);
116+
}
117+
118+
public function providerNullOrCreate(): array
119+
{
120+
return [
121+
[null, 4, 0], // Default behaviour
122+
[CellIterator::IF_NOT_EXISTS_CREATE_NEW, 4, 0],
123+
[CellIterator::IF_NOT_EXISTS_RETURN_NULL, 0, 4],
124+
];
125+
}
77126
}

0 commit comments

Comments
 (0)