From 6add9066fc5c0455eb764c1f3af6e4e4e3562419 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois Date: Tue, 5 May 2026 14:12:17 +0200 Subject: [PATCH] Fix unbounded memoisation of `IntlDateFormatter` / `NumberFormatter` --- extra/intl-extra/IntlExtension.php | 8 +++++ extra/intl-extra/Tests/IntlExtensionTest.php | 31 ++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index 43fd1c66e..3c6401349 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -145,6 +145,8 @@ final class IntlExtension extends AbstractExtension 'monetary_grouping_separator' => \NumberFormatter::MONETARY_GROUPING_SEPARATOR_SYMBOL, ]; + private const MAX_CACHED_FORMATTERS = 100; + private $dateFormatters = []; private $numberFormatters = []; private $dateFormatterPrototype; @@ -441,6 +443,9 @@ final class IntlExtension extends AbstractExtension $hash = $locale.'|'.$dateFormatValue.'|'.$timeFormatValue.'|'.$timezoneName.'|'.$calendar.'|'.$pattern; if (!isset($this->dateFormatters[$hash])) { + if (\count($this->dateFormatters) >= self::MAX_CACHED_FORMATTERS) { + array_shift($this->dateFormatters); + } $this->dateFormatters[$hash] = new \IntlDateFormatter($locale, $dateFormatValue, $timeFormatValue, $timezone, $calendar, $pattern); } @@ -487,6 +492,9 @@ final class IntlExtension extends AbstractExtension $hash = $locale.'|'.$style.'|'.json_encode($attrs).'|'.json_encode($textAttrs).'|'.json_encode($symbols); if (!isset($this->numberFormatters[$hash])) { + if (\count($this->numberFormatters) >= self::MAX_CACHED_FORMATTERS) { + array_shift($this->numberFormatters); + } $this->numberFormatters[$hash] = new \NumberFormatter($locale, self::NUMBER_STYLES[$style]); } diff --git a/extra/intl-extra/Tests/IntlExtensionTest.php b/extra/intl-extra/Tests/IntlExtensionTest.php index 91aa9e84f..403efbfa1 100644 --- a/extra/intl-extra/Tests/IntlExtensionTest.php +++ b/extra/intl-extra/Tests/IntlExtensionTest.php @@ -96,4 +96,35 @@ class IntlExtensionTest extends TestCase $ext->formatDateTime($env, new \DateTime('2020-02-20T13:37:00+00:00'), 'short', 'short', 'yyyy-MM-dd HH:mm:ss', 'UTC', 'gregorian', 'en_US') ); } + + public function testDateFormatterCacheIsBounded() + { + $ext = new IntlExtension(); + $env = new Environment(new ArrayLoader()); + $date = new \DateTime('2020-02-20T13:37:00+00:00'); + + for ($i = 0; $i < 250; ++$i) { + $ext->formatDateTime($env, $date, 'medium', 'medium', 'yyyy-MM-dd-'.$i, 'UTC', 'gregorian', 'en_US'); + } + + $cache = (new \ReflectionProperty(IntlExtension::class, 'dateFormatters'))->getValue($ext); + $this->assertLessThanOrEqual(100, \count($cache)); + $this->assertSame( + '2020-02-20-249', + $ext->formatDateTime($env, $date, 'medium', 'medium', 'yyyy-MM-dd-249', 'UTC', 'gregorian', 'en_US') + ); + } + + public function testNumberFormatterCacheIsBounded() + { + $ext = new IntlExtension(); + + for ($i = 0; $i < 250; ++$i) { + $ext->formatNumber(1, ['multiplier' => $i + 1], 'decimal', 'default', 'en_US'); + } + + $cache = (new \ReflectionProperty(IntlExtension::class, 'numberFormatters'))->getValue($ext); + $this->assertLessThanOrEqual(100, \count($cache)); + $this->assertGreaterThan(1, \count($cache)); + } }