PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 All 45 releases
tablepress / libraries / vendor / PhpSpreadsheet / Collection / Cells.php

Cells.php in TablePress – Tables in WordPress made easy 3.4, at libraries/vendor/PhpSpreadsheet/Collection/Cells.php

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