Skip to content

Commit ef176f3

Browse files
committed
Excel Handle Array Functions as Dynamic Rather than CSE
With a number of changes, PhpSpreadsheet can finally generate a spreadsheet which Excel will recognize as a Dynamic Array function rather than CSE. In particular, changes are needed to ContentTypes, workbook.xml.rels, cell definitions in the worksheet, and a new metadata.xml is added.
1 parent 3daac0a commit ef176f3

File tree

7 files changed

+265
-3
lines changed

7 files changed

+265
-3
lines changed

src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class Namespaces
8282

8383
const CONTENT_TYPES = 'http://schemas.openxmlformats.org/package/2006/content-types';
8484

85+
const RELATIONSHIPS_METADATA = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata';
86+
8587
const RELATIONSHIPS_PRINTER_SETTINGS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings';
8688

8789
const RELATIONSHIPS_TABLE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table';
@@ -115,4 +117,6 @@ class Namespaces
115117
const PURL_CHART = 'http://purl.oclc.org/ooxml/drawingml/chart';
116118

117119
const PURL_WORKSHEET = 'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet';
120+
121+
const DYNAMIC_ARRAY = 'http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray';
118122
}

src/PhpSpreadsheet/Writer/Xlsx.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class Xlsx extends BaseWriter
136136

137137
private bool $explicitStyle0 = false;
138138

139+
private bool $useCSEArrays = false;
140+
141+
private bool $useDynamicArray = false;
142+
139143
/**
140144
* Create a new Xlsx Writer.
141145
*/
@@ -247,6 +251,7 @@ public function getWriterPartWorksheet(): Worksheet
247251
public function save($filename, int $flags = 0): void
248252
{
249253
$this->processFlags($flags);
254+
$this->useDynamicArray = $this->preCalculateFormulas && Calculation::getInstance($this->spreadSheet)->getArrayReturnType() === Calculation::RETURN_ARRAY_AS_ARRAY && !$this->useCSEArrays;
250255

251256
// garbage collect
252257
$this->pathNames = [];
@@ -277,6 +282,10 @@ public function save($filename, int $flags = 0): void
277282
$zipContent = [];
278283
// Add [Content_Types].xml to ZIP file
279284
$zipContent['[Content_Types].xml'] = $this->getWriterPartContentTypes()->writeContentTypes($this->spreadSheet, $this->includeCharts);
285+
if ($this->useDynamicArrays()) {
286+
$writerPartMetadata = new Xlsx\Metadata($this);
287+
$zipContent['xl/metadata.xml'] = $writerPartMetadata->writeMetadata();
288+
}
280289

281290
//if hasMacros, add the vbaProject.bin file, Certificate file(if exists)
282291
if ($this->spreadSheet->hasMacros()) {
@@ -711,4 +720,14 @@ public function setExplicitStyle0(bool $explicitStyle0): self
711720

712721
return $this;
713722
}
723+
724+
public function setUseCSEArrays(bool $useCSEArrays): void
725+
{
726+
$this->useCSEArrays = $useCSEArrays;
727+
}
728+
729+
public function useDynamicArrays(): bool
730+
{
731+
return $this->useDynamicArray;
732+
}
714733
}

src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,11 @@ public function writeContentTypes(Spreadsheet $spreadsheet, bool $includeCharts
209209
}
210210
}
211211

212+
// Metadata needed for Dynamic Arrays
213+
if ($this->getParentWriter()->useDynamicArrays()) {
214+
$this->writeOverrideContentType($objWriter, '/xl/metadata.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml');
215+
}
216+
212217
$objWriter->endElement();
213218

