Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 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
    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
        $this->parent = $parent;
62
        $this->cache = $cache;
63
        $this->cachePrefix = $this->getUniqueID();
64
    }
65
 
66
    /**
67
     * Return the parent worksheet for this cell collection.
68
     */
69
    public function getParent(): ?Worksheet
70
    {
71
        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
    public function has(string $cellCoordinate): bool
80
    {
81
        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
    /**
90
     * Add or update a cell in the collection.
91
     *
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
    /**
100
     * Delete a cell in cache identified by coordinate.
101
     *
102
     * @param string $cellCoordinate Coordinate of the cell to delete
103
     */
104
    public function delete(string $cellCoordinate): void
105
    {
106
        if ($cellCoordinate === $this->currentCoordinate && $this->currentCell !== null) {
107
            $this->currentCell->detach();
108
            $this->currentCoordinate = null;
109
            $this->currentCell = null;
110
            $this->currentCellIsDirty = false;
111
        }
112
 
113
        unset($this->index[$cellCoordinate]);
114
 
115
        // Delete the entry from cache
116
        $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
    public function getCoordinates(): array
125
    {
126
        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
    public function getSortedCoordinates(): array
135
    {
136
        asort($this->index);
137
 
138
        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
    public function getSortedCoordinatesInt(): array
147
    {
148
        asort($this->index);
149
 
150
        return array_values($this->index);
151
    }
152
 
153
    /**
154
     * Return the cell coordinate of the currently active cell object.
155
     */
156
    public function getCurrentCoordinate(): ?string
157
    {
158
        return $this->currentCoordinate;
159
    }
160
 
161
    /**
162
     * Return the column coordinate of the currently active cell object.
163
     */
164
    public function getCurrentColumn(): string
165
    {
166
        $column = 0;
167
        $row = '';
168
        sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row);
169
 
170
        return (string) $column;
171
    }
172
 
173
    /**
174
     * Return the row coordinate of the currently active cell object.
175
     */
176
    public function getCurrentRow(): int
177
    {
178
        $column = 0;
179
        $row = '';
180
        sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row);
181
 
182
        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
    public function getHighestRowAndColumn(): array
191
    {
192
        // Lookup highest column and highest row
193
        $maxRow = $maxColumn = 1;
194
        foreach ($this->index as $coordinate) {
195
            $row = (int) floor(($coordinate - 1) / self::MAX_COLUMN_ID) + 1;
196
            $maxRow = ($maxRow > $row) ? $maxRow : $row;
197
            $column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID;
198
            $maxColumn = ($maxColumn > $column) ? $maxColumn : $column;
199
        }
200
 
201
        return [
202
            'row' => $maxRow,
203
            'column' => Coordinate::stringFromColumnIndex($maxColumn),
204
        ];
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
    public function getHighestColumn($row = null): string
216
    {
217
        if ($row === null) {
218
            return $this->getHighestRowAndColumn()['column'];
219
        }
220
 
221
        $row = (int) $row;
222
        if ($row <= 0) {
223
            throw new PhpSpreadsheetException('Row number must be a positive integer');
224
        }
225
 
226
        $maxColumn = 1;
227
        $toRow = $row * self::MAX_COLUMN_ID;
228
        $fromRow = --$row * self::MAX_COLUMN_ID;
229
        foreach ($this->index as $coordinate) {
230
            if ($coordinate < $fromRow || $coordinate >= $toRow) {
231
                continue;
232
            }
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
     * @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
    public function getHighestRow(?string $column = null): int
249
    {
250
        if ($column === null) {
251
            return $this->getHighestRowAndColumn()['row'];
252
        }
253
 
254
        $maxRow = 1;
255
        $columnIndex = Coordinate::columnIndexFromString($column);
256
        foreach ($this->index as $coordinate) {
257
            if ($coordinate % self::MAX_COLUMN_ID !== $columnIndex) {
258
                continue;
259
            }
260
            $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
261
            $maxRow = ($maxRow > $row) ? $maxRow : $row;
262
        }
263
 
264
        return $maxRow;
265
    }
266
 
267
    /**
268
     * Generate a unique ID for cache referencing.
269
     *
270
     * @return string Unique Reference
271
     */
272
    private function getUniqueID(): string
273
    {
274
        $cacheType = Settings::getCache();
275
 
276
        return ($cacheType instanceof Memory\SimpleCache1 || $cacheType instanceof Memory\SimpleCache3)
277
            ? random_bytes(7) . ':'
278
            : uniqid('phpspreadsheet.', true) . '.';
279
    }
280
 
281
    /**
282
     * Clone the cell collection.
283
     */
284
    public function cloneCellCollection(Worksheet $worksheet): static
285
    {
286
        $this->storeCurrentCell();
287
        $newCollection = clone $this;
288
 
289
        $newCollection->parent = $worksheet;
290
        $newCollection->cachePrefix = $newCollection->getUniqueID();
291
 
292
        foreach ($this->index as $key => $value) {
293
            $newCollection->index[$key] = $value;
294
            $stored = $newCollection->cache->set(
295
                $newCollection->cachePrefix . $key,
296
                clone $this->getCache($key)
297
            );
298
            if ($stored === false) {
299
                $this->destructIfNeeded($newCollection, 'Failed to copy cells in cache');
300
            }
301
        }
302
 
303
        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
    public function removeRow($row): void
312
    {
313
        $this->storeCurrentCell();
314
        $row = (int) $row;
315
        if ($row <= 0) {
316
            throw new PhpSpreadsheetException('Row number must be a positive integer');
317
        }
318
 
319
        $toRow = $row * self::MAX_COLUMN_ID;
320
        $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
    /**
331
     * Remove a column, deleting all cells in that column.
332
     *
333
     * @param string $column Column ID to remove
334
     */
335
    public function removeColumn(string $column): void
336
    {
337
        $this->storeCurrentCell();
338
 
339
        $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
 
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
    private function storeCurrentCell(): void
354
    {
355
        if ($this->currentCellIsDirty && isset($this->currentCoordinate, $this->currentCell)) {
356
            $this->currentCell->detach();
357
 
358
            $stored = $this->cache->set($this->cachePrefix . $this->currentCoordinate, $this->currentCell);
359
            if ($stored === false) {
360
                $this->destructIfNeeded($this, "Failed to store cell {$this->currentCoordinate} in cache");
361
            }
362
            $this->currentCellIsDirty = false;
363
        }
364
 
365
        $this->currentCoordinate = null;
366
        $this->currentCell = null;
367
    }
368
 
369
    private function destructIfNeeded(self $cells, string $message): void
370
    {
371
        $cells->__destruct();
372
 
373
        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
    public function add(string $cellCoordinate, Cell $cell): Cell
383
    {
384
        if ($cellCoordinate !== $this->currentCoordinate) {
385
            $this->storeCurrentCell();
386
        }
387
        $column = 0;
388
        $row = '';
389
        sscanf($cellCoordinate, '%[A-Z]%d', $column, $row);
390
        $this->index[$cellCoordinate] = (--$row * self::MAX_COLUMN_ID) + Coordinate::columnIndexFromString((string) $column);
391
 
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
     *
402
     * @param string $cellCoordinate Coordinate of the cell
403
     *
404
     * @return null|Cell Cell that was found, or null if not found
405
     */
406
    public function get(string $cellCoordinate): ?Cell
407
    {
408
        if ($cellCoordinate === $this->currentCoordinate) {
409
            return $this->currentCell;
410
        }
411
        $this->storeCurrentCell();
412
 
413
        // Return null if requested entry doesn't exist in collection
414
        if ($this->has($cellCoordinate) === false) {
415
            return null;
416
        }
417
 
418
        $cell = $this->getcache($cellCoordinate);
419
 
420
        // Set current entry to the requested entry
421
        $this->currentCoordinate = $cellCoordinate;
422
        $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
    }
429
 
430
    /**
431
     * Clear the cell collection and disconnect from our parent.
432
     */
433
    public function unsetWorksheetCells(): void
434
    {
435
        if ($this->currentCell !== null) {
436
            $this->currentCell->detach();
437
            $this->currentCell = null;
438
            $this->currentCoordinate = null;
439
        }
440
 
441
        // Flush the cache
442
        $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
    }
449
 
450
    /**
451
     * Destroy this cell collection.
452
     */
453
    public function __destruct()
454
    {
455
        $this->cache->deleteMultiple($this->getAllCacheKeys());
456
        $this->parent = null;
457
    }
458
 
459
    /**
460
     * Returns all known cache keys.
461
     *
462
     * @return iterable<string>
463
     */
464
    private function getAllCacheKeys(): iterable
465
    {
466
        foreach ($this->index as $coordinate => $value) {
467
            yield $this->cachePrefix . $coordinate;
468
        }
469
    }
470
 
471
    private function getCache(string $cellCoordinate): Cell
472
    {
473
        $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
}