Skip to content

Commit 7697814

Browse files
authored
Allow Spreadsheet Serialization Branch release222 (#4407)
1 parent a02e4c2 commit 7697814

File tree

9 files changed

+154
-77
lines changed

9 files changed

+154
-77
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
1111

1212
- Allow php-cs-fixer to Handle Implicit Backslashes.
1313

14+
### Added
15+
16+
- Allow spreadsheet to be serialized. [PR #4407](https://github.com/PHPOffice/PhpSpreadsheet/pull/4407)
17+
1418
### Fixed
1519

1620
- TEXT and TIMEVALUE functions. [Issue #4249](https://github.com/PHPOffice/PhpSpreadsheet/issues/4249) [PR #4354](https://github.com/PHPOffice/PhpSpreadsheet/pull/4354)

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,6 @@ class Calculation
125125
*/
126126
public ?string $formulaError = null;
127127

128-
/**
129-
* Reference Helper.
130-
*/
131-
private static ReferenceHelper $referenceHelper;
132-
133128
/**
134129
* An array of the nested cell references accessed by the calculation engine, used for the debug log.
135130
*/
@@ -2878,7 +2873,6 @@ public function __construct(?Spreadsheet $spreadsheet = null)
28782873
$this->cyclicReferenceStack = new CyclicReferenceStack();
28792874
$this->debugLog = new Logger($this->cyclicReferenceStack);
28802875
$this->branchPruner = new BranchPruner($this->branchPruningEnabled);
2881-
self::$referenceHelper = ReferenceHelper::getInstance();
28822876
}
28832877

28842878
private static function loadLocales(): void
@@ -5616,11 +5610,14 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
56165610
$recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate();
56175611

56185612
// Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5619-
$definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet(
5620-
$definedNameValue,
5621-
Coordinate::columnIndexFromString($cell->getColumn()) - 1,
5622-
$cell->getRow() - 1
5623-
);
5613+
$definedNameValue = ReferenceHelper::getInstance()
5614+
->updateFormulaReferencesAnyWorksheet(
5615+
$definedNameValue,
5616+
Coordinate::columnIndexFromString(
5617+
$cell->getColumn()
5618+
) - 1,
5619+
$cell->getRow() - 1
5620+
);
56245621

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

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,7 @@ class Cell implements Stringable
5858
*/
5959
private int $xfIndex = 0;
6060

61-
/**
62-
* Attributes of the formula.
63-
*/
64-
private mixed $formulaAttributes = null;
61+
private ?array $formulaAttributes = null;
6562

6663
private IgnoredErrors $ignoredErrors;
6764

@@ -801,22 +798,14 @@ public function setXfIndex(int $indexValue): self
801798
return $this->updateInCollection();
802799
}
803800

804-
/**
805-
* Set the formula attributes.
806-
*
807-
* @return $this
808-
*/
809-
public function setFormulaAttributes(mixed $attributes): self
801+
public function setFormulaAttributes(?array $attributes): self
810802
{
811803
$this->formulaAttributes = $attributes;
812804

813805
return $this;
814806
}
815807

816-
/**
817-
* Get the formula attributes.
818-
*/
819-
public function getFormulaAttributes(): mixed
808+
public function getFormulaAttributes(): ?array
820809
{
821810
return $this->formulaAttributes;
822811
}

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -843,6 +843,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
843843
}
844844

