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