Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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\Worksheet\Worksheet;
  10  use Psr\SimpleCache\CacheInterface;
  11  
  12  class Cells
  13  {
  14      /**
  15       * @var CacheInterface
  16       */
  17      private $cache;
  18  
  19      /**
  20       * Parent worksheet.
  21       *
  22       * @var null|Worksheet
  23       */
  24      private $parent;
  25  
  26      /**
  27       * The currently active Cell.
  28       *
  29       * @var null|Cell
  30       */
  31      private $currentCell;
  32  
  33      /**
  34       * Coordinate of the currently active Cell.
  35       *
  36       * @var null|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 null|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 $cellCoordinate Coordinate of the cell to check
  90       *
  91       * @return bool
  92       */
  93      public function has($cellCoordinate)
  94      {
  95          if ($cellCoordinate === $this->currentCoordinate) {
  96              return true;
  97          }
  98  
  99          // Check if the requested entry exists in the index
 100          return isset($this->index[$cellCoordinate]);
 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 $cellCoordinate Coordinate of the cell to delete
 119       */
 120      public function delete($cellCoordinate): void
 121      {
 122          if ($cellCoordinate === $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[$cellCoordinate]);
 130  
 131          // Delete the entry from cache
 132          $this->cache->delete($this->cachePrefix . $cellCoordinate);
 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((string) @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 null|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 null|int|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((int) @max($columnList));
 261      }
 262  
 263      /**
 264       * Get highest worksheet row.
 265       *
 266       * @param null|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       * @return self
 308       */
 309      public function cloneCellCollection(Worksheet $worksheet)
 310      {
 311          $this->storeCurrentCell();
 312          $newCollection = clone $this;
 313  
 314          $newCollection->parent = $worksheet;
 315          if (is_object($newCollection->currentCell)) {
 316              $newCollection->currentCell->attach($this);
 317          }
 318  
 319          // Get old values
 320          $oldKeys = $newCollection->getAllCacheKeys();
 321          $oldValues = $newCollection->cache->getMultiple($oldKeys);
 322          $newValues = [];
 323          $oldCachePrefix = $newCollection->cachePrefix;
 324  
 325          // Change prefix
 326          $newCollection->cachePrefix = $newCollection->getUniqueID();
 327          foreach ($oldValues as $oldKey => $value) {
 328              /** @var string $newKey */
 329              $newKey = str_replace($oldCachePrefix, $newCollection->cachePrefix, $oldKey);
 330              $newValues[$newKey] = clone $value;
 331          }
 332  
 333          // Store new values
 334          $stored = $newCollection->cache->setMultiple($newValues);
 335          $this->destructIfNeeded($stored, $newCollection, 'Failed to copy cells in cache');
 336  
 337          return $newCollection;
 338      }
 339  
 340      /**
 341       * Remove a row, deleting all cells in that row.
 342       *
 343       * @param string $row Row number to remove
 344       */
 345      public function removeRow($row): void
 346      {
 347          foreach ($this->getCoordinates() as $coord) {
 348              $c = '';
 349              $r = 0;
 350  
 351              sscanf($coord, '%[A-Z]%d', $c, $r);
 352              if ($r == $row) {
 353                  $this->delete($coord);
 354              }
 355          }
 356      }
 357  
 358      /**
 359       * Remove a column, deleting all cells in that column.
 360       *
 361       * @param string $column Column ID to remove
 362       */
 363      public function removeColumn($column): void
 364      {
 365          foreach ($this->getCoordinates() as $coord) {
 366              $c = '';
 367              $r = 0;
 368  
 369              sscanf($coord, '%[A-Z]%d', $c, $r);
 370              if ($c == $column) {
 371                  $this->delete($coord);
 372              }
 373          }
 374      }
 375  
 376      /**
 377       * Store cell data in cache for the current cell object if it's "dirty",
 378       * and the 'nullify' the current cell object.
 379       */
 380      private function storeCurrentCell(): void
 381      {
 382          if ($this->currentCellIsDirty && isset($this->currentCoordinate, $this->currentCell)) {
 383              $this->currentCell->detach();
 384  
 385              $stored = $this->cache->set($this->cachePrefix . $this->currentCoordinate, $this->currentCell);
 386              $this->destructIfNeeded($stored, $this, "Failed to store cell {$this->currentCoordinate} in cache");
 387              $this->currentCellIsDirty = false;
 388          }
 389  
 390          $this->currentCoordinate = null;
 391          $this->currentCell = null;
 392      }
 393  
 394      private function destructIfNeeded(bool $stored, self $cells, string $message): void
 395      {
 396          if (!$stored) {
 397              $cells->__destruct();
 398  
 399              throw new PhpSpreadsheetException($message);
 400          }
 401      }
 402  
 403      /**
 404       * Add or update a cell identified by its coordinate into the collection.
 405       *
 406       * @param string $cellCoordinate Coordinate of the cell to update
 407       * @param Cell $cell Cell to update
 408       *
 409       * @return Cell
 410       */
 411      public function add($cellCoordinate, Cell $cell)
 412      {
 413          if ($cellCoordinate !== $this->currentCoordinate) {
 414              $this->storeCurrentCell();
 415          }
 416          $this->index[$cellCoordinate] = true;
 417  
 418          $this->currentCoordinate = $cellCoordinate;
 419          $this->currentCell = $cell;
 420          $this->currentCellIsDirty = true;
 421  
 422          return $cell;
 423      }
 424  
 425      /**
 426       * Get cell at a specific coordinate.
 427       *
 428       * @param string $cellCoordinate Coordinate of the cell
 429       *
 430       * @return null|Cell Cell that was found, or null if not found
 431       */
 432      public function get($cellCoordinate)
 433      {
 434          if ($cellCoordinate === $this->currentCoordinate) {
 435              return $this->currentCell;
 436          }
 437          $this->storeCurrentCell();
 438  
 439          // Return null if requested entry doesn't exist in collection
 440          if (!$this->has($cellCoordinate)) {
 441              return null;
 442          }
 443  
 444          // Check if the entry that has been requested actually exists
 445          $cell = $this->cache->get($this->cachePrefix . $cellCoordinate);
 446          if ($cell === null) {
 447              throw new PhpSpreadsheetException("Cell entry {$cellCoordinate} no longer exists in cache. This probably means that the cache was cleared by someone else.");
 448          }
 449  
 450          // Set current entry to the requested entry
 451          $this->currentCoordinate = $cellCoordinate;
 452          $this->currentCell = $cell;
 453          // Re-attach this as the cell's parent
 454          $this->currentCell->attach($this);
 455  
 456          // Return requested entry
 457          return $this->currentCell;
 458      }
 459  
 460      /**
 461       * Clear the cell collection and disconnect from our parent.
 462       */
 463      public function unsetWorksheetCells(): void
 464      {
 465          if ($this->currentCell !== null) {
 466              $this->currentCell->detach();
 467              $this->currentCell = null;
 468              $this->currentCoordinate = null;
 469          }
 470  
 471          // Flush the cache
 472          $this->__destruct();
 473  
 474          $this->index = [];
 475  
 476          // detach ourself from the worksheet, so that it can then delete this object successfully
 477          $this->parent = null;
 478      }
 479  
 480      /**
 481       * Destroy this cell collection.
 482       */
 483      public function __destruct()
 484      {
 485          $this->cache->deleteMultiple($this->getAllCacheKeys());
 486      }
 487  
 488      /**
 489       * Returns all known cache keys.
 490       *
 491       * @return Generator|string[]
 492       */
 493      private function getAllCacheKeys()
 494      {
 495          foreach ($this->getCoordinates() as $coordinate) {
 496              yield $this->cachePrefix . $coordinate;
 497          }
 498      }
 499  }