From 1bc4c637622f0b11176478f3587f19d000167037 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Mon, 7 Sep 2026 21:25:41 +0200 Subject: [PATCH 1/4] fix(I18n): fix timezone and testNow calculation in Time::today(), yesterday(), and tomorrow() - Compute start of day relative to the requested timezone instead of the server default timezone - Respect Time::setTestNow() mock instance in today(), yesterday(), and tomorrow() - Add unit tests covering date boundary transitions across different timezones - Add changelog entry in v4.7.5.rst --- system/I18n/TimeTrait.php | 6 +-- tests/system/I18n/TimeTest.php | 51 +++++++++++++++++++++ user_guide_src/source/changelogs/v4.7.5.rst | 1 + 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 0660724f5019..172860f31e08 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -144,7 +144,7 @@ public static function parse(string $datetime, $timezone = null, ?string $locale */ public static function today($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00'), $timezone, $locale); + return static::now($timezone, $locale)->setTime(0, 0, 0, 0); } /** @@ -158,7 +158,7 @@ public static function today($timezone = null, ?string $locale = null) */ public static function yesterday($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale); + return static::now($timezone, $locale)->modify('-1 day')->setTime(0, 0, 0, 0); } /** @@ -172,7 +172,7 @@ public static function yesterday($timezone = null, ?string $locale = null) */ public static function tomorrow($timezone = null, ?string $locale = null) { - return new static(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale); + return static::now($timezone, $locale)->modify('+1 day')->setTime(0, 0, 0, 0); } /** diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index ef6712f37259..4eed1738e548 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -190,6 +190,57 @@ public function testTomorrow(): void $this->assertSame(date('Y-m-d 00:00:00', strtotime('+1 day')), $time->toDateTimeString()); } + public function testTodayWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Asia/Tokyo (+09:00) it is already 2026-09-08 08:30:00 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoToday = Time::today('Asia/Tokyo'); + $this->assertSame('2026-09-08 00:00:00', $tokyoToday->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in America/New_York (-04:00 EDT) it is still 2026-09-07 22:00:00 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyToday = Time::today('America/New_York'); + $this->assertSame('2026-09-07 00:00:00', $nyToday->toDateTimeString()); + + Time::setTestNow(); + } + + public function testYesterdayWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo yesterday is 2026-09-07 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoYesterday = Time::yesterday('Asia/Tokyo'); + $this->assertSame('2026-09-07 00:00:00', $tokyoYesterday->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY yesterday is 2026-09-06 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyYesterday = Time::yesterday('America/New_York'); + $this->assertSame('2026-09-06 00:00:00', $nyYesterday->toDateTimeString()); + + Time::setTestNow(); + } + + public function testTomorrowWithTimezoneAcrossDateBoundary(): void + { + // When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo tomorrow is 2026-09-09 + Time::setTestNow('2026-09-07 23:30:00', 'UTC'); + + $tokyoTomorrow = Time::tomorrow('Asia/Tokyo'); + $this->assertSame('2026-09-09 00:00:00', $tokyoTomorrow->toDateTimeString()); + + // When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY tomorrow is 2026-09-08 + Time::setTestNow('2026-09-08 02:00:00', 'UTC'); + + $nyTomorrow = Time::tomorrow('America/New_York'); + $this->assertSame('2026-09-08 00:00:00', $nyTomorrow->toDateTimeString()); + + Time::setTestNow(); + } + public function testCreateFromDate(): void { $time = Time::createFromDate(2017, 03, 05, 'America/Chicago'); diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 6dee8ee9aa6d..3efdb3ffaf6d 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -47,6 +47,7 @@ Bugs Fixed - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). +- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. - **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics. From 49593a5c416ba4c5bcb1e8ab60934f03685eea1d Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Wed, 9 Sep 2026 19:07:23 +0200 Subject: [PATCH 2/4] Update user_guide_src/source/changelogs/v4.7.5.rst Co-authored-by: neznaika0 --- user_guide_src/source/changelogs/v4.7.5.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 3efdb3ffaf6d..84e5ef714c27 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -47,7 +47,7 @@ Bugs Fixed - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). -- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. +- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone``, ``$locale`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. - **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics. From d0b8a9aaead66011f7181b3192202115ed66fb0f Mon Sep 17 00:00:00 2001 From: Bogdan Lambarski Date: Thu, 10 Sep 2026 20:33:41 +0200 Subject: [PATCH 3/4] Fix multiple bugs in Files, Helpers, Honeypot, I18n, Logger, and Cache Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- user_guide_src/source/changelogs/v4.7.5.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.7.5.rst b/user_guide_src/source/changelogs/v4.7.5.rst index 84e5ef714c27..3efdb3ffaf6d 100644 --- a/user_guide_src/source/changelogs/v4.7.5.rst +++ b/user_guide_src/source/changelogs/v4.7.5.rst @@ -47,7 +47,7 @@ Bugs Fixed - **Files:** Fixed a bug where ``File::move()`` and ``UploadedFile::move()`` set executable and overly permissive file permissions (``0777 & ~umask()`` instead of ``0666 & ~umask()``), and ``UploadedFile::move()`` targeted the parent directory instead of the destination file for ``chmod()``. - **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them. - **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden). -- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone``, ``$locale`` and ``setTestNow()`` when calculating the day. +- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day. - **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors. - **Cache:** Fixed ``MemcachedHandler::decrement()`` initializing a non-existent counter to the positive offset. Missing counters are now initialized to ``0``, reflecting Memcached's unsigned, saturating counter semantics. From 51d0e30c52f65eb5e9397708348ebc70724c0c49 Mon Sep 17 00:00:00 2001 From: Bogdan Date: Thu, 10 Sep 2026 20:40:44 +0200 Subject: [PATCH 4/4] fix(I18n): address review suggestions for Time::today/yesterday/tomorrow Use new static(null) instead of static::now() to avoid coupling overridable extension points, and freeze time in testTodayLocalized to prevent intermittent failure across timezones. --- system/I18n/TimeTrait.php | 6 +++--- tests/system/I18n/TimeLegacyTest.php | 7 ++++++- tests/system/I18n/TimeTest.php | 7 ++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 172860f31e08..58b4c25c005e 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -144,7 +144,7 @@ public static function parse(string $datetime, $timezone = null, ?string $locale */ public static function today($timezone = null, ?string $locale = null) { - return static::now($timezone, $locale)->setTime(0, 0, 0, 0); + return (new static(null, $timezone, $locale))->setTime(0, 0, 0, 0); } /** @@ -158,7 +158,7 @@ public static function today($timezone = null, ?string $locale = null) */ public static function yesterday($timezone = null, ?string $locale = null) { - return static::now($timezone, $locale)->modify('-1 day')->setTime(0, 0, 0, 0); + return (new static(null, $timezone, $locale))->modify('-1 day')->setTime(0, 0, 0, 0); } /** @@ -172,7 +172,7 @@ public static function yesterday($timezone = null, ?string $locale = null) */ public static function tomorrow($timezone = null, ?string $locale = null) { - return static::now($timezone, $locale)->modify('+1 day')->setTime(0, 0, 0, 0); + return (new static(null, $timezone, $locale))->modify('+1 day')->setTime(0, 0, 0, 0); } /** diff --git a/tests/system/I18n/TimeLegacyTest.php b/tests/system/I18n/TimeLegacyTest.php index 6cdcdb45ff74..ec6cdb042640 100644 --- a/tests/system/I18n/TimeLegacyTest.php +++ b/tests/system/I18n/TimeLegacyTest.php @@ -170,9 +170,14 @@ public function testToday(): void public function testTodayLocalized(): void { + // Freeze time to avoid flakiness when server and London are on different calendar days + TimeLegacy::setTestNow('2026-06-15 12:00:00', 'UTC'); + $time = TimeLegacy::today('Europe/London'); - $this->assertSame(date('Y-m-d 00:00:00'), $time->toDateTimeString()); + $this->assertSame('2026-06-15 00:00:00', $time->toDateTimeString()); + + TimeLegacy::setTestNow(); } public function testYesterday(): void diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 4eed1738e548..1120f22f99b2 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -171,9 +171,14 @@ public function testToday(): void public function testTodayLocalized(): void { + // Freeze time to avoid flakiness when server and London are on different calendar days + Time::setTestNow('2026-06-15 12:00:00', 'UTC'); + $time = Time::today('Europe/London'); - $this->assertSame(date('Y-m-d 00:00:00'), $time->toDateTimeString()); + $this->assertSame('2026-06-15 00:00:00', $time->toDateTimeString()); + + Time::setTestNow(); } public function testYesterday(): void