Passed
Push — master ( 0c5808...8c86ca )
by
unknown
18:02 queued 06:53
created

Cells::getCache()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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