845845
// Read cell!
846+
$useFormula = isset($c->f)
847+
&& ((string) $c->f !== '' || (isset($c->f->attributes()['t'])
848+
&& strtolower((string) $c->f->attributes()['t']) === 'shared'));
846849
switch ($cellDataType) {
847850
case 's':
848851
if ((string) $c->v != '') {
@@ -867,10 +870,16 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
867870
} else {
868871
// Formula
869872
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
870-
if (isset($c->f['t'])) {
871-
$att = $c->f;
872-
$docSheet->getCell($r)->setFormulaAttributes($att);
873-
}
873+
self::storeFormulaAttributes($c->f, $docSheet, $r);
874+
}
875+
876+
break;
877+
case 'str':
878+
if ($useFormula) {
879+
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
880+
self::storeFormulaAttributes($c->f, $docSheet, $r);
881+
} else {
882+
$value = self::castToString($c);
874883
}
875884

876885
break;
@@ -897,10 +906,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
897906
} else {
898907
// Formula
899908
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
900-
if (isset($c->f['t'])) {
901-
$attributes = $c->f['t'];
902-
$docSheet->getCell($r)->setFormulaAttributes(['t' => (string) $attributes]);
909+
if (is_numeric($calculatedValue)) {
910+
$calculatedValue += 0;
903911
}
912+
self::storeFormulaAttributes($c->f, $docSheet, $r);
904913
}
905914

906915
break;
@@ -2365,4 +2374,19 @@ private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet):
23652374
}
23662375
}
23672376
}
2377+
2378+
private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
2379+
{
2380+
$formulaAttributes = [];
2381+
$attributes = $f->attributes();
2382+
if (isset($attributes['t'])) {
2383+
$formulaAttributes['t'] = (string) $attributes['t'];
2384+
}
2385+
if (isset($attributes['ref'])) {
2386+
$formulaAttributes['ref'] = (string) $attributes['ref'];
2387+
}
2388+
if (!empty($formulaAttributes)) {
2389+
$docSheet->getCell($r)->setFormulaAttributes($formulaAttributes);
2390+
}
2391+
}
23682392
}

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,12 @@
66
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
77
use PhpOffice\PhpSpreadsheet\Document\Properties;
88
use PhpOffice\PhpSpreadsheet\Document\Security;
9-
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
109
use PhpOffice\PhpSpreadsheet\Shared\Date;
11-
use PhpOffice\PhpSpreadsheet\Shared\File;
1210
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1311
use PhpOffice\PhpSpreadsheet\Style\Style;
1412
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
1513
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
1614
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
17-
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
1815

1916
class Spreadsheet implements JsonSerializable
2017
{
@@ -1051,17 +1048,7 @@ public function getWorksheetIterator(): Iterator
10511048
*/
10521049
public function copy(): self
10531050
{
1054-
$filename = File::temporaryFilename();
1055-
$writer = new XlsxWriter($this);
1056-
$writer->setIncludeCharts(true);
1057-
$writer->save($filename);
1058-
1059-
$reader = new XlsxReader();
1060-
$reader->setIncludeCharts(true);
1061-
$reloadedSpreadsheet = $reader->load($filename);
1062-
unlink($filename);
1063-
1064-
return $reloadedSpreadsheet;
1051+
return unserialize(serialize($this));
10651052
}
10661053

10671054
public function __clone()
@@ -1525,14 +1512,6 @@ public function reevaluateAutoFilters(bool $resetToMax): void
15251512
}
15261513
}
15271514

1528-
/**
1529-
* @throws Exception
1530-
*/
1531-
public function __serialize(): array
1532-
{
1533-
throw new Exception('Spreadsheet objects cannot be serialized');
1534-
}
1535-
15361515
/**
15371516
* @throws Exception
15381517
*/

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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
$spreadsheet = unserialize($ser);
92+
self::assertInstanceOf(Spreadsheet::class, $spreadsheet);
93+
$this->spreadsheet = $spreadsheet;
94+
$sheet = $this->spreadsheet->getActiveSheet();
95+
self::assertSame('=SUM(summedcells)', $sheet->getCell('C1')->getValue());
96+
self::assertSame(15, $sheet->getCell('C1')->getCalculatedValue());
97+
}
98+
}

tests/PhpSpreadsheetTests/SpreadsheetTest.php

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

tests/PhpSpreadsheetTests/Worksheet/CloneTest.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@ public function testGetCloneIndex(): void
5555

5656
public function testSerialize1(): void
5757
{
58-
// If worksheet attached to spreadsheet, can't serialize it.
59-
$this->expectException(SpreadsheetException::class);
60-
$this->expectExceptionMessage('cannot be serialized');
6158
$sheet1 = $this->spreadsheet->getActiveSheet();
62-
serialize($sheet1);
59+
$sheet1->getCell('A1')->setValue(10);
60+
$serialized = serialize($sheet1);
61+
$newSheet = unserialize($serialized);
62+
self::assertInstanceOf(Worksheet::class, $newSheet);
63+
self::assertSame(10, $newSheet->getCell('A1')->getValue());
64+
self::assertNotEquals($newSheet->getHashInt(), $sheet1->getHashInt());
65+
self::assertNotNull($newSheet->getParent());
66+
self::assertNotSame($newSheet->getParent(), $sheet1->getParent());
67+
$newSheet->getParent()->disconnectWorksheets();
6368
}
6469

6570
public function testSerialize2(): void

0 commit comments

Comments
 (0)