19
19
*/
20
20
final class XlsxFastEditor
21
21
{
22
- private const OXML_NAMESPACE = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main ' ;
22
+ public const OXML_NAMESPACE = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main ' ;
23
23
24
24
private const CALC_CHAIN_CACHE_PATH = 'xl/calcChain.xml ' ;
25
25
private const SHARED_STRINGS_PATH = 'xl/sharedStrings.xml ' ;
@@ -130,7 +130,7 @@ public function getWorksheetNumber(string $sheetName): int
130
130
$ dom = $ this ->getDomFromPath (self ::WORKBOOK_PATH );
131
131
$ xpath = new \DOMXPath ($ dom );
132
132
$ 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) " );
134
134
if (is_string ($ sheetId )) {
135
135
return (int )$ sheetId ;
136
136
}
@@ -168,6 +168,95 @@ private function getDomFromPath(string $path): \DOMDocument
168
168
return $ dom ;
169
169
}
170
170
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
+
171
260
/**
172
261
* Access the DOMElement representing a cell formula `<f>` in the worksheet.
173
262
*
@@ -186,7 +275,7 @@ private function getF(int $sheetNumber, string $cellName): ?\DOMElement
186
275
$ xpath ->registerNamespace ('o ' , self ::OXML_NAMESPACE );
187
276
188
277
$ 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] " );
190
279
if ($ fs !== false && $ fs ->length > 0 ) {
191
280
$ f = $ fs [0 ];
192
281
if (!($ f instanceof \DOMElement)) {
@@ -220,7 +309,7 @@ public function readFormula(int $sheetNumber, string $cellName): ?string
220
309
private function getV (int $ sheetNumber , string $ cellName ): ?\DOMElement
221
310
{
222
311
if (!ctype_alnum ($ cellName )) {
223
- throw new XlsxFastEditorInputException ("Invalid cell reference {$ cellName }! " );
312
+ throw new XlsxFastEditorInputException ("Invalid cell reference {$ cellName }! " );
224
313
}
225
314
$ cellName = strtoupper ($ cellName );
226
315
@@ -229,7 +318,7 @@ private function getV(int $sheetNumber, string $cellName): ?\DOMElement
229
318
$ xpath ->registerNamespace ('o ' , self ::OXML_NAMESPACE );
230
319
231
320
$ 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] " );
233
322
if ($ vs !== false && $ vs ->length > 0 ) {
234
323
$ v = $ vs [0 ];
235
324
if (!($ v instanceof \DOMElement)) {
@@ -269,6 +358,29 @@ public function readInt(int $sheetNumber, string $cellName): ?int
269
358
return (int )$ v ->nodeValue ;
270
359
}
271
360
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
+
272
384
/**
273
385
* Read a string in the given worksheet at the given cell location,
274
386
* compatible with the shared string approach.
@@ -293,27 +405,11 @@ public function readString(int $sheetNumber, string $cellName): ?string
293
405
if (!ctype_digit ($ v ->nodeValue )) {
294
406
throw new XlsxFastEditorXmlException ("Error querying XML fragment for shared string {$ sheetNumber }/ {$ cellName }! " );
295
407
}
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 );
311
409
} else {
312
410
// Local value
313
411
return $ v ->nodeValue ;
314
412
}
315
-
316
- return null ;
317
413
}
318
414
319
415
/**
@@ -380,12 +476,12 @@ private function getCell(int $sheetNumber, string $cellName, bool $autoCreate):
380
476
$ xpath ->registerNamespace ('o ' , self ::OXML_NAMESPACE );
381
477
382
478
if (!ctype_alnum ($ cellName )) {
383
- throw new XlsxFastEditorInputException ("Invalid cell reference {$ cellName }! " );
479
+ throw new XlsxFastEditorInputException ("Invalid cell reference {$ cellName }! " );
384
480
}
385
481
$ cellName = strtoupper ($ cellName );
386
482
387
483
$ 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] " );
389
485
if ($ cs !== false && $ cs ->length > 0 ) {
390
486
$ c = $ cs [0 ];
391
487
if (!($ c instanceof \DOMElement)) {
@@ -402,7 +498,7 @@ private function getCell(int $sheetNumber, string $cellName, bool $autoCreate):
402
498
}
403
499
404
500
$ row = null ;
405
- $ rows = $ xpath ->query ("(/ /o:row[@r='$ rowNumber']) [1] " );
501
+ $ rows = $ xpath ->query ("/o:worksheet /o:sheetData/o: row[@r='{ $ rowNumber} '] [1] " );
406
502
if ($ rows !== false && $ rows ->length > 0 ) {
407
503
$ row = $ rows [0 ];
408
504
if (!($ row instanceof \DOMElement)) {
0 commit comments