Skip to content

Commit 8af9963

Browse files
committed
Allow Spreadsheet Serialization
Fix #4324. Serialization was explicity forbidden by PR #3199. This was in response to several issues, and concern that the Spreadsheet object contained non-serializable properties. This PR restores the ability to serialize a spreadsheet. Json serialization remains unsupported. Fix #1757, closed in Nov. 2023 but just reopened. At the time, Cell property `formulaAttributes` was stored as a SimpleXmlElement. Dynamic arrays PR #3962 defined that property as `null|array<string, string>` in the doc block. However, it left the formal Php type for the property as `mixed`. This PR changes the formal type to `?array`. Fix #1741, closed in Dec. 2020 but just reopened. Calculation property `referenceHelper` was defined as static, and, since static properties don't take part in serialization, this caused a problem after unserialization. There are at least 3 trivial ways to deal with this - make it an instance property, reinitialize it when unserialized using a wakeup method, or remove the property altogether. This PR uses the last of those 3. Calculation does have other static properties. Almost all of these deal with locale. So serialize/unserialize might wind up using a default locale when non-default is desired (but not necessarily required). If that is a problem for end-users, it will be a new one, and I will work on a solution if and when the time comes. Static property `returnArrayAsType` is potentially problematic. However, instance property `instanceArrayReturnType` is the preferred method of handling this, and using that will avoid any problems. Issue #932 also dealt with serialization. I do not have the wherewithal to investigate that issue. If it is not solved by this and the earlier PR's, I will have to leave it to others to re-raise it. Spreadsheet `copy` is now simplified to use serialize followed by unserialize. Formal tests are added. In addition, I have made a number of informal tests on very complicated spreadsheets, and it has performed correctly for all of them.
1 parent fb757cf commit 8af9963

File tree

7 files changed

+117
-58
lines changed

7 files changed

+117
-58
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,6 @@ class Calculation
133133
*/
134134
public ?string $formulaError = null;
135135

136-
/**
137-
* Reference Helper.
138-
*/
139-
private static ReferenceHelper $referenceHelper;
140-
141136
/**
142137
* An array of the nested cell references accessed by the calculation engine, used for the debug log.
143138
*/
@@ -2890,7 +2885,6 @@ public function __construct(?Spreadsheet $spreadsheet = null)
28902885
$this->cyclicReferenceStack = new CyclicReferenceStack();
28912886
$this->debugLog = new Logger($this->cyclicReferenceStack);
28922887
$this->branchPruner = new BranchPruner($this->branchPruningEnabled);
2893-
self::$referenceHelper = ReferenceHelper::getInstance();
28942888
}
28952889

28962890
private static function loadLocales(): void
@@ -5732,11 +5726,14 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
57325726
$recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate();
57335727

57345728
// Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5735-
$definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet(
5736-
$definedNameValue,
5737-
Coordinate::columnIndexFromString($cell->getColumn()) - 1,
5738-
$cell->getRow() - 1
5739-
);
5729+
$definedNameValue = ReferenceHelper::getInstance()
5730+
->updateFormulaReferencesAnyWorksheet(
5731+
$definedNameValue,
5732+
Coordinate::columnIndexFromString(
5733+
$cell->getColumn()
5734+
) - 1,
5735+
$cell->getRow() - 1
5736+
);
57405737

57415738
$this->debugLog->writeDebugLog('Value adjusted for relative references is %s', $definedNameValue);
57425739

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Cell implements Stringable
6363
*
6464
* @var null|array<string, string>
6565
*/
66-
private mixed $formulaAttributes = null;
66+
private ?array $formulaAttributes = null;
6767

6868
private IgnoredErrors $ignoredErrors;
6969

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@
77
use PhpOffice\PhpSpreadsheet\Cell\IValueBinder;
88
use PhpOffice\PhpSpreadsheet\Document\Properties;
99
use PhpOffice\PhpSpreadsheet\Document\Security;
10-
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
1110
use PhpOffice\PhpSpreadsheet\Shared\Date;
12-
use PhpOffice\PhpSpreadsheet\Shared\File;
1311
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1412
use PhpOffice\PhpSpreadsheet\Style\Style;
1513
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
1614
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
1715
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
18-
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
1916

