Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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  }