214219
// Return
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
4+
5+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
6+
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
7+
8+
class Metadata extends WriterPart
9+
{
10+
/**
11+
* Write content types to XML format.
12+
*
13+
* @return string XML Output
14+
*/
15+
public function writeMetadata(): string
16+
{
17+
// Create XML writer
18+
$objWriter = null;
19+
if ($this->getParentWriter()->getUseDiskCaching()) {
20+
$objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
21+
} else {
22+
$objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
23+
}
24+
25+
// XML header
26+
$objWriter->startDocument('1.0', 'UTF-8', 'yes');
27+
28+
// Types
29+
$objWriter->startElement('metadata');
30+
$objWriter->writeAttribute('xmlns', Namespaces::MAIN);
31+
$objWriter->writeAttribute('xmlns:xda', Namespaces::DYNAMIC_ARRAY);
32+
33+
$objWriter->startElement('metadataTypes');
34+
$objWriter->writeAttribute('count', '1');
35+
$objWriter->startElement('metadataType');
36+
$objWriter->writeAttribute('name', 'XLDAPR');
37+
$objWriter->writeAttribute('minSupportedVersion', '120000');
38+
$objWriter->writeAttribute('copy', '1');
39+
$objWriter->writeAttribute('pasteAll', '1');
40+
$objWriter->writeAttribute('pasteValues', '1');
41+
$objWriter->writeAttribute('merge', '1');
42+
$objWriter->writeAttribute('splitFirst', '1');
43+
$objWriter->writeAttribute('rowColShift', '1');
44+
$objWriter->writeAttribute('clearFormats', '1');
45+
$objWriter->writeAttribute('clearComments', '1');
46+
$objWriter->writeAttribute('assign', '1');
47+
$objWriter->writeAttribute('coerce', '1');
48+
$objWriter->writeAttribute('cellMeta', '1');
49+
$objWriter->endElement(); // metadataType
50+
$objWriter->endElement(); // metadataTypes
51+
52+
$objWriter->startElement('futureMetadata');
53+
$objWriter->writeAttribute('name', 'XLDAPR');
54+
$objWriter->writeAttribute('count', '1');
55+
$objWriter->startElement('bk');
56+
$objWriter->startElement('extLst');
57+
$objWriter->startElement('ext');
58+
$objWriter->writeAttribute('uri', '{bdbb8cdc-fa1e-496e-a857-3c3f30c029c3}');
59+
$objWriter->startElement('xda:dynamicArrayProperties');
60+
$objWriter->writeAttribute('fDynamic', '1');
61+
$objWriter->writeAttribute('fCollapsed', '0');
62+
$objWriter->endElement(); // xda:dynamicArrayProperties
63+
$objWriter->endElement(); // ext
64+
$objWriter->endElement(); // extLst
65+
$objWriter->endElement(); // bk
66+
$objWriter->endElement(); // futureMetadata
67+
68+
$objWriter->startElement('cellMetadata');
69+
$objWriter->writeAttribute('count', '1');
70+
$objWriter->startElement('bk');
71+
$objWriter->startElement('rc');
72+
$objWriter->writeAttribute('t', '1');
73+
$objWriter->writeAttribute('v', '0');
74+
$objWriter->endElement(); // rc
75+
$objWriter->endElement(); // bk
76+
$objWriter->endElement(); // cellMetadata
77+
78+
$objWriter->endElement(); // metadata
79+
80+
// Return
81+
return $objWriter->getData();
82+
}
83+
}

src/PhpSpreadsheet/Writer/Xlsx/Rels.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,17 @@ public function writeWorkbookRelationships(Spreadsheet $spreadsheet): string
151151
++$i; //increment i if needed for an another relation
152152
}
153153

154+
// Metadata needed for Dynamic Arrays
155+
if ($this->getParentWriter()->useDynamicArrays()) {
156+
$this->writeRelationShip(
157+
$objWriter,
158+
($i + 1 + 3),
159+
Namespaces::RELATIONSHIPS_METADATA,
160+
'metadata.xml'
161+
);
162+
++$i; //increment i if needed for an another relation
163+
}
164+
154165
$objWriter->endElement();
155166

156167
return $objWriter->getData();

src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,6 +1558,15 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksh
15581558
}
15591559
$objWriter->startElement('c');
15601560
$objWriter->writeAttribute('r', $cellAddress);
1561+
$mappedType = $pCell->getDataType();
1562+
if (strtolower($mappedType) === 'f') {
1563+
if ($this->getParentWriter()->useDynamicArrays()) {
1564+
$tempCalc = $pCell->getCalculatedValue();
1565+
if (is_array($tempCalc)) {
1566+
$objWriter->writeAttribute('cm', '1');
1567+
}
1568+
}
1569+
}
15611570

