1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace PhpOffice\PhpSpreadsheet\Collection; |
4
|
|
|
|
5
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\Cell; |
6
|
|
|
use PhpOffice\PhpSpreadsheet\Cell\Coordinate; |
7
|
|
|
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; |
8
|
|
|
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; |
9
|
|
|
use Psr\SimpleCache\CacheInterface; |
10
|
|
|
|
11
|
|
|
class Cells |
12
|
|
|
{ |
13
|
|
|
/** |
14
|
|
|
* @var \Psr\SimpleCache\CacheInterface |
15
|
|
|
*/ |
16
|
|
|
private $cache; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* Parent worksheet. |
20
|
|
|
* |
21
|
|
|
* @var Worksheet |
22
|
|
|
*/ |
23
|
|
|
private $parent; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* The currently active Cell. |
27
|
|
|
* |
28
|
|
|
* @var Cell |
29
|
|
|
*/ |
30
|
|
|
private $currentCell; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Coordinate of the currently active Cell. |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
private $currentCoordinate; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Flag indicating whether the currently active Cell requires saving. |
41
|
|
|
* |
42
|
|
|
* @var bool |
43
|
|
|
*/ |
44
|
|
|
private $currentCellIsDirty = false; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* An index of existing cells. Booleans indexed by their coordinate. |
48
|
|
|
* |
49
|
|
|
* @var bool[] |
50
|
|
|
*/ |
51
|
|
|
private $index = []; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Prefix used to uniquely identify cache data for this worksheet. |
55
|
|
|
* |
56
|
|
|
* @var string |
57
|
|
|
*/ |
58
|
|
|
private $cachePrefix; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Initialise this new cell collection. |
62
|
|
|
* |
63
|
|
|
* @param Worksheet $parent The worksheet for this cell collection |
64
|
|
|
* @param CacheInterface $cache |
65
|
|
|
*/ |
66
|
209 |
|
public function __construct(Worksheet $parent, CacheInterface $cache) |
67
|
|
|
{ |
68
|
|
|
// Set our parent worksheet. |
69
|
|
|
// This is maintained here to facilitate re-attaching it to Cell objects when |
70
|
|
|
// they are woken from a serialized state |
71
|
209 |
|
$this->parent = $parent; |
72
|
209 |
|
$this->cache = $cache; |
73
|
209 |
|
$this->cachePrefix = $this->getUniqueID(); |
74
|
209 |
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Return the parent worksheet for this cell collection. |
78
|
|
|
* |
79
|
|
|
* @return Worksheet |
80
|
|
|
*/ |
81
|
80 |
|
public function getParent() |
82
|
|
|
{ |
83
|
80 |
|
return $this->parent; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Whether the collection holds a cell for the given coordinate. |
88
|
|
|
* |
89
|
|
|
* @param string $pCoord Coordinate of the cell to check |
90
|
|
|
* |
91
|
|
|
* @return bool |
92
|
|
|
*/ |
93
|
171 |
|
public function has($pCoord) |
94
|
|
|
{ |
95
|
171 |
|
if ($pCoord === $this->currentCoordinate) { |
96
|
141 |
|
return true; |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
// Check if the requested entry exists in the index |
100
|
171 |
|
return isset($this->index[$pCoord]); |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Add or update a cell in the collection. |
105
|
|
|
* |
106
|
|
|
* @param Cell $cell Cell to update |
107
|
|
|
* |
108
|
|
|
* @throws PhpSpreadsheetException |
109
|
|
|
* |
110
|
|
|
* @return Cell |
111
|
|
|
*/ |
112
|
159 |
|
public function update(Cell $cell) |
113
|
|
|
{ |
114
|
159 |
|
return $this->add($cell->getCoordinate(), $cell); |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
/** |
118
|
|
|
* Delete a cell in cache identified by coordinate. |
119
|
|
|
* |
120
|
|
|
* @param string $pCoord Coordinate of the cell to delete |
121
|
|
|
*/ |
122
|
16 |
|
public function delete($pCoord) |
123
|
|
|
{ |
124
|
16 |
|
if ($pCoord === $this->currentCoordinate && $this->currentCell !== null) { |
125
|
|
|
$this->currentCell->detach(); |
126
|
|
|
$this->currentCoordinate = null; |
127
|
|
|
$this->currentCell = null; |
128
|
|
|
$this->currentCellIsDirty = false; |
129
|
|
|
} |
130
|
|
|
|
131
|
16 |
|
unset($this->index[$pCoord]); |
132
|
|
|
|
133
|
|
|
// Delete the entry from cache |
134
|
16 |
|
$this->cache->delete($this->cachePrefix . $pCoord); |
135
|
16 |
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Get a list of all cell coordinates currently held in the collection. |
139
|
|
|
* |
140
|
|
|
* @return string[] |
141
|
|
|
*/ |
142
|
127 |
|
public function getCoordinates() |
143
|
|
|
{ |
144
|
127 |
|
return array_keys($this->index); |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Get a sorted list of all cell coordinates currently held in the collection by row and column. |
149
|
|
|
* |
150
|
|
|
* @return string[] |
151
|
|
|
*/ |
152
|
95 |
|
public function getSortedCoordinates() |
153
|
|
|
{ |
154
|
95 |
|
$sortKeys = []; |
155
|
95 |
|
foreach ($this->getCoordinates() as $coord) { |
156
|
87 |
|
$column = ''; |
157
|
87 |
|
$row = 0; |
158
|
87 |
|
sscanf($coord, '%[A-Z]%d', $column, $row); |
159
|
87 |
|
$sortKeys[sprintf('%09d%3s', $row, $column)] = $coord; |
160
|
|
|
} |
161
|
95 |
|
ksort($sortKeys); |
162
|
|
|
|
163
|
95 |
|
return array_values($sortKeys); |
164
|
|
|
} |
165
|
|
|
|
166
|
|
|
/** |
167
|
|
|
* Get highest worksheet column and highest row that have cell records. |
168
|
|
|
* |
169
|
|
|
* @return array Highest column name and highest row number |
170
|
|
|
*/ |
171
|
118 |
|
public function getHighestRowAndColumn() |
172
|
|
|
{ |
173
|
|
|
// Lookup highest column and highest row |
174
|
118 |
|
$col = ['A' => '1A']; |
175
|
118 |
|
$row = [1]; |
176
|
118 |
|
foreach ($this->getCoordinates() as $coord) { |
177
|
110 |
|
$c = ''; |
178
|
110 |
|
$r = 0; |
179
|
110 |
|
sscanf($coord, '%[A-Z]%d', $c, $r); |
180
|
110 |
|
$row[$r] = $r; |
181
|
110 |
|
$col[$c] = strlen($c) . $c; |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
// Determine highest column and row |
185
|
118 |
|
$highestRow = max($row); |
186
|
118 |
|
$highestColumn = substr(max($col), 1); |
187
|
|
|
|
188
|
|
|
return [ |
189
|
118 |
|
'row' => $highestRow, |
190
|
118 |
|
'column' => $highestColumn, |
191
|
|
|
]; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Return the cell coordinate of the currently active cell object. |
196
|
|
|
* |
197
|
|
|
* @return string |
198
|
|
|
*/ |
199
|
162 |
|
public function getCurrentCoordinate() |
200
|
|
|
{ |
201
|
162 |
|
return $this->currentCoordinate; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Return the column coordinate of the currently active cell object. |
206
|
|
|
* |
207
|
|
|
* @return string |
208
|
|
|
*/ |
209
|
54 |
|
public function getCurrentColumn() |
210
|
|
|
{ |
211
|
54 |
|
$column = ''; |
212
|
54 |
|
$row = 0; |
213
|
|
|
|
214
|
54 |
|
sscanf($this->currentCoordinate, '%[A-Z]%d', $column, $row); |
215
|
|
|
|
216
|
54 |
|
return $column; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Return the row coordinate of the currently active cell object. |
221
|
|
|
* |
222
|
|
|
* @return int |
223
|
|
|
*/ |
224
|
48 |
|
public function getCurrentRow() |
225
|
|
|
{ |
226
|
48 |
|
$column = ''; |
227
|
48 |
|
$row = 0; |
228
|
|
|
|
229
|
48 |
|
sscanf($this->currentCoordinate, '%[A-Z]%d', $column, $row); |
230
|
|
|
|
231
|
48 |
|
return (int) $row; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Get highest worksheet column. |
236
|
|
|
* |
237
|
|
|
* @param string $row Return the highest column for the specified row, |
238
|
|
|
* or the highest column of any row if no row number is passed |
239
|
|
|
* |
240
|
|
|
* @return string Highest column name |
241
|
|
|
*/ |
242
|
13 |
|
public function getHighestColumn($row = null) |
243
|
|
|
{ |
244
|
13 |
|
if ($row == null) { |
|
|
|
|
245
|
13 |
|
$colRow = $this->getHighestRowAndColumn(); |
246
|
|
|
|
247
|
13 |
|
return $colRow['column']; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
$columnList = [1]; |
251
|
|
|
foreach ($this->getCoordinates() as $coord) { |
252
|
|
|
$c = ''; |
253
|
|
|
$r = 0; |
254
|
|
|
|
255
|
|
|
sscanf($coord, '%[A-Z]%d', $c, $r); |
256
|
|
|
if ($r != $row) { |
257
|
|
|
continue; |
258
|
|
|
} |
259
|
|
|
$columnList[] = Coordinate::columnIndexFromString($c); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
return Coordinate::stringFromColumnIndex(max($columnList) + 1); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Get highest worksheet row. |
267
|
|
|
* |
268
|
|
|
* @param string $column Return the highest row for the specified column, |
269
|
|
|
* or the highest row of any column if no column letter is passed |
270
|
|
|
* |
271
|
|
|
* @return int Highest row number |
272
|
|
|
*/ |
273
|
15 |
|
public function getHighestRow($column = null) |
274
|
|
|
{ |
275
|
15 |
|
if ($column == null) { |
|
|
|
|
276
|
15 |
|
$colRow = $this->getHighestRowAndColumn(); |
277
|
|
|
|
278
|
15 |
|
return $colRow['row']; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
$rowList = [0]; |
282
|
|
|
foreach ($this->getCoordinates() as $coord) { |
283
|
|
|
$c = ''; |
284
|
|
|
$r = 0; |
285
|
|
|
|
286
|
|
|
sscanf($coord, '%[A-Z]%d', $c, $r); |
287
|
|
|
if ($c != $column) { |
288
|
|
|
continue; |
289
|
|
|
} |
290
|
|
|
$rowList[] = $r; |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
return max($rowList); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Generate a unique ID for cache referencing. |
298
|
|
|
* |
299
|
|
|
* @return string Unique Reference |
300
|
|
|
*/ |
301
|
209 |
|
private function getUniqueID() |
302
|
|
|
{ |
303
|
209 |
|
return uniqid('phpspreadsheet.', true) . '.'; |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Clone the cell collection. |
308
|
|
|
* |
309
|
|
|
* @param Worksheet $parent The new worksheet that we're copying to |
310
|
|
|
* |
311
|
|
|
* @return self |
312
|
|
|
*/ |
313
|
3 |
|
public function cloneCellCollection(Worksheet $parent) |
314
|
|
|
{ |
315
|
3 |
|
$this->storeCurrentCell(); |
316
|
3 |
|
$newCollection = clone $this; |
317
|
|
|
|
318
|
3 |
|
$newCollection->parent = $parent; |
319
|
3 |
|
if (($newCollection->currentCell !== null) && (is_object($newCollection->currentCell))) { |
320
|
|
|
$newCollection->currentCell->attach($this); |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
// Get old values |
324
|
3 |
|
$oldKeys = $newCollection->getAllCacheKeys(); |
325
|
3 |
|
$oldValues = $newCollection->cache->getMultiple($oldKeys); |
326
|
3 |
|
$newValues = []; |
327
|
3 |
|
$oldCachePrefix = $newCollection->cachePrefix; |
328
|
|
|
|
329
|
|
|
// Change prefix |
330
|
3 |
|
$newCollection->cachePrefix = $newCollection->getUniqueID(); |
331
|
3 |
|
foreach ($oldValues as $oldKey => $value) { |
332
|
3 |
|
$newValues[str_replace($oldCachePrefix, $newCollection->cachePrefix, $oldKey)] = clone $value; |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
// Store new values |
336
|
3 |
|
$stored = $newCollection->cache->setMultiple($newValues); |
337
|
3 |
|
if (!$stored) { |
338
|
|
|
$newCollection->__destruct(); |
339
|
|
|
|
340
|
|
|
throw new PhpSpreadsheetException('Failed to copy cells in cache'); |
341
|
|
|
} |
342
|
|
|
|
343
|
3 |
|
return $newCollection; |
344
|
|
|
} |
345
|
|
|
|
346
|
|
|
/** |
347
|
|
|
* Remove a row, deleting all cells in that row. |
348
|
|
|
* |
349
|
|
|
* @param string $row Row number to remove |
350
|
|
|
*/ |
351
|
14 |
|
public function removeRow($row) |
352
|
|
|
{ |
353
|
14 |
|
foreach ($this->getCoordinates() as $coord) { |
354
|
14 |
|
$c = ''; |
355
|
14 |
|
$r = 0; |
356
|
|
|
|
357
|
14 |
|
sscanf($coord, '%[A-Z]%d', $c, $r); |
358
|
14 |
|
if ($r == $row) { |
359
|
14 |
|
$this->delete($coord); |
360
|
|
|
} |
361
|
|
|
} |
362
|
14 |
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Remove a column, deleting all cells in that column. |
366
|
|
|
* |
367
|
|
|
* @param string $column Column ID to remove |
368
|
|
|
*/ |
369
|
11 |
|
public function removeColumn($column) |
370
|
|
|
{ |
371
|
11 |
|
foreach ($this->getCoordinates() as $coord) { |
372
|
11 |
|
$c = ''; |
373
|
11 |
|
$r = 0; |
374
|
|
|
|
375
|
11 |
|
sscanf($coord, '%[A-Z]%d', $c, $r); |
376
|
11 |
|
if ($c == $column) { |
377
|
11 |
|
$this->delete($coord); |
378
|
|
|
} |
379
|
|
|
} |
380
|
11 |
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Store cell data in cache for the current cell object if it's "dirty", |
384
|
|
|
* and the 'nullify' the current cell object. |
385
|
|
|
* |
386
|
|
|
* @throws PhpSpreadsheetException |
387
|
|
|
*/ |
388
|
173 |
|
private function storeCurrentCell() |
389
|
|
|
{ |
390
|
173 |
|
if ($this->currentCellIsDirty && !empty($this->currentCoordinate)) { |
391
|
137 |
|
$this->currentCell->detach(); |
392
|
|
|
|
393
|
137 |
|
$stored = $this->cache->set($this->cachePrefix . $this->currentCoordinate, $this->currentCell); |
394
|
137 |
|
if (!$stored) { |
395
|
1 |
|
$this->__destruct(); |
396
|
|
|
|
397
|
1 |
|
throw new PhpSpreadsheetException("Failed to store cell {$this->currentCoordinate} in cache"); |
398
|
|
|
} |
399
|
136 |
|
$this->currentCellIsDirty = false; |
400
|
|
|
} |
401
|
|
|
|
402
|
173 |
|
$this->currentCoordinate = null; |
403
|
173 |
|
$this->currentCell = null; |
404
|
173 |
|
} |
405
|
|
|
|
406
|
|
|
/** |
407
|
|
|
* Add or update a cell identified by its coordinate into the collection. |
408
|
|
|
* |
409
|
|
|
* @param string $pCoord Coordinate of the cell to update |
410
|
|
|
* @param Cell $cell Cell to update |
411
|
|
|
* |
412
|
|
|
* @throws PhpSpreadsheetException |
413
|
|
|
* |
414
|
|
|
* @return \PhpOffice\PhpSpreadsheet\Cell\Cell |
415
|
|
|
*/ |
416
|
167 |
|
public function add($pCoord, Cell $cell) |
417
|
|
|
{ |
418
|
167 |
|
if ($pCoord !== $this->currentCoordinate) { |
419
|
167 |
|
$this->storeCurrentCell(); |
420
|
|
|
} |
421
|
167 |
|
$this->index[$pCoord] = true; |
422
|
|
|
|
423
|
167 |
|
$this->currentCoordinate = $pCoord; |
424
|
167 |
|
$this->currentCell = $cell; |
425
|
167 |
|
$this->currentCellIsDirty = true; |
426
|
|
|
|
427
|
167 |
|
return $cell; |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
/** |
431
|
|
|
* Get cell at a specific coordinate. |
432
|
|
|
* |
433
|
|
|
* @param string $pCoord Coordinate of the cell |
434
|
|
|
* |
435
|
|
|
* @throws PhpSpreadsheetException |
436
|
|
|
* |
437
|
|
|
* @return \PhpOffice\PhpSpreadsheet\Cell\Cell Cell that was found, or null if not found |
438
|
|
|
*/ |
439
|
163 |
|
public function get($pCoord) |
440
|
|
|
{ |
441
|
163 |
|
if ($pCoord === $this->currentCoordinate) { |
442
|
141 |
|
return $this->currentCell; |
443
|
|
|
} |
444
|
133 |
|
$this->storeCurrentCell(); |
445
|
|
|
|
446
|
|
|
// Return null if requested entry doesn't exist in collection |
447
|
133 |
|
if (!$this->has($pCoord)) { |
448
|
34 |
|
return null; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
// Check if the entry that has been requested actually exists |
452
|
125 |
|
$cell = $this->cache->get($this->cachePrefix . $pCoord); |
453
|
125 |
|
if ($cell === null) { |
454
|
1 |
|
throw new PhpSpreadsheetException("Cell entry {$pCoord} no longer exists in cache. This probably means that the cache was cleared by someone else."); |
455
|
|
|
} |
456
|
|
|
|
457
|
|
|
// Set current entry to the requested entry |
458
|
124 |
|
$this->currentCoordinate = $pCoord; |
459
|
124 |
|
$this->currentCell = $cell; |
460
|
|
|
// Re-attach this as the cell's parent |
461
|
124 |
|
$this->currentCell->attach($this); |
462
|
|
|
|
463
|
|
|
// Return requested entry |
464
|
124 |
|
return $this->currentCell; |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* Clear the cell collection and disconnect from our parent. |
469
|
|
|
*/ |
470
|
3 |
|
public function unsetWorksheetCells() |
471
|
|
|
{ |
472
|
3 |
|
if ($this->currentCell !== null) { |
473
|
3 |
|
$this->currentCell->detach(); |
474
|
3 |
|
$this->currentCell = null; |
475
|
3 |
|
$this->currentCoordinate = null; |
476
|
|
|
} |
477
|
|
|
|
478
|
|
|
// Flush the cache |
479
|
3 |
|
$this->__destruct(); |
480
|
|
|
|
481
|
3 |
|
$this->index = []; |
482
|
|
|
|
483
|
|
|
// detach ourself from the worksheet, so that it can then delete this object successfully |
484
|
3 |
|
$this->parent = null; |
485
|
3 |
|
} |
486
|
|
|
|
487
|
|
|
/** |
488
|
|
|
* Destroy this cell collection. |
489
|
|
|
*/ |
490
|
5 |
|
public function __destruct() |
491
|
|
|
{ |
492
|
5 |
|
$this->cache->deleteMultiple($this->getAllCacheKeys()); |
493
|
5 |
|
} |
494
|
|
|
|
495
|
|
|
/** |
496
|
|
|
* Returns all known cache keys. |
497
|
|
|
* |
498
|
|
|
* @return string[] |
499
|
|
|
*/ |
500
|
8 |
|
private function getAllCacheKeys() |
501
|
|
|
{ |
502
|
8 |
|
$keys = []; |
503
|
8 |
|
foreach ($this->getCoordinates() as $coordinate) { |
504
|
7 |
|
$keys[] = $this->cachePrefix . $coordinate; |
505
|
|
|
} |
506
|
|
|
|
507
|
8 |
|
return $keys; |
508
|
|
|
} |
509
|
|
|
} |
510
|
|
|
|