vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php line 395

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