Skip to content

Commit 7216e80

Browse files
committed
Add support for reading and writing formulas
1 parent b980e29 commit 7216e80

File tree

3 files changed

+107
-11
lines changed

3 files changed

+107
-11
lines changed

README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,22 @@
33
PHP library to make basic but fast read & write operations on existing Excel workbooks.
44

55
It handles XLSX/XLSM documents (Microsoft Excel 2007+, Office Open XML Workbook) using fast and simple low-level ZIP & XML manipulations,
6-
without requiring any library dependency.
6+
without requiring any library dependency, while minimising unintended side-effects.
77

88
## Rationale
99

10-
If you need advanced manipulation of Excel documents such as working with formulas and styles,
10+
If you need advanced manipulation of Excel documents such as working with styles,
1111
check the [PhpSpreadsheet](https://github.com/PHPOffice/PhpSpreadsheet) library
1212
(previously [PHPExcel](https://github.com/PHPOffice/PHPExcel/)),
13-
but for simply reading & writing basic values from existing Excel workbooks, `PhpSpreadsheet` is over an order of magnitude too slow.
13+
but for simply reading & writing basic values from existing Excel workbooks, `PhpSpreadsheet` is over an order of magnitude too slow,
14+
and has the risk of breaking some unsupported Excel features such as notes.
1415

1516
There are also libraries to create new Excel documents from scratch, or for just reading some values, but not any obvious one for editing.
1617

17-
`php-xlsx-fast-editor` addresses the need of quickly reading & writing & editing existing Excel documents.
18+
`php-xlsx-fast-editor` addresses the need of quickly reading & writing & editing existing Excel documents,
19+
while reducing the risk of breaking anything.
20+
21+
Note that to create a new document, you can just provide a blank Excel document as input.
1822

1923
## Use
2024

@@ -43,13 +47,15 @@ $xlsxFastEditor = new XlsxFastEditor('test.xlsx');
4347
$worksheetId1 = $xlsxFastEditor->getWorksheetNumber('Sheet1');
4448
$worksheetId2 = $xlsxFastEditor->getWorksheetNumber('Sheet2');
4549

50+
$fx = $xlsxFastEditor->readFormula($worksheetId1, 'A1');
4651
$f = $xlsxFastEditor->readFloat($worksheetId1, 'B2');
4752
$i = $xlsxFastEditor->readInt($worksheetId1, 'C3');
4853
$s = $xlsxFastEditor->readString($worksheetId2, 'D4');
4954

5055
// If you want to force Excel to recalculate formulas on next load:
5156
$xlsxFastEditor->setFullCalcOnLoad($worksheetId2, true);
5257

58+
$xlsxFastEditor->writeFormula($worksheetId1, 'A1', '=B2*3');
5359
$xlsxFastEditor->writeFloat($worksheetId1, 'B2', 3.14);
5460
$xlsxFastEditor->writeInt($worksheetId1, 'C3', 13);
5561
$xlsxFastEditor->writeString($worksheetId2, 'D4', 'Hello');

src/XlsxFastEditor.php

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,12 @@ private function getDomFromPath(string $path): \DOMDocument
157157
}
158158

159159
/**
160-
* Access the DOMElement representing a cell value `<v>` in the worksheet.
160+
* Access the DOMElement representing a cell formula `<f>` in the worksheet.
161161
*
162162
* @param int $sheetNumber Worksheet number (base 1)
163163
* @param $cellName Cell name such as `B4`
164164
*/
165-
private function getV(int $sheetNumber, string $cellName): ?\DOMElement
165+
private function getF(int $sheetNumber, string $cellName): ?\DOMElement
166166
{
167167
if (!ctype_alnum($cellName)) {
168168
throw new XlsxFastEditorInputException("Invalid cell reference {$cellName}! ");
@@ -173,11 +173,49 @@ private function getV(int $sheetNumber, string $cellName): ?\DOMElement
173173
$xpath = new \DOMXPath($dom);
174174
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
175175

176+
$f = null;
177+
$fs = $xpath->query("(//o:c[@r='$cellName'])[1]/o:f");
178+
if ($fs !== false && $fs->length > 0) {
179+
$f = $fs[0];
180+
if (!($f instanceof \DOMElement)) {
181+
throw new XlsxFastEditorXmlException("Error querying XML fragment for cell formula {$sheetNumber}/{$cellName}!");
182+
}
183+
}
184+
return $f;
185+
}
186+
187+
/**
188+
* Read a formula in the given worksheet at the given cell location.
189+
*
190+
* @param int $sheetNumber Worksheet number (base 1)
191+
* @param $cellName Cell name such as `B4`
192+
*/
193+
public function readFormula(int $sheetNumber, string $cellName): ?string
194+
{
195+
$f = $this->getF($sheetNumber, $cellName);
196+
if ($f === null || !is_string($f->nodeValue) || $f->nodeValue === '') {
197+
return null;
198+
}
199+
return '=' . $f->nodeValue;
200+
}
201+
202+
/**
203+
* Access the DOMElement representing a cell value `<v>` in the worksheet.
204+
*
205+
* @param int $sheetNumber Worksheet number (base 1)
206+
* @param $cellName Cell name such as `B4`
207+
*/
208+
private function getV(int $sheetNumber, string $cellName): ?\DOMElement
209+
{
176210
if (!ctype_alnum($cellName)) {
177211
throw new XlsxFastEditorInputException("Invalid cell reference {$cellName}! ");
178212
}
179213
$cellName = strtoupper($cellName);
180214

215+
$dom = $this->getDomFromPath(self::getWorksheetPath($sheetNumber));
216+
$xpath = new \DOMXPath($dom);
217+
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
218+
181219
$v = null;
182220
$vs = $xpath->query("(//o:c[@r='$cellName'])[1]/o:v");
183221
if ($vs !== false && $vs->length > 0) {
@@ -438,6 +476,49 @@ private function initCellValue(\DOMElement $c): \DOMElement
438476
return $v;
439477
}
440478

479+
/**
480+
* Write a formulat in the given worksheet at the given cell location, without changing the type/style of the cell.
481+
* Removes the formulas of the cell, if any.
482+
*
483+
* @param int $sheetNumber Worksheet number (base 1)
484+
* @param $cellName Cell name such as `B4`
485+
*/
486+
public function writeFormula(int $sheetNumber, string $cellName, string $value): void
487+
{
488+
$c = $this->getCell($sheetNumber, $cellName, true);
489+
if ($c === null) {
490+
throw new XlsxFastEditorInputException("Internal error accessing cell {$sheetNumber}/{$cellName}!");
491+
}
492+
493+
$value = ltrim($value, '=');
494+
495+
$vs = $c->getElementsByTagName('v');
496+
for ($i = $vs->length - 1; $i >= 0; $i--) {
497+
$v = $vs[$i];
498+
if ($v instanceof \DOMElement) {
499+
$c->removeChild($v);
500+
}
501+
}
502+
503+
$fs = $c->getElementsByTagName('f');
504+
for ($i = $fs->length - 1; $i >= 0; $i--) {
505+
$f = $fs[$i];
506+
if ($f instanceof \DOMElement) {
507+
$c->removeChild($f);
508+
}
509+
}
510+
511+
$dom = $c->ownerDocument;
512+
if ($dom === null) {
513+
throw new XlsxFastEditorInputException("Internal error accessing cell {$sheetNumber}/{$cellName}!");
514+
}
515+
$f = $dom->createElement('f', $value);
516+
$c->appendChild($f);
517+
518+
$this->mustClearCalcChain = true;
519+
$this->touchWorksheet($sheetNumber);
520+
}
521+
441522
/**
442523
* Write a number in the given worksheet at the given cell location, without changing the type/style of the cell.
443524
* Removes the formulas of the cell, if any.

tests/test.php

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,41 @@
1818

1919
assert($xlsxFastEditor->readFloat($sheet1, 'D2') === 3.14159);
2020
assert($xlsxFastEditor->readFloat($sheet1, 'D4') === -1.0);
21-
assert($xlsxFastEditor->readFloat($sheet1, 'E5') === null);
21+
assert($xlsxFastEditor->readFloat($sheet1, 'e5') === null);
2222
assert($xlsxFastEditor->readInt($sheet1, 'c3') === -5);
2323
assert($xlsxFastEditor->readInt($sheet1, 'F6') === null);
24-
assert($xlsxFastEditor->readString($sheet1, 'B4') === 'naïveté');
24+
assert($xlsxFastEditor->readString($sheet1, 'b4') === 'naïveté');
2525
assert($xlsxFastEditor->readString($sheet1, 'F7') === null);
2626

2727
$sheet2 = $xlsxFastEditor->getWorksheetNumber('Sheet2');
2828
assert($sheet2 === 2);
2929

30+
assert($xlsxFastEditor->readFormula($sheet2, 'c2') === '=Sheet1!C2*2');
3031
assert($xlsxFastEditor->readFloat($sheet2, 'D2') === 3.14159 * 2);
3132
assert($xlsxFastEditor->readFloat($sheet2, 'D4') === -1.0 * 2);
3233
assert($xlsxFastEditor->readInt($sheet2, 'c3') === -5 * 2);
3334
assert($xlsxFastEditor->readString($sheet2, 'B3') === 'déjà-vu');
3435

3536
// Existing cells
36-
$xlsxFastEditor->writeString($sheet1, 'B4', 'α');
37-
$xlsxFastEditor->writeInt($sheet1, 'C4', 15);
38-
$xlsxFastEditor->writeFloat($sheet1, 'D4', -66.6);
37+
$xlsxFastEditor->writeFormula($sheet1, 'c2', '=2*3');
38+
$xlsxFastEditor->writeString($sheet1, 'b4', 'α');
39+
$xlsxFastEditor->writeInt($sheet1, 'c4', 15);
40+
$xlsxFastEditor->writeFloat($sheet1, 'd4', -66.6);
3941

4042
// Existing cells with formulas
43+
$xlsxFastEditor->writeFormula($sheet2, 'c2', '=Sheet1!C2*3');
4144
$xlsxFastEditor->writeString($sheet2, 'B3', 'β');
4245
$xlsxFastEditor->writeInt($sheet2, 'C3', -7);
4346
$xlsxFastEditor->writeFloat($sheet2, 'D3', 273.15);
4447

4548
// Non-existing cells but existing lines
49+
$xlsxFastEditor->writeFormula($sheet2, 'I2', '=7*3');
4650
$xlsxFastEditor->writeString($sheet2, 'F2', 'γ');
4751
$xlsxFastEditor->writeInt($sheet2, 'G3', -7);
4852
$xlsxFastEditor->writeFloat($sheet2, 'H4', 273.15);
4953

5054
// Non-existing lines
55+
$xlsxFastEditor->writeFormula($sheet2, 'E11', '=7*5');
5156
$xlsxFastEditor->writeString($sheet2, 'B10', 'δ');
5257
$xlsxFastEditor->writeInt($sheet2, 'C9', 13);
5358
$xlsxFastEditor->writeFloat($sheet2, 'D10', -273.15);
@@ -58,18 +63,22 @@
5863

5964
$xlsxFastEditor = new XlsxFastEditor(__DIR__ . '/copy.xlsx');
6065

66+
assert($xlsxFastEditor->readFormula($sheet1, 'c2') === '=2*3');
6167
assert($xlsxFastEditor->readString($sheet1, 'B4') === 'α');
6268
assert($xlsxFastEditor->readInt($sheet1, 'C4') === 15);
6369
assert($xlsxFastEditor->readFloat($sheet1, 'D4') === -66.6);
6470

71+
assert($xlsxFastEditor->readFormula($sheet2, 'c2') === '=Sheet1!C2*3');
6572
assert($xlsxFastEditor->readString($sheet2, 'B3') === 'β');
6673
assert($xlsxFastEditor->readInt($sheet2, 'C3') === -7);
6774
assert($xlsxFastEditor->readFloat($sheet2, 'D3') === 273.15);
6875

76+
assert($xlsxFastEditor->readFormula($sheet2, 'I2') === '=7*3');
6977
assert($xlsxFastEditor->readString($sheet2, 'F2') === 'γ');
7078
assert($xlsxFastEditor->readInt($sheet2, 'G3') === -7);
7179
assert($xlsxFastEditor->readFloat($sheet2, 'H4') === 273.15);
7280

81+
assert($xlsxFastEditor->readFormula($sheet2, 'E11') === '=7*5');
7382
assert($xlsxFastEditor->readString($sheet2, 'B10') === 'δ');
7483
assert($xlsxFastEditor->readInt($sheet2, 'C9') === 13);
7584
assert($xlsxFastEditor->readFloat($sheet2, 'D10') === -273.15);

0 commit comments

Comments
 (0)