diff --git a/CHANGELOG b/CHANGELOG index 72e77863465..ba4b573764d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ # 3.29.0 (2026-XX-XX) + * Fix `IntlExtension` ignoring explicit date/time formats and the `format_date`/`format_time` filters when a date formatter prototype is configured * Add a `format_list` filter to `IntlExtension` to format a list of strings using PHP 8.5's `IntlListFormatter` * Fix array access with a `Stringable` key coercing the key to string for `ArrayAccess` objects that use object keys (such as `SplObjectStorage`) * Fix duplicated macro argument names triggering a PHP fatal error instead of a `SyntaxError` diff --git a/extra/intl-extra/IntlExtension.php b/extra/intl-extra/IntlExtension.php index cc740f6563c..c561c38d65d 100644 --- a/extra/intl-extra/IntlExtension.php +++ b/extra/intl-extra/IntlExtension.php @@ -405,7 +405,7 @@ public function formatNumberStyle(string $style, $number, array $attrs = [], str * @param \DateTimeInterface|string|null $date A date or null to use the current time * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ - public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'medium', ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string + public function formatDateTime(Environment $env, $date, ?string $dateFormat = null, ?string $timeFormat = null, string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { $date = $env->getExtension(CoreExtension::class)->convertDate($date, $timezone); @@ -428,7 +428,7 @@ public function formatDateTime(Environment $env, $date, ?string $dateFormat = 'm * @param \DateTimeInterface|string|null $date A date or null to use the current time * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ - public function formatDate(Environment $env, $date, ?string $dateFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string + public function formatDate(Environment $env, $date, ?string $dateFormat = null, string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { return $this->formatDateTime($env, $date, $dateFormat, 'none', $pattern, $timezone, $calendar, $locale); } @@ -437,7 +437,7 @@ public function formatDate(Environment $env, $date, ?string $dateFormat = 'mediu * @param \DateTimeInterface|string|null $date A date or null to use the current time * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged */ - public function formatTime(Environment $env, $date, ?string $timeFormat = 'medium', string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string + public function formatTime(Environment $env, $date, ?string $timeFormat = null, string $pattern = '', $timezone = null, string $calendar = 'gregorian', ?string $locale = null): string { return $this->formatDateTime($env, $date, 'none', $timeFormat, $pattern, $timezone, $calendar, $locale); } @@ -475,17 +475,24 @@ private function createDateFormatter(?string $locale, ?string $dateFormat, ?stri $calendar = 'gregorian' === $calendar ? \IntlDateFormatter::GREGORIAN : \IntlDateFormatter::TRADITIONAL; - $dateFormatValue = $dateFormats[$dateFormat] ?? null; - $timeFormatValue = self::TIME_FORMATS[$timeFormat] ?? null; + $dateFormatValue = null === $dateFormat ? null : $dateFormats[$dateFormat]; + $timeFormatValue = null === $timeFormat ? null : self::TIME_FORMATS[$timeFormat]; if ($this->dateFormatterPrototype) { - $dateFormatValue = $dateFormatValue ?: $this->dateFormatterPrototype->getDateType(); - $timeFormatValue = $timeFormatValue ?: $this->dateFormatterPrototype->getTimeType(); + $dateFormatValue ??= $this->dateFormatterPrototype->getDateType(); + $timeFormatValue ??= $this->dateFormatterPrototype->getTimeType(); $timezone = $timezone ?: $this->dateFormatterPrototype->getTimeZone()->toDateTimeZone(); $calendar = $calendar ?: $this->dateFormatterPrototype->getCalendar(); - $pattern = $pattern ?: $this->dateFormatterPrototype->getPattern(); + // fall back to the prototype's pattern only when nothing else was given, else it would override the explicit date/time formats; + // a pattern describes a full datetime rendering, so it cannot be honored by format_date/format_time, which pass 'none' for the other part + if ('' === $pattern && null === $dateFormat && null === $timeFormat) { + $pattern = $this->dateFormatterPrototype->getPattern(); + } } + $dateFormatValue ??= \IntlDateFormatter::MEDIUM; + $timeFormatValue ??= \IntlDateFormatter::MEDIUM; + $timezoneName = $timezone ? $timezone->getName() : '(none)'; $hash = $locale.'|'.$dateFormatValue.'|'.$timeFormatValue.'|'.$timezoneName.'|'.$calendar.'|'.$pattern; diff --git a/extra/intl-extra/Tests/IntlExtensionTest.php b/extra/intl-extra/Tests/IntlExtensionTest.php index e870b7c4f47..6c254d7af99 100644 --- a/extra/intl-extra/Tests/IntlExtensionTest.php +++ b/extra/intl-extra/Tests/IntlExtensionTest.php @@ -97,6 +97,32 @@ public function testFormatterOverridenProto(): void ); } + public function testFormatterProtoDoesNotOverrideExplicitFormats(): void + { + $dateFormatterProto = new \IntlDateFormatter('nl_NL', \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM, new \DateTimeZone('Europe/Amsterdam')); + $ext = new IntlExtension($dateFormatterProto); + $env = new Environment(new ArrayLoader()); + $date = new \DateTime('2020-02-20T22:22:00+00:00', new \DateTimeZone('UTC')); + + $this->assertSame('20 feb 2020', $ext->formatDate($env, $date)); + $this->assertSame('22:22:00', $ext->formatTime($env, $date)); + $this->assertSame('donderdag 20 februari 2020', $ext->formatDateTime($env, $date, 'full', 'none')); + } + + public function testFormatterProtoWithCustomPatternIsUsedByDefault(): void + { + $dateFormatterProto = new \IntlDateFormatter('nl_NL', \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM, new \DateTimeZone('Europe/Amsterdam'), \IntlDateFormatter::GREGORIAN, 'yyyy-MM-dd'); + $ext = new IntlExtension($dateFormatterProto); + $env = new Environment(new ArrayLoader()); + $date = new \DateTime('2020-02-20T22:22:00+00:00', new \DateTimeZone('UTC')); + + $this->assertSame('2020-02-20', $ext->formatDateTime($env, $date)); + $this->assertSame('20 feb 2020', $ext->formatDate($env, $date, 'medium')); + // the prototype pattern describes a full datetime rendering, so format_date/format_time ignore it + $this->assertSame('20 feb 2020', $ext->formatDate($env, $date)); + $this->assertSame('22:22:00', $ext->formatTime($env, $date)); + } + public function testDateFormatterCacheIsBounded(): void { $ext = new IntlExtension();