From 7850daa8681c1f65cd1846a5b8758cebee0e81b7 Mon Sep 17 00:00:00 2001 From: Fuzuki Date: Wed, 27 Aug 2025 17:09:03 +0200 Subject: [PATCH 1/2] Improve performance when working with large amounts of cells --- src/PhpSpreadsheet/Collection/Cells.php | 69 ++++++++++++++++++++-- src/PhpSpreadsheet/Worksheet/Worksheet.php | 21 +++++++ 2 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index 365c37e89c..aac5cd0b49 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -43,6 +43,25 @@ class Cells */ private array $index = []; + /** + * Flag to avoid sorting the index every time. + */ + private bool $indexSorted = false; + + /** + * Index keys cache to avoid recalculating on large arrays. + * + * @var null|string[] + */ + private ?array $indexKeysCache = null; + + /** + * Index values cache to avoid recalculating on large arrays. + * + * @var null|int[] + */ + private ?array $indexValuesCache = null; + /** * Prefix used to uniquely identify cache data for this worksheet. */ @@ -112,6 +131,10 @@ public function delete(string $cellCoordinate): void unset($this->index[$cellCoordinate]); + // Clear index caches + $this->indexKeysCache = null; + $this->indexValuesCache = null; + // Delete the entry from cache $this->cache->delete($this->cachePrefix . $cellCoordinate); } @@ -123,7 +146,12 @@ public function delete(string $cellCoordinate): void */ public function getCoordinates(): array { - return array_keys($this->index); + // Build or rebuild index keys cache + if ($this->indexKeysCache === null) { + $this->indexKeysCache = array_keys($this->index); + } + + return $this->indexKeysCache; } /** @@ -133,9 +161,18 @@ public function getCoordinates(): array */ public function getSortedCoordinates(): array { - asort($this->index); + // Sort only when required + if (!$this->indexSorted) { + asort($this->index); + $this->indexSorted = true; + } - return array_keys($this->index); + // Build or rebuild index keys cache + if ($this->indexKeysCache === null) { + $this->indexKeysCache = array_keys($this->index); + } + + return $this->indexKeysCache; } /** @@ -145,9 +182,16 @@ public function getSortedCoordinates(): array */ public function getSortedCoordinatesInt(): array { - asort($this->index); + if (!$this->indexSorted) { + asort($this->index); + $this->indexSorted = true; + } - return array_values($this->index); + if ($this->indexValuesCache === null) { + $this->indexValuesCache = array_values($this->index); + } + + return $this->indexValuesCache; } /** @@ -300,6 +344,11 @@ public function cloneCellCollection(Worksheet $worksheet): static } } + // Clear index sorted flag and index caches + $newCollection->indexSorted = false; + $newCollection->indexKeysCache = null; + $newCollection->indexValuesCache = null; + return $newCollection; } @@ -390,6 +439,11 @@ public function add(string $cellCoordinate, Cell $cell): Cell /** @var int $row */ $this->index[$cellCoordinate] = (--$row * self::MAX_COLUMN_ID) + Coordinate::columnIndexFromString((string) $column); + // Clear index sorted flag and index caches + $this->indexSorted = false; + $this->indexKeysCache = null; + $this->indexValuesCache = null; + $this->currentCoordinate = $cellCoordinate; $this->currentCell = $cell; $this->currentCellIsDirty = true; @@ -444,6 +498,11 @@ public function unsetWorksheetCells(): void $this->index = []; + // Clear index sorted flag and index caches + $this->indexSorted = false; + $this->indexKeysCache = null; + $this->indexValuesCache = null; + // detach ourself from the worksheet, so that it can then delete this object successfully $this->parent = null; } diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 137d2b4b6c..d4fae65043 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -3104,9 +3104,30 @@ public function rangeToArrayYieldRows( $index = ($row - 1) * AddressRange::MAX_COLUMN_INT + 1; $indexPlus = $index + AddressRange::MAX_COLUMN_INT - 1; + + // Binary search to quickly approach the correct index + $keyIndex = intdiv($keysCount, 2); + $boundLow = 0; + $boundHigh = $keysCount - 1; + while ($boundLow <= $boundHigh) { + $keyIndex = intdiv($boundLow + $boundHigh, 2); + if ($keys[$keyIndex] < $index) { + $boundLow = $keyIndex + 1; + } elseif ($keys[$keyIndex] > $index) { + $boundHigh = $keyIndex - 1; + } else { + break; + } + } + + // Realign to the proper index value + while ($keyIndex > 0 && $keys[$keyIndex] > $index) { + --$keyIndex; + } while ($keyIndex < $keysCount && $keys[$keyIndex] < $index) { ++$keyIndex; } + while ($keyIndex < $keysCount && $keys[$keyIndex] <= $indexPlus) { $key = $keys[$keyIndex]; $thisRow = intdiv($key - 1, AddressRange::MAX_COLUMN_INT) + 1; From 5474bd70639bdd1bef517451e69e78148d645fad Mon Sep 17 00:00:00 2001 From: Fuzuki Date: Wed, 27 Aug 2025 17:48:17 +0200 Subject: [PATCH 2/2] Fix cells cache not being reset when sorting the index --- src/PhpSpreadsheet/Collection/Cells.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PhpSpreadsheet/Collection/Cells.php b/src/PhpSpreadsheet/Collection/Cells.php index aac5cd0b49..0661867d69 100644 --- a/src/PhpSpreadsheet/Collection/Cells.php +++ b/src/PhpSpreadsheet/Collection/Cells.php @@ -165,6 +165,9 @@ public function getSortedCoordinates(): array if (!$this->indexSorted) { asort($this->index); $this->indexSorted = true; + // Clear unsorted cache + $this->indexKeysCache = null; + $this->indexValuesCache = null; } // Build or rebuild index keys cache @@ -185,6 +188,9 @@ public function getSortedCoordinatesInt(): array if (!$this->indexSorted) { asort($this->index); $this->indexSorted = true; + // Clear unsorted cache + $this->indexKeysCache = null; + $this->indexValuesCache = null; } if ($this->indexValuesCache === null) {