Skip to content

Commit 126309f

Browse files
authored
Allow Spreadsheet Serialization Branch release210 (#4406)
1 parent 99f5c18 commit 126309f

File tree

9 files changed

+155
-74
lines changed

9 files changed

+155
-74
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 #4406](https://github.com/PHPOffice/PhpSpreadsheet/pull/4406)
17+
1418
### Fixed
1519

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

src/PhpSpreadsheet/Calculation/Calculation.php

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

130-
/**
131-
* Reference Helper.
132-
*/
133-
private static ReferenceHelper $referenceHelper;
134-
135130
/**
136131
* An array of the nested cell references accessed by the calculation engine, used for the debug log.
137132
*/
@@ -2880,7 +2875,6 @@ public function __construct(?Spreadsheet $spreadsheet = null)
28802875
$this->cyclicReferenceStack = new CyclicReferenceStack();
28812876
$this->debugLog = new Logger($this->cyclicReferenceStack);
28822877
$this->branchPruner = new BranchPruner($this->branchPruningEnabled);
2883-
self::$referenceHelper = ReferenceHelper::getInstance();
28842878
}
28852879

28862880
private static function loadLocales(): void
@@ -5608,11 +5602,14 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
56085602
$recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate();
56095603

56105604
// Adjust relative references in ranges and formulae so that we execute the calculation for the correct rows and columns
5611-
$definedNameValue = self::$referenceHelper->updateFormulaReferencesAnyWorksheet(
5612-
$definedNameValue,
5613-
Coordinate::columnIndexFromString($cell->getColumn()) - 1,
5614-
$cell->getRow() - 1
5615-
);
5605+
$definedNameValue = ReferenceHelper::getInstance()
5606+
->updateFormulaReferencesAnyWorksheet(
5607+
$definedNameValue,
5608+
Coordinate::columnIndexFromString(
5609+
$cell->getColumn()
5610+
) - 1,
5611+
$cell->getRow() - 1
5612+
);
56165613

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

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class Cell implements Stringable
6262
/**
6363
* Attributes of the formula.
6464
*/
65-
private mixed $formulaAttributes = null;
65+
private ?array $formulaAttributes = null;
6666

6767
private IgnoredErrors $ignoredErrors;
6868

@@ -765,22 +765,14 @@ public function setXfIndex(int $indexValue): self
765765
return $this->updateInCollection();
766766
}
767767

768-
/**
769-
* Set the formula attributes.
770-
*
771-
* @return $this
772-
*/
773-
public function setFormulaAttributes(mixed $attributes): self
768+
public function setFormulaAttributes(?array $attributes): self
774769
{
775770
$this->formulaAttributes = $attributes;
776771

777772
return $this;
778773
}
779774

780-
/**
781-
* Get the formula attributes.
782-
*/
783-
public function getFormulaAttributes(): mixed
775+
public function getFormulaAttributes(): ?array
784776
{
785777
return $this->formulaAttributes;
786778
}

src/PhpSpreadsheet/Reader/Xlsx.php

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

840840
// Read cell!
841+
$useFormula = isset($c->f)
842+
&& ((string) $c->f !== '' || (isset($c->f->attributes()['t'])
843+
&& strtolower((string) $c->f->attributes()['t']) === 'shared'));
841844
switch ($cellDataType) {
842845
case 's':
843846
if ((string) $c->v != '') {
@@ -862,10 +865,16 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
862865
} else {
863866
// Formula
864867
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
865-
if (isset($c->f['t'])) {
866-
$att = $c->f;
867-
$docSheet->getCell($r)->setFormulaAttributes($att);
868-
}
868+
self::storeFormulaAttributes($c->f, $docSheet, $r);
869+
}
870+
871+
break;
872+
case 'str':
873+
if ($useFormula) {
874+
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
875+
self::storeFormulaAttributes($c->f, $docSheet, $r);
876+
} else {
877+
$value = self::castToString($c);
869878
}
870879

871880
break;
@@ -892,10 +901,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
892901
} else {
893902
// Formula
894903
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
895-
if (isset($c->f['t'])) {
896-
$attributes = $c->f['t'];
897-
$docSheet->getCell($r)->setFormulaAttributes(['t' => (string) $attributes]);
904+
if (is_numeric($calculatedValue)) {
905+
$calculatedValue += 0;
898906
}
907+
self::storeFormulaAttributes($c->f, $docSheet, $r);
899908
}
900909

901910
break;
@@ -2349,4 +2358,19 @@ private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet):
23492358
}
23502359
}
23512360
}
2361+
2362+
private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
2363+
{
2364+
$formulaAttributes = [];
2365+
$attributes = $f->attributes();
2366+
if (isset($attributes['t'])) {
2367+
$formulaAttributes['t'] = (string) $attributes['t'];
2368+
}
2369+
if (isset($attributes['ref'])) {
2370+
$formulaAttributes['ref'] = (string) $attributes['ref'];
2371+
}
2372+
if (!empty($formulaAttributes)) {
2373+
$docSheet->getCell($r)->setFormulaAttributes($formulaAttributes);
2374+
}
2375+
}
23522376
}

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@
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;
10-
use PhpOffice\PhpSpreadsheet\Shared\File;
119
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1210
use PhpOffice\PhpSpreadsheet\Style\Style;
1311
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
1412
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
1513
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
16-
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
1714

1815
class Spreadsheet implements JsonSerializable
1916
{
@@ -1045,17 +1042,7 @@ public function getWorksheetIterator(): Iterator
10451042
*/
10461043
public function copy(): self
10471044
{
1048-
$filename = File::temporaryFilename();
1049-
$writer = new XlsxWriter($this);
1050-
$writer->setIncludeCharts(true);
1051-
$writer->save($filename);
1052-
1053-
$reader = new XlsxReader();
1054-
$reader->setIncludeCharts(true);
1055-
$reloadedSpreadsheet = $reader->load($filename);
1056-
unlink($filename);
1057-
1058-
return $reloadedSpreadsheet;
1045+
return unserialize(serialize($this));
10591046
}
10601047

10611048
public function __clone()
@@ -1519,14 +1506,6 @@ public function reevaluateAutoFilters(bool $resetToMax): void
15191506
}
15201507
}
15211508

1522-
/**
1523-
* @throws Exception
1524-
*/
1525-
public function __serialize(): array
1526-
{
1527-
throw new Exception('Spreadsheet objects cannot be serialized');
1528-
}
1529-
15301509
/**
15311510
* @throws Exception
15321511
*/

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

tests/PhpSpreadsheetTests/SpreadsheetTest.php

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

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)