2017
class Spreadsheet implements JsonSerializable
2118
{
@@ -1042,17 +1039,7 @@ public function getWorksheetIterator(): Iterator
10421039
*/
10431040
public function copy(): self
10441041
{
1045-
$filename = File::temporaryFilename();
1046-
$writer = new XlsxWriter($this);
1047-
$writer->setIncludeCharts(true);
1048-
$writer->save($filename);
1049-
1050-
$reader = new XlsxReader();
1051-
$reader->setIncludeCharts(true);
1052-
$reloadedSpreadsheet = $reader->load($filename);
1053-
unlink($filename);
1054-
1055-
return $reloadedSpreadsheet;
1042+
return unserialize(serialize($this));
10561043
}
10571044

10581045
public function __clone()
@@ -1516,14 +1503,6 @@ public function reevaluateAutoFilters(bool $resetToMax): void
15161503
}
15171504
}
15181505

1519-
/**
1520-
* @throws Exception
1521-
*/
1522-
public function __serialize(): array
1523-
{
1524-
throw new Exception('Spreadsheet objects cannot be serialized');
1525-
}
1526-
15271506
/**
15281507
* @throws Exception
15291508
*/

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,6 @@ public function __destruct()
381381
public function __wakeup(): void
382382
{
383383
$this->hash = spl_object_id($this);
384-
$this->parent = null;
385384
}
386385

387386
/**
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests;
6+
7+
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
8+
use PhpOffice\PhpSpreadsheet\Helper\Sample;
9+
use PhpOffice\PhpSpreadsheet\NamedRange;
10+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
11+
use PHPUnit\Framework\Attributes;
12+
use PHPUnit\Framework\TestCase;
13+
14+
class SpreadsheetSerializeTest extends TestCase
15+
{
16+
private ?Spreadsheet $spreadsheet = null;
17+
18+
protected function tearDown(): void
19+
{
20+
if ($this->spreadsheet !== null) {
21+
$this->spreadsheet->disconnectWorksheets();
22+
$this->spreadsheet = null;
23+
}
24+
}
25+
26+
public function testSerialize(): void
27+
{
28+
$this->spreadsheet = new Spreadsheet();
29+
$sheet = $this->spreadsheet->getActiveSheet();
30+
$sheet->getCell('A1')->setValue(10);
31+
32+
$serialized = serialize($this->spreadsheet);
33+
$newSpreadsheet = unserialize($serialized);
34+
self::assertInstanceOf(Spreadsheet::class, $newSpreadsheet);
35+
self::assertNotSame($this->spreadsheet, $newSpreadsheet);
36+
$newSheet = $newSpreadsheet->getActiveSheet();
37+
self::assertSame(10, $newSheet->getCell('A1')->getValue());
38+
$newSpreadsheet->disconnectWorksheets();
39+
}
40+
41+
public function testNotJsonEncodable(): void
42+
{
43+
$this->spreadsheet = new Spreadsheet();
44+
45+
$this->expectException(SpreadsheetException::class);
46+
$this->expectExceptionMessage('Spreadsheet objects cannot be json encoded');
47+
json_encode($this->spreadsheet);
48+
}
49+
50+
/**
51+
* These tests are a bit weird.
52+
* If prepareSerialize and readSerialize are run in the same
53+
* process, the latter's assertions will always succeed.
54+
* So to demonstrate that the
55+
* problem is solved, they need to run in separate processes.
56+
* But then they can't share the file name. So we need to send
57+
* the file to a semi-hard-coded destination.
58+
*/
59+
private static function getTempFileName(): string
60+
{
61+
$helper = new Sample();
62+
63+
return $helper->getTemporaryFolder() . '/spreadsheet.serialize.test.txt';
64+
}
65+
66+
public function testPrepareSerialize(): void
67+
{
68+
$this->spreadsheet = new Spreadsheet();
69+
$sheet = $this->spreadsheet->getActiveSheet();
70+
$this->spreadsheet->addNamedRange(new NamedRange('summedcells', $sheet, '$A$1:$A$5'));
71+
$sheet->setCellValue('A1', 1);
72+
$sheet->setCellValue('A2', 2);
73+
$sheet->setCellValue('A3', 3);
74+
$sheet->setCellValue('A4', 4);
75+
$sheet->setCellValue('A5', 5);
76+
$sheet->setCellValue('C1', '=SUM(summedcells)');
77+
$ser = serialize($this->spreadsheet);
78+
$this->spreadsheet->disconnectWorksheets();
79+
$outputFileName = self::getTempFileName();
80+
self::assertNotFalse(
81+
file_put_contents($outputFileName, $ser)
82+
);
83+
}
84+
85+
#[Attributes\RunInSeparateProcess]
86+
public function testReadSerialize(): void
87+
{
88+
$inputFileName = self::getTempFileName();
89+
$ser = (string) file_get_contents($inputFileName);
90+
unlink($inputFileName);
91+
$this->spreadsheet = unserialize($ser);
92+
$sheet = $this->spreadsheet->getActiveSheet();
93+
self::assertSame('=SUM(summedcells)', $sheet->getCell('C1')->getValue());
94+
self::assertSame(15, $sheet->getCell('C1')->getCalculatedValue());
95+
}
96+
}

