Passed
Pull Request — master (#4377)
by Owen
12:45
created

Cells::storeCurrentCell()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

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