Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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