tests/PhpSpreadsheetTests/SpreadsheetTest.php

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -291,22 +291,4 @@ public function testAddExternalRowDimensionStyles(): void
291291
self::assertEquals($countXfs + $index, $sheet3->getCell('A2')->getXfIndex());
292292
self::assertEquals($countXfs + $index, $sheet3->getRowDimension(2)->getXfIndex());
293293
}
294-
295-
public function testNotSerializable(): void
296-
{
297-
$this->spreadsheet = new Spreadsheet();
298-
299-
$this->expectException(Exception::class);
300-
$this->expectExceptionMessage('Spreadsheet objects cannot be serialized');
301-
serialize($this->spreadsheet);
302-
}
303-
304-
public function testNotJsonEncodable(): void
305-
{
306-
$this->spreadsheet = new Spreadsheet();
307-
308-
$this->expectException(Exception::class);
309-
$this->expectExceptionMessage('Spreadsheet objects cannot be json encoded');
310-
json_encode($this->spreadsheet);
311-
}
312294
}

tests/PhpSpreadsheetTests/Worksheet/CloneTest.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,27 @@ public function testGetCloneIndex(): void
4444

4545
public function testSerialize1(): void
4646
{
47-
// If worksheet attached to spreadsheet, can't serialize it.
48-
$this->expectException(SpreadsheetException::class);
49-
$this->expectExceptionMessage('cannot be serialized');
5047
$spreadsheet = new Spreadsheet();
5148
$sheet1 = $spreadsheet->getActiveSheet();
52-
serialize($sheet1);
49+
$sheet1->getCell('A1')->setValue(10);
50+
$serialized = serialize($sheet1);
51+
$newSheet = unserialize($serialized);
52+
self::assertInstanceOf(Worksheet::class, $newSheet);
53+
self::assertSame(10, $newSheet->getCell('A1')->getValue());
54+
self::assertNotEquals($newSheet->getHashInt(), $sheet1->getHashInt());
55+
self::assertNotNull($newSheet->getParent());
56+
self::assertNotSame($newSheet->getParent(), $sheet1->getParent());
57+
$newSheet->getParent()->disconnectWorksheets();
58+
$spreadsheet->disconnectWorksheets();
5359
}
5460

5561
public function testSerialize2(): void
5662
{
5763
$sheet1 = new Worksheet();
5864
$sheet1->getCell('A1')->setValue(10);
5965
$serialized = serialize($sheet1);
60-
/** @var Worksheet */
6166
$newSheet = unserialize($serialized);
67+
self::assertInstanceOf(Worksheet::class, $newSheet);
6268
self::assertSame(10, $newSheet->getCell('A1')->getValue());
6369
self::assertNotEquals($newSheet->getHashInt(), $sheet1->getHashInt());
6470
}

0 commit comments

Comments
 (0)