15621571
// Sheet styles
15631572
if ($xfi) {
@@ -1568,9 +1577,6 @@ private function writeCell(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksh
15681577

15691578
// If cell value is supplied, write cell value
15701579
if ($writeValue) {
1571-
// Map type
1572-
$mappedType = $pCell->getDataType();
1573-
15741580
// Write data depending on its type
15751581
switch (strtolower($mappedType)) {
15761582
case 'inlinestr': // Inline string

tests/PhpSpreadsheetTests/Writer/Xlsx/ArrayFunctionsTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,122 @@ public function testArrayOutput(): void
116116
self::assertSame($expectedSort, $sheet2->getCell('D1')->getCalculatedValue());
117117
$spreadsheet2->disconnectWorksheets();
118118

119+
$file = 'zip://';
120+
$file .= $this->outputFile;
121+
$file .= '#xl/worksheets/sheet1.xml';
122+
$data = file_get_contents($file);
123+
if ($data === false) {
124+
self::fail('Unable to read file');
125+
} else {
126+
self::assertStringContainsString('<c r="C1" cm="1"><f t="array" ref="C1:C15" aca="1" ca="1">_xlfn.UNIQUE(A1:A19)</f><v>41</v></c>', $data, '15 results for UNIQUE');
127+
self::assertStringContainsString('<c r="D1" cm="1"><f t="array" ref="D1:D19" aca="1" ca="1">_xlfn._xlws.SORT(A1:A19)</f><v>26</v></c>', $data, '19 results for SORT');
128+
}
129+
130+
$file = 'zip://';
131+
$file .= $this->outputFile;
132+
$file .= '#xl/metadata.xml';
133+
$data = @file_get_contents($file);
134+
self::assertNotFalse($data, 'metadata.xml should exist');
135+
136+
$file = 'zip://';
137+
$file .= $this->outputFile;
138+
$file .= '#[Content_Types].xml';
139+
$data = file_get_contents($file);
140+
self::assertStringContainsString('metadata', $data);
141+
142+
$file = 'zip://';
143+
$file .= $this->outputFile;
144+
$file .= '#xl/_rels/workbook.xml.rels';
145+
$data = file_get_contents($file);
146+
self::assertStringContainsString('metadata', $data);
147+
}
148+
149+
public function testArrayOutputCSE(): void
150+
{
151+
Calculation::setArrayReturnType(Calculation::RETURN_ARRAY_AS_ARRAY);
152+
$spreadsheet = new Spreadsheet();
153+
$sheet = $spreadsheet->getActiveSheet();
154+
$columnArray = [
155+
[41],
156+
[57],
157+
[51],
158+
[54],
159+
[49],
160+
[43],
161+
[35],
162+
[35],
163+
[44],
164+
[47],
165+
[48],
166+
[26],
167+
[57],
168+
[34],
169+
[61],
170+
[34],
171+
[28],
172+
[29],
173+
[41],
174+
];
175+
$sheet->fromArray($columnArray, 'A1');
176+
$sheet->setCellValue('C1', '=UNIQUE(A1:A19)');
177+
$sheet->setCellValue('D1', '=SORT(A1:A19)');
178+
$writer = new XlsxWriter($spreadsheet);
179+
$this->outputFile = File::temporaryFilename();
180+
$writer->setUseCSEArrays(true);
181+
$writer->save($this->outputFile);
182+
$spreadsheet->disconnectWorksheets();
183+
184+
$reader = new XlsxReader();
185+
$spreadsheet2 = $reader->load($this->outputFile);
186+
$sheet2 = $spreadsheet2->getActiveSheet();
187+
$expectedUnique = [
188+
[41],
189+
[57],
190+
[51],
191+
[54],
192+
[49],
193+
[43],
194+
[35],
195+
[44],
196+
[47],
197+
[48],
198+
[26],
199+
[34],
200+
[61],
201+
[28],
202+
[29],
203+
];
204+
self::assertCount(15, $expectedUnique);
205+
self::assertSame($expectedUnique, $sheet2->getCell('C1')->getCalculatedValue());
206+
for ($row = 2; $row <= 15; ++$row) {
207+
self::assertSame($expectedUnique[$row - 1][0], $sheet2->getCell("C$row")->getCalculatedValue(), "cell C$row");
208+
}
209+
$expectedSort = [
210+
[26],
211+
[28],
212+
[29],
213+
[34],
214+
[34],
215+
[35],
216+
[35],
217+
[41],
218+
[41],
219+
[43],
220+
[44],
221+
[47],
222+
[48],
223+
[49],
224+
[51],
225+
[54],
226+
[57],
227+
[57],
228+
[61],
229+
];
230+
self::assertCount(19, $expectedSort);
231+
self::assertCount(19, $columnArray);
232+
self::assertSame($expectedSort, $sheet2->getCell('D1')->getCalculatedValue());
233+
$spreadsheet2->disconnectWorksheets();
234+
119235
$file = 'zip://';
120236
$file .= $this->outputFile;
121237
$file .= '#xl/worksheets/sheet1.xml';
@@ -126,6 +242,24 @@ public function testArrayOutput(): void
126242
self::assertStringContainsString('<c r="C1"><f t="array" ref="C1:C15" aca="1" ca="1">_xlfn.UNIQUE(A1:A19)</f><v>41</v></c>', $data, '15 results for UNIQUE');
127243
self::assertStringContainsString('<c r="D1"><f t="array" ref="D1:D19" aca="1" ca="1">_xlfn._xlws.SORT(A1:A19)</f><v>26</v></c>', $data, '19 results for SORT');
128244
}
245+
246+
$file = 'zip://';
247+
$file .= $this->outputFile;
248+
$file .= '#xl/metadata.xml';
249+
$data = @file_get_contents($file);
250+
self::assertFalse($data, 'metadata.xml should not exist');
251+
252+
$file = 'zip://';
253+
$file .= $this->outputFile;
254+
$file .= '#[Content_Types].xml';
255+
$data = file_get_contents($file);
256+
self::assertStringNotContainsString('metadata', $data);
257+
258+
$file = 'zip://';
259+
$file .= $this->outputFile;
260+
$file .= '#xl/_rels/workbook.xml.rels';
261+
$data = file_get_contents($file);
262+
self::assertStringNotContainsString('metadata', $data);
129263
}
130264

131265
public function testUnimplementedArrayOutput(): void

0 commit comments

Comments
 (0)