Skip to content

Commit f2c6554

Browse files
committed
Add methods for navigation and iterators
1 parent 1ba99e0 commit f2c6554

File tree

5 files changed

+450
-31
lines changed

5 files changed

+450
-31
lines changed

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ 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/)),
1313
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.
14+
and has the risk of breaking some unsupported Excel features such as some notes and charts.
1515

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

@@ -49,14 +49,47 @@ try {
4949
$worksheetId1 = $xlsxFastEditor->getWorksheetNumber('Sheet1');
5050
$worksheetId2 = $xlsxFastEditor->getWorksheetNumber('Sheet2');
5151

52+
// Direct read access
5253
$fx = $xlsxFastEditor->readFormula($worksheetId1, 'A1');
5354
$f = $xlsxFastEditor->readFloat($worksheetId1, 'B2');
5455
$i = $xlsxFastEditor->readInt($worksheetId1, 'C3');
5556
$s = $xlsxFastEditor->readString($worksheetId2, 'D4');
5657

58+
// Navigation methods for existing rows
59+
$row = $xlsxFastEditor->getFirstRow($worksheetId1);
60+
$row = $xlsxFastEditor->getRow($worksheetId1, 2);
61+
$row = $row->getPreviousRow();
62+
$row = $row->getNextRow();
63+
$row = $xlsxFastEditor->getLastRow($worksheetId1);
64+
65+
// Read methods for rows
66+
$rowNumber = $row->number();
67+
68+
// Navigation methods for existing cells
69+
$cell = $row->getFirstCell();
70+
$cell = $row->getCell('D4');
71+
$cell = $cell->getPreviousCell();
72+
$cell = $cell->getNextCell();
73+
$cell = $row->getLastCell();
74+
75+
// Read methods for cells
76+
$cellName = $cell->name();
77+
$fx = $cell->readFormula();
78+
$f = $cell->readFloat();
79+
$i = $cell->readInt();
80+
$s = $cell->readString();
81+
82+
// Iterators for existing rows and cells
83+
foreach ($xlsxFastEditor->rowsIterator($worksheetId1) as $row) {
84+
foreach ($row->cellsIterator() as $cell) {
85+
// $cell->...
86+
}
87+
}
88+
5789
// If you want to force Excel to recalculate formulas on next load:
5890
$xlsxFastEditor->setFullCalcOnLoad($worksheetId2, true);
5991

92+
// Direct write access
6093
$xlsxFastEditor->writeFormula($worksheetId1, 'A1', '=B2*3');
6194
$xlsxFastEditor->writeFloat($worksheetId1, 'B2', 3.14);
6295
$xlsxFastEditor->writeInt($worksheetId1, 'C3', 13);

src/XlsxFastEditor.php

Lines changed: 121 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*/
2020
final class XlsxFastEditor
2121
{
22-
private const OXML_NAMESPACE = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
22+
public const OXML_NAMESPACE = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main';
2323

2424
private const CALC_CHAIN_CACHE_PATH = 'xl/calcChain.xml';
2525
private const SHARED_STRINGS_PATH = 'xl/sharedStrings.xml';
@@ -130,7 +130,7 @@ public function getWorksheetNumber(string $sheetName): int
130130
$dom = $this->getDomFromPath(self::WORKBOOK_PATH);
131131
$xpath = new \DOMXPath($dom);
132132
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
133-
$sheetId = $xpath->evaluate("normalize-space(//o:sheet[@name='$sheetName'][1]/@sheetId)");
133+
$sheetId = $xpath->evaluate("normalize-space(/o:workbook/o:sheets/o:sheet[@name='$sheetName'][1]/@sheetId)");
134134
if (is_string($sheetId)) {
135135
return (int)$sheetId;
136136
}
@@ -168,6 +168,95 @@ private function getDomFromPath(string $path): \DOMDocument
168168
return $dom;
169169
}
170170

171+
/**
172+
* Get the first existing row of the worksheet.
173+
* @param int $sheetNumber Worksheet number (base 1)
174+
* @return XlsxFastEditorRow|null The first row of the worksheet if there is any row, null otherwise.
175+
*/
176+
public function getFirstRow(int $sheetNumber): ?XlsxFastEditorRow
177+
{
178+
$dom = $this->getDomFromPath(self::getWorksheetPath($sheetNumber));
179+
$xpath = new \DOMXPath($dom);
180+
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
181+
182+
$rs = $xpath->query("/o:worksheet/o:sheetData/o:row[position() = 1]");
183+
if ($rs !== false && $rs->length > 0) {
184+
$r = $rs[0];
185+
if (!($r instanceof \DOMElement)) {
186+
throw new XlsxFastEditorXmlException("Error querying XML fragment for row {$sheetNumber} of worksheet {$sheetNumber}!");
187+
}
188+
return new XlsxFastEditorRow($this, $r);
189+
}
190+
return null;
191+
}
192+
193+
/**
194+
* Get the row of the given number in the given worksheet.
195+
* @param int $sheetNumber Worksheet number (base 1)
196+
* @param int $rowNumber Number (ID) of the row (base 1). Warning: this is not an index (not all rows necessarily exist in a sequence)
197+
* @return XlsxFastEditorRow|null The row of that number in that worksheet if it exists, null otherwise.
198+
*/
199+
public function getRow(int $sheetNumber, int $rowNumber): ?XlsxFastEditorRow
200+
{
201+
$dom = $this->getDomFromPath(self::getWorksheetPath($sheetNumber));
202+
$xpath = new \DOMXPath($dom);
203+
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
204+
205+
$rs = $xpath->query("/o:worksheet/o:sheetData/o:row[@r='{$rowNumber}'][1]");
206+
if ($rs !== false && $rs->length > 0) {
207+
$r = $rs[0];
208+
if (!($r instanceof \DOMElement)) {
209+
throw new XlsxFastEditorXmlException("Error querying XML fragment for row {$sheetNumber} of worksheet {$sheetNumber}!");
210+
}
211+
return new XlsxFastEditorRow($this, $r);
212+
}
213+
return null;
214+
}
215+
216+
/**
217+
* Get the last existing row of the worksheet.
218+
* @param int $sheetNumber Worksheet number (base 1)
219+
* @return XlsxFastEditorRow|null The last row of the worksheet if there is any row, null otherwise.
220+
*/
221+
public function getLastRow(int $sheetNumber): ?XlsxFastEditorRow
222+
{
223+
$dom = $this->getDomFromPath(self::getWorksheetPath($sheetNumber));
224+
$xpath = new \DOMXPath($dom);
225+
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
226+
227+
$rs = $xpath->query("/o:worksheet/o:sheetData/o:row[position() = last()]");
228+
if ($rs !== false && $rs->length > 0) {
229+
$r = $rs[0];
230+
if (!($r instanceof \DOMElement)) {
231+
throw new XlsxFastEditorXmlException("Error querying XML fragment for row {$sheetNumber} of worksheet {$sheetNumber}!");
232+
}
233+
return new XlsxFastEditorRow($this, $r);
234+
}
235+
return null;
236+
}
237+
238+
/**
239+
* To iterate over the all the rows of a given worksheet.
240+
* @return \Traversable<XlsxFastEditorRow>
241+
*/
242+
public function rowsIterator(int $sheetNumber): \Traversable
243+
{
244+
$dom = $this->getDomFromPath(self::getWorksheetPath($sheetNumber));
245+
$xpath = new \DOMXPath($dom);
246+
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
247+
248+
$rs = $xpath->query("/o:worksheet/o:sheetData/o:row");
249+
if ($rs !== false) {
250+
for ($i = 0; $i < $rs->length; $i++) {
251+
$r = $rs[$i];
252+
if (!($r instanceof \DOMElement)) {
253+
throw new XlsxFastEditorXmlException("Error querying XML fragment for row {$sheetNumber}!");
254+
}
255+
yield new XlsxFastEditorRow($this, $r);
256+
}
257+
}
258+
}
259+
171260
/**
172261
* Access the DOMElement representing a cell formula `<f>` in the worksheet.
173262
*
@@ -186,7 +275,7 @@ private function getF(int $sheetNumber, string $cellName): ?\DOMElement
186275
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
187276

188277
$f = null;
189-
$fs = $xpath->query("(//o:c[@r='$cellName'])[1]/o:f");
278+
$fs = $xpath->query("(/o:worksheet/o:sheetData/o:row/o:c[@r='$cellName'])[1]/o:f[1]");
190279
if ($fs !== false && $fs->length > 0) {
191280
$f = $fs[0];
192281
if (!($f instanceof \DOMElement)) {
@@ -220,7 +309,7 @@ public function readFormula(int $sheetNumber, string $cellName): ?string
220309
private function getV(int $sheetNumber, string $cellName): ?\DOMElement
221310
{
222311
if (!ctype_alnum($cellName)) {
223-
throw new XlsxFastEditorInputException("Invalid cell reference {$cellName}! ");
312+
throw new XlsxFastEditorInputException("Invalid cell reference {$cellName}!");
224313
}
225314
$cellName = strtoupper($cellName);
226315

@@ -229,7 +318,7 @@ private function getV(int $sheetNumber, string $cellName): ?\DOMElement
229318
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
230319

231320
$v = null;
232-
$vs = $xpath->query("(//o:c[@r='$cellName'])[1]/o:v");
321+
$vs = $xpath->query("(/o:worksheet/o:sheetData/o:row/o:c[@r='$cellName'])[1]/o:v[1]");
233322
if ($vs !== false && $vs->length > 0) {
234323
$v = $vs[0];
235324
if (!($v instanceof \DOMElement)) {
@@ -269,6 +358,29 @@ public function readInt(int $sheetNumber, string $cellName): ?int
269358
return (int)$v->nodeValue;
270359
}
271360

361+
/**
362+
* Access a string stored in the shared strings list.
363+
* @param int $stringNumber String number (ID), base 0.
364+
*/
365+
public function getSharedString(int $stringNumber): ?string
366+
{
367+
$dom = $this->getDomFromPath(self::SHARED_STRINGS_PATH);
368+
$xpath = new \DOMXPath($dom);
369+
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
370+
371+
$stringNumber++; // Base 1
372+
373+
$ts = $xpath->query("/o:sst/o:si[$stringNumber][1]/o:t[1]");
374+
if ($ts !== false && $ts->length > 0) {
375+
$t = $ts[0];
376+
if (!($t instanceof \DOMElement)) {
377+
throw new XlsxFastEditorXmlException("Error querying XML shared string {$stringNumber}!");
378+
}
379+
return $t->nodeValue;
380+
}
381+
return null;
382+
}
383+
272384
/**
273385
* Read a string in the given worksheet at the given cell location,
274386
* compatible with the shared string approach.
@@ -293,27 +405,11 @@ public function readString(int $sheetNumber, string $cellName): ?string
293405
if (!ctype_digit($v->nodeValue)) {
294406
throw new XlsxFastEditorXmlException("Error querying XML fragment for shared string {$sheetNumber}/{$cellName}!");
295407
}
296-
297-
$sharedStringId = 1 + (int)$v->nodeValue;
298-
299-
$dom = $this->getDomFromPath(self::SHARED_STRINGS_PATH);
300-
$xpath = new \DOMXPath($dom);
301-
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
302-
303-
$ts = $xpath->query("/o:sst/o:si[$sharedStringId]/o:t[1]");
304-
if ($ts !== false && $ts->length > 0) {
305-
$t = $ts[0];
306-
if (!($t instanceof \DOMElement)) {
307-
throw new XlsxFastEditorXmlException("Error querying XML shared string for {$sheetNumber}/{$cellName}!");
308-
}
309-
return $t->nodeValue;
310-
}
408+
return $this->getSharedString((int)$v->nodeValue);
311409
} else {
312410
// Local value
313411
return $v->nodeValue;
314412
}
315-
316-
return null;
317413
}
318414

319415
/**
@@ -380,12 +476,12 @@ private function getCell(int $sheetNumber, string $cellName, bool $autoCreate):
380476
$xpath->registerNamespace('o', self::OXML_NAMESPACE);
381477

382478
if (!ctype_alnum($cellName)) {
383-
throw new XlsxFastEditorInputException("Invalid cell reference {$cellName}! ");
479+
throw new XlsxFastEditorInputException("Invalid cell reference {$cellName}!");
384480
}
385481
$cellName = strtoupper($cellName);
386482

387483
$c = null;
388-
$cs = $xpath->query("(//o:c[@r='$cellName'])[1]");
484+
$cs = $xpath->query("/o:worksheet/o:sheetData/o:row/o:c[@r='{$cellName}'][1]");
389485
if ($cs !== false && $cs->length > 0) {
390486
$c = $cs[0];
391487
if (!($c instanceof \DOMElement)) {
@@ -402,7 +498,7 @@ private function getCell(int $sheetNumber, string $cellName, bool $autoCreate):
402498
}
403499

404500
$row = null;
405-
$rows = $xpath->query("(//o:row[@r='$rowNumber'])[1]");
501+
$rows = $xpath->query("/o:worksheet/o:sheetData/o:row[@r='{$rowNumber}'][1]");
406502
if ($rows !== false && $rows->length > 0) {
407503
$row = $rows[0];
408504
if (!($row instanceof \DOMElement)) {

0 commit comments

Comments
 (0)