From ab79a69f168b7ad099081a148b5543a3451dda7f Mon Sep 17 00:00:00 2001 From: Roberts Salna Date: Fri, 19 Jun 2026 16:57:51 +0300 Subject: [PATCH 1/3] Fix PHP 8.5 deprecations flagged in CI Address the exact PHP 8.5.7 deprecations reported by CI, with behaviour-identical changes: - Reader/Xlsx/ColumnAndRowAttributes.php: replace the `++` increments on the Excel column-letter strings with Coordinate::stringFromColumnIndex(columnIndexFromString(...) + 1), fixing "Increment on non-numeric string is deprecated". - Worksheet/Worksheet.php (rangeToArray): same column-string `++` replacement using the Coordinate helper. - Calculation/Calculation.php (renameCalculationCacheForWorksheet): coerce the array offset to '' ($fromWorksheetName ?? '') to fix "Using null as an array offset is deprecated". The Coordinate helper is used (rather than str_increment) because it keeps the library working on the full supported PHP range (^7.2||^8.0) and matches the surrounding code which already uses Coordinate::stringFromColumnIndex. --- src/PhpSpreadsheet/Calculation/Calculation.php | 3 ++- src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php | 5 +++-- src/PhpSpreadsheet/Worksheet/Worksheet.php | 6 ++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index f5790a1de6..1cefa64145 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2861,7 +2861,8 @@ public function clearCalculationCacheForWorksheet($worksheetName): void */ public function renameCalculationCacheForWorksheet($fromWorksheetName, $toWorksheetName): void { - if (isset($this->calculationCache[$fromWorksheetName])) { + // PHP 8.5: avoid using null as an array offset, coerce to empty string instead + if (isset($this->calculationCache[$fromWorksheetName ?? ''])) { $this->calculationCache[$toWorksheetName] = &$this->calculationCache[$fromWorksheetName]; unset($this->calculationCache[$fromWorksheetName]); } diff --git a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php index 4134b2f184..db57248170 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php @@ -137,8 +137,9 @@ private function readColumnAttributes(SimpleXMLElement $worksheetCols, $readData foreach ($worksheetCols->col as $column) { $startColumn = Coordinate::stringFromColumnIndex((int) $column['min']); $endColumn = Coordinate::stringFromColumnIndex((int) $column['max']); - ++$endColumn; - for ($columnAddress = $startColumn; $columnAddress !== $endColumn; ++$columnAddress) { + // PHP 8.5: avoid increment on non-numeric string, use column helper instead + $endColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($endColumn) + 1); + for ($columnAddress = $startColumn; $columnAddress !== $endColumn; $columnAddress = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($columnAddress) + 1)) { $columnAttributes[$columnAddress] = $this->readColumnRangeAttributes($column, $readDataOnly); if ((int) ($column['max']) == 16384) { diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 19833b71e5..e2e43cbbea 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -2503,14 +2503,16 @@ public function rangeToArray($pRange, $nullValue = null, $calculateFormulas = tr $maxCol = Coordinate::stringFromColumnIndex($rangeEnd[0]); $maxRow = $rangeEnd[1]; - ++$maxCol; + // PHP 8.5: avoid increment on non-numeric string, use column helper instead + $maxCol = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($maxCol) + 1); // Loop through rows $r = -1; for ($row = $minRow; $row <= $maxRow; ++$row) { $rRef = $returnCellRef ? $row : ++$r; $c = -1; // Loop through columns in the current row - for ($col = $minCol; $col != $maxCol; ++$col) { + // PHP 8.5: avoid increment on non-numeric string, use column helper instead + for ($col = $minCol; $col != $maxCol; $col = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($col) + 1)) { $cRef = $returnCellRef ? $col : ++$c; // Using getCell() will create a new cell if it doesn't already exist. We don't want that to happen // so we test and retrieve directly against cellCollection From 59fa646fbe28bd12313e8fdb0533acd7c673bd4c Mon Sep 17 00:00:00 2001 From: Roberts Salna Date: Fri, 19 Jun 2026 17:30:45 +0300 Subject: [PATCH 2/3] Restore pre-8.4 BC rounding for Excel ROUND PHP 8.4 rewrote round() and removed its internal pre-rounding step. For a value whose IEEE-754 representation sits just below a .5 boundary due to accumulated float error (e.g. 366.49999999999977, intended as 366.5), pre-8.4 round() snapped it up to 367 while 8.4+ rounds down to 366. That silently drifted insurer pricing/tax output by a cent (e.g. Visana content_insurance_discount_on_tax). Reimplement Kasko::ROUND to reproduce the pre-8.4 _php_math_round algorithm (pre-round to absorb representation error, then round half away from zero) so Excel ROUND output stays backward-compatible / insurer-signed-off and independent of the PHP version. Deliberately ROUND-only; no other function is touched. --- src/PhpSpreadsheet/Calculation/Kasko.php | 81 +++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Calculation/Kasko.php b/src/PhpSpreadsheet/Calculation/Kasko.php index a3a56eacc5..60f888844b 100644 --- a/src/PhpSpreadsheet/Calculation/Kasko.php +++ b/src/PhpSpreadsheet/Calculation/Kasko.php @@ -24,7 +24,86 @@ public static function ROUND($num, $precision) $num = Functions::flattenSingleValue($num); $precision = Functions::flattenSingleValue($precision); - return round((float) $num, (int) $precision); + // php 8.4 rewrote round() and removed its internal "pre-rounding" step. + // for a value whose ieee-754 representation sits just below a .5 boundary + // because of accumulated float error (e.g. 366.49999999999977, which the + // spreadsheet math intends as 366.5) pre-8.4 round() snapped it up to 367, + // while php 8.4+ rounds down to 366. that silently drifted insurer pricing + // output by a cent (e.g. visana content_insurance_discount_on_tax). + // to keep excel ROUND output backward-compatible / insurer-signed-off and + // independent of the php version we run on, we reproduce the pre-8.4 + // _php_math_round algorithm here (pre-round to absorb representation error, + // then round half away from zero) instead of calling the native round(). + // this is deliberately ROUND-only: MROUND / ROUNDUP / ROUNDDOWN / ROUNDBAHT + // and every other function keep using the native php behaviour. + return self::roundPre84((float) $num, (int) $precision); + } + + // faithful port of pre-8.4 php-src _php_math_round (ext/standard/math.c), + // default mode = round half away from zero. kept private and used only by + // ROUND() above so no other calculation is affected. + private static function roundPre84(float $value, int $places): float + { + if (!is_finite($value) || $value == 0.0) { + return $value; + } + + // number of significant decimal places we can trust before float noise. + $precisionPlaces = 14 - (int) floor(log10(abs($value))); + + $f1 = 10.0 ** (float) abs($places); + + if ($precisionPlaces > $places && $precisionPlaces - $places < 15) { + // pre-round at the trustworthy precision first to absorb the + // representation error, then round down to the requested precision. + $f2 = 10.0 ** (float) abs($precisionPlaces); + $tmpValue = ($precisionPlaces >= 0) ? $value * $f2 : $value / $f2; + $tmpValue = self::roundHelperPre84($tmpValue); + + $usePrecision = $precisionPlaces - $places; + $usePrecision = ($usePrecision < -15) ? -15 : (($usePrecision > 15) ? 15 : $usePrecision); + + $f2 = 10.0 ** (float) abs($usePrecision); + $tmpValue = ($usePrecision >= 0) ? $tmpValue / $f2 : $tmpValue * $f2; + $tmpValue = self::roundHelperPre84($tmpValue); + } else { + $tmpValue = ($places >= 0) ? $value * $f1 : $value / $f1; + // values >= 1e15 have no fractional part left to round. + if (abs($tmpValue) >= 1e15) { + return $value; + } + $tmpValue = self::roundHelperPre84($tmpValue); + } + + if (abs($places) < 23) { + $tmpValue = ($places > 0) ? $tmpValue / $f1 : $tmpValue * $f1; + } else { + $str = sprintf('%15fe%d', $tmpValue, -$places); + $tmpValue = (float) $str; + if (!is_finite($tmpValue) || $tmpValue == 0.0) { + $tmpValue = $value; + } + } + + return $tmpValue; + } + + // pre-8.4 php_round_helper for the default (half away from zero) mode. + private static function roundHelperPre84(float $value): float + { + if ($value >= 0.0) { + $tmpValue = floor($value + 0.5); + if (($tmpValue - $value) > 0.5) { + $tmpValue -= 1.0; + } + } else { + $tmpValue = ceil($value - 0.5); + if (($value - $tmpValue) > 0.5) { + $tmpValue += 1.0; + } + } + + return $tmpValue; } /** From 86dfd78b4dc9e87c1501ae762797b3b6fff230f7 Mon Sep 17 00:00:00 2001 From: Roberts Salna Date: Sat, 27 Jun 2026 17:10:22 +0300 Subject: [PATCH 3/3] Use native PHP round() for Excel ROUND (drop pre-8.4 BC compensation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts 59fa646f. Decision (migration ยง2.1, option 1): accept PHP 8.4+ native round() output as the correct values rather than carrying a non-standard pre-8.4 rounding port in the fork. Excel ROUND now maps to Kasko::ROUND -> native round(); the roundPre84/roundHelperPre84 port is removed. Insurer pricing snapshots are regenerated to the corrected figures in the consuming services. PHP 8.5 deprecation fixes on this branch (incl. the (int) $precision cast) are unaffected. --- src/PhpSpreadsheet/Calculation/Kasko.php | 81 +----------------------- 1 file changed, 1 insertion(+), 80 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Kasko.php b/src/PhpSpreadsheet/Calculation/Kasko.php index 60f888844b..a3a56eacc5 100644 --- a/src/PhpSpreadsheet/Calculation/Kasko.php +++ b/src/PhpSpreadsheet/Calculation/Kasko.php @@ -24,86 +24,7 @@ public static function ROUND($num, $precision) $num = Functions::flattenSingleValue($num); $precision = Functions::flattenSingleValue($precision); - // php 8.4 rewrote round() and removed its internal "pre-rounding" step. - // for a value whose ieee-754 representation sits just below a .5 boundary - // because of accumulated float error (e.g. 366.49999999999977, which the - // spreadsheet math intends as 366.5) pre-8.4 round() snapped it up to 367, - // while php 8.4+ rounds down to 366. that silently drifted insurer pricing - // output by a cent (e.g. visana content_insurance_discount_on_tax). - // to keep excel ROUND output backward-compatible / insurer-signed-off and - // independent of the php version we run on, we reproduce the pre-8.4 - // _php_math_round algorithm here (pre-round to absorb representation error, - // then round half away from zero) instead of calling the native round(). - // this is deliberately ROUND-only: MROUND / ROUNDUP / ROUNDDOWN / ROUNDBAHT - // and every other function keep using the native php behaviour. - return self::roundPre84((float) $num, (int) $precision); - } - - // faithful port of pre-8.4 php-src _php_math_round (ext/standard/math.c), - // default mode = round half away from zero. kept private and used only by - // ROUND() above so no other calculation is affected. - private static function roundPre84(float $value, int $places): float - { - if (!is_finite($value) || $value == 0.0) { - return $value; - } - - // number of significant decimal places we can trust before float noise. - $precisionPlaces = 14 - (int) floor(log10(abs($value))); - - $f1 = 10.0 ** (float) abs($places); - - if ($precisionPlaces > $places && $precisionPlaces - $places < 15) { - // pre-round at the trustworthy precision first to absorb the - // representation error, then round down to the requested precision. - $f2 = 10.0 ** (float) abs($precisionPlaces); - $tmpValue = ($precisionPlaces >= 0) ? $value * $f2 : $value / $f2; - $tmpValue = self::roundHelperPre84($tmpValue); - - $usePrecision = $precisionPlaces - $places; - $usePrecision = ($usePrecision < -15) ? -15 : (($usePrecision > 15) ? 15 : $usePrecision); - - $f2 = 10.0 ** (float) abs($usePrecision); - $tmpValue = ($usePrecision >= 0) ? $tmpValue / $f2 : $tmpValue * $f2; - $tmpValue = self::roundHelperPre84($tmpValue); - } else { - $tmpValue = ($places >= 0) ? $value * $f1 : $value / $f1; - // values >= 1e15 have no fractional part left to round. - if (abs($tmpValue) >= 1e15) { - return $value; - } - $tmpValue = self::roundHelperPre84($tmpValue); - } - - if (abs($places) < 23) { - $tmpValue = ($places > 0) ? $tmpValue / $f1 : $tmpValue * $f1; - } else { - $str = sprintf('%15fe%d', $tmpValue, -$places); - $tmpValue = (float) $str; - if (!is_finite($tmpValue) || $tmpValue == 0.0) { - $tmpValue = $value; - } - } - - return $tmpValue; - } - - // pre-8.4 php_round_helper for the default (half away from zero) mode. - private static function roundHelperPre84(float $value): float - { - if ($value >= 0.0) { - $tmpValue = floor($value + 0.5); - if (($tmpValue - $value) > 0.5) { - $tmpValue -= 1.0; - } - } else { - $tmpValue = ceil($value - 0.5); - if (($value - $tmpValue) > 0.5) { - $tmpValue += 1.0; - } - } - - return $tmpValue; + return round((float) $num, (int) $precision); } /**