Skip to content

Commit ffbbf61

Browse files
authored
Allow Spreadsheet Serialization Branch release129 (#4405)
1 parent 7628348 commit ffbbf61

File tree

9 files changed

+156
-82
lines changed

9 files changed

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

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

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,6 @@ class Calculation
152152
*/
153153
public $formulaError;
154154

155-
/**
156-
* Reference Helper.
157-
*
158-
* @var ReferenceHelper
159-
*/
160-
private static $referenceHelper;
161-
162155
/**
163156
* An array of the nested cell references accessed by the calculation engine, used for the debug log.
164157
*
@@ -2925,7 +2918,6 @@ public function __construct(?Spreadsheet $spreadsheet = null)
29252918
$this->cyclicReferenceStack = new CyclicReferenceStack();
29262919
$this->debugLog = new Logger($this->cyclicReferenceStack);
29272920
$this->branchPruner = new BranchPruner($this->branchPruningEnabled);
2928-
self::$referenceHelper = ReferenceHelper::getInstance();
29292921
}
29302922

29312923
private static function loadLocales(): void
@@ -5730,11 +5722,14 @@ private function evaluateDefinedName(Cell $cell, DefinedName $namedRange, Worksh
57305722
$recursiveCalculationCellAddress = $recursiveCalculationCell->getCoordinate();
57315723

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

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

src/PhpSpreadsheet/Cell/Cell.php

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class Cell
6767
/**
6868
* Attributes of the formula.
6969
*
70-
* @var mixed
70+
* @var ?array
7171
*/
7272
private $formulaAttributes;
7373

@@ -767,26 +767,14 @@ public function setXfIndex(int $indexValue): self
767767
return $this->updateInCollection();
768768
}
769769

770-
/**
771-
* Set the formula attributes.
772-
*
773-
* @param mixed $attributes
774-
*
775-
* @return $this
776-
*/
777-
public function setFormulaAttributes($attributes): self
770+
public function setFormulaAttributes(?array $attributes): self
778771
{
779772
$this->formulaAttributes = $attributes;
780773

781774
return $this;
782775
}
783776

784-
/**
785-
* Get the formula attributes.
786-
*
787-
* @return mixed
788-
*/
789-
public function getFormulaAttributes()
777+
public function getFormulaAttributes(): ?array
790778
{
791779
return $this->formulaAttributes;
792780
}

src/PhpSpreadsheet/Reader/Xlsx.php

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

860860
// Read cell!
861+
$useFormula = isset($c->f)
862+
&& ((string) $c->f !== '' || (isset($c->f->attributes()['t'])
863+
&& strtolower((string) $c->f->attributes()['t']) === 'shared'));
861864
switch ($cellDataType) {
862865
case 's':
863866
if ((string) $c->v != '') {
@@ -882,10 +885,16 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
882885
} else {
883886
// Formula
884887
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToBoolean');
885-
if (isset($c->f['t'])) {
886-
$att = $c->f;
887-
$docSheet->getCell($r)->setFormulaAttributes($att);
888-
}
888+
self::storeFormulaAttributes($c->f, $docSheet, $r);
889+
}
890+
891+
break;
892+
case 'str':
893+
if ($useFormula) {
894+
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
895+
self::storeFormulaAttributes($c->f, $docSheet, $r);
896+
} else {
897+
$value = self::castToString($c);
889898
}
890899

891900
break;
@@ -912,10 +921,10 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
912921
} else {
913922
// Formula
914923
$this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, 'castToString');
915-
if (isset($c->f['t'])) {
916-
$attributes = $c->f['t'];
917-
$docSheet->getCell($r)->setFormulaAttributes(['t' => (string) $attributes]);
924+
if (is_numeric($calculatedValue)) {
925+
$calculatedValue += 0;
918926
}
927+
self::storeFormulaAttributes($c->f, $docSheet, $r);
919928
}
920929

921930
break;
@@ -2325,4 +2334,19 @@ private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet):
23252334
}
23262335
}
23272336
}
2337+
2338+
private static function storeFormulaAttributes(SimpleXMLElement $f, Worksheet $docSheet, string $r): void
2339+
{
2340+
$formulaAttributes = [];
2341+
$attributes = $f->attributes();
2342+
if (isset($attributes['t'])) {
2343+
$formulaAttributes['t'] = (string) $attributes['t'];
2344+
}
2345+
if (isset($attributes['ref'])) {
2346+
$formulaAttributes['ref'] = (string) $attributes['ref'];
2347+
}
2348+
if (!empty($formulaAttributes)) {
2349+
$docSheet->getCell($r)->setFormulaAttributes($formulaAttributes);
2350+
}
2351+
}
23282352
}

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44

55
use JsonSerializable;
66
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
7-
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
8-
use PhpOffice\PhpSpreadsheet\Shared\File;
97
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
108
use PhpOffice\PhpSpreadsheet\Style\Style;
119
use PhpOffice\PhpSpreadsheet\Worksheet\Iterator;
1210
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
13-
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
1411

1512
class Spreadsheet implements JsonSerializable
1613
{
@@ -1161,17 +1158,7 @@ public function getWorksheetIterator()
11611158
*/
11621159
public function copy()
11631160
{
1164-
$filename = File::temporaryFilename();
1165-
$writer = new XlsxWriter($this);
1166-
$writer->setIncludeCharts(true);
1167-
$writer->save($filename);
1168-
1169-
$reader = new XlsxReader();
1170-
$reader->setIncludeCharts(true);
1171-
$reloadedSpreadsheet = $reader->load($filename);
1172-
unlink($filename);
1173-
1174-
return $reloadedSpreadsheet;
1161+
return unserialize(serialize($this));
11751162
}
11761163

11771164
public function __clone()
@@ -1663,16 +1650,6 @@ public function getSharedComponent(): Style
16631650
return new Style();
16641651
}
16651652

1666-
/**
1667-
* @throws Exception
1668-
*
1669-
* @return mixed
1670-
*/
1671-
public function __serialize()
1672-
{
1673-
throw new Exception('Spreadsheet objects cannot be serialized');
1674-
}
1675-
16761653
/**
16771654
* @throws Exception
16781655
*/

src/PhpSpreadsheet/Worksheet/Worksheet.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,6 @@ public function __destruct()
420420
public function __wakeup(): void
421421
{
422422
$this->hash = spl_object_id($this);
423-
$this->parent = null;
424423
}
425424

426425
/**
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
@@ -275,22 +275,4 @@ public function testAddExternalRowDimensionStyles(): void
275275
self::assertEquals($countXfs + $index, $sheet3->getCell('A2')->getXfIndex());
276276
self::assertEquals($countXfs + $index, $sheet3->getRowDimension(2)->getXfIndex());
277277
}
278-
279-
public function testNotSerializable(): void
280-
{
281-
$this->spreadsheet = new Spreadsheet();
282-
283-
$this->expectException(Exception::class);
284-
$this->expectExceptionMessage('Spreadsheet objects cannot be serialized');
285-
serialize($this->spreadsheet);
286-
}
287-
288-
public function testNotJsonEncodable(): void
289-
{
290-
$this->spreadsheet = new Spreadsheet();
291-
292-
$this->expectException(Exception::class);
293-
$this->expectExceptionMessage('Spreadsheet objects cannot be json encoded');
294-
json_encode($this->spreadsheet);
295-
}
296278
}

tests/PhpSpreadsheetTests/Worksheet/CloneTest.php

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,18 @@ 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

0 commit comments

Comments
 (0)