Skip to content

Commit 01fd6ee

Browse files
gr8manneznaika0Copilotmichalsn
authored
fix(I18n): compute correct date in Time::today(), yesterday(), and tomorrow() across timezones (#10532)
* 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 * Update user_guide_src/source/changelogs/v4.7.5.rst Co-authored-by: neznaika0 <ozornick.ks@gmail.com> * 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> * 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. * Update system/I18n/TimeTrait.php Co-authored-by: Michal Sniatala <michal@sniatala.pl> * Update tests/system/I18n/TimeTest.php Co-authored-by: Michal Sniatala <michal@sniatala.pl> * Update system/I18n/TimeTrait.php Co-authored-by: Michal Sniatala <michal@sniatala.pl> * Update tests/system/I18n/TimeTest.php Co-authored-by: Michal Sniatala <michal@sniatala.pl> * style: correct indentation in TimeTest --------- Co-authored-by: neznaika0 <ozornick.ks@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Michal Sniatala <michal@sniatala.pl>
1 parent 48bdc64 commit 01fd6ee

4 files changed

Lines changed: 75 additions & 5 deletions

File tree

‎system/I18n/TimeTrait.php‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public static function parse(string $datetime, $timezone = null, ?string $locale
144144
*/
145145
public static function today($timezone = null, ?string $locale = null)
146146
{
147-
return new static(date('Y-m-d 00:00:00'), $timezone, $locale);
147+
return (new static(null, $timezone, $locale))->setTime(0, 0, 0, 0);
148148
}
149149

150150
/**
@@ -158,7 +158,7 @@ public static function today($timezone = null, ?string $locale = null)
158158
*/
159159
public static function yesterday($timezone = null, ?string $locale = null)
160160
{
161-
return new static(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale);
161+
return (new static(null, $timezone, $locale))->modify('yesterday');
162162
}
163163

164164
/**
@@ -172,7 +172,7 @@ public static function yesterday($timezone = null, ?string $locale = null)
172172
*/
173173
public static function tomorrow($timezone = null, ?string $locale = null)
174174
{
175-
return new static(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale);
175+
return (new static(null, $timezone, $locale))->modify('tomorrow');
176176
}
177177

178178
/**

‎tests/system/I18n/TimeLegacyTest.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,9 +170,14 @@ public function testToday(): void
170170

171171
public function testTodayLocalized(): void
172172
{
173+
// Freeze time to avoid flakiness when server and London are on different calendar days
174+
TimeLegacy::setTestNow('2026-06-15 12:00:00', 'UTC');
175+
173176
$time = TimeLegacy::today('Europe/London');
174177

175-
$this->assertSame(date('Y-m-d 00:00:00'), $time->toDateTimeString());
178+
$this->assertSame('2026-06-15 00:00:00', $time->toDateTimeString());
179+
180+
TimeLegacy::setTestNow();
176181
}
177182

178183
public function testYesterday(): void

‎tests/system/I18n/TimeTest.php‎

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,14 @@ public function testToday(): void
171171

172172
public function testTodayLocalized(): void
173173
{
174+
// Freeze time to avoid flakiness when server and London are on different calendar days
175+
Time::setTestNow('2026-06-15 12:00:00', 'UTC');
176+
174177
$time = Time::today('Europe/London');
175178

176-
$this->assertSame(date('Y-m-d 00:00:00'), $time->toDateTimeString());
179+
$this->assertSame('2026-06-15 00:00:00', $time->toDateTimeString());
180+
181+
Time::setTestNow();
177182
}
178183

179184
public function testYesterday(): void
@@ -190,6 +195,65 @@ public function testTomorrow(): void
190195
$this->assertSame(date('Y-m-d 00:00:00', strtotime('+1 day')), $time->toDateTimeString());
191196
}
192197

198+
public function testTodayWithTimezoneAcrossDateBoundary(): void
199+
{
200+
// When UTC is 2026-09-07 23:30:00, in Asia/Tokyo (+09:00) it is already 2026-09-08 08:30:00
201+
Time::setTestNow('2026-09-07 23:30:00', 'UTC');
202+
203+
$tokyoToday = Time::today('Asia/Tokyo');
204+
$this->assertSame('2026-09-08 00:00:00', $tokyoToday->toDateTimeString());
205+
206+
// 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
207+
Time::setTestNow('2026-09-08 02:00:00', 'UTC');
208+
209+
$nyToday = Time::today('America/New_York');
210+
$this->assertSame('2026-09-07 00:00:00', $nyToday->toDateTimeString());
211+
212+
Time::setTestNow();
213+
}
214+
215+
public function testYesterdayWithTimezoneAcrossDateBoundary(): void
216+
{
217+
// When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo yesterday is 2026-09-07
218+
Time::setTestNow('2026-09-07 23:30:00', 'UTC');
219+
220+
$tokyoYesterday = Time::yesterday('Asia/Tokyo');
221+
$this->assertSame('2026-09-07 00:00:00', $tokyoYesterday->toDateTimeString());
222+
223+
// When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY yesterday is 2026-09-06
224+
Time::setTestNow('2026-09-08 02:00:00', 'UTC');
225+
226+
$nyYesterday = Time::yesterday('America/New_York');
227+
$this->assertSame('2026-09-06 00:00:00', $nyYesterday->toDateTimeString());
228+
// March 28 at 23:30 does not exist in Nuuk due to the DST transition.
229+
Time::setTestNow('2026-03-29 23:30:00', 'America/Nuuk');
230+
231+
$nuukYesterday = Time::yesterday('America/Nuuk');
232+
$this->assertSame('2026-03-28 00:00:00', $nuukYesterday->toDateTimeString());
233+
Time::setTestNow();
234+
}
235+
236+
public function testTomorrowWithTimezoneAcrossDateBoundary(): void
237+
{
238+
// When UTC is 2026-09-07 23:30:00, in Tokyo it is 2026-09-08, so Tokyo tomorrow is 2026-09-09
239+
Time::setTestNow('2026-09-07 23:30:00', 'UTC');
240+
241+
$tokyoTomorrow = Time::tomorrow('Asia/Tokyo');
242+
$this->assertSame('2026-09-09 00:00:00', $tokyoTomorrow->toDateTimeString());
243+
244+
// When UTC is 2026-09-08 02:00:00, in NY it is 2026-09-07, so NY tomorrow is 2026-09-08
245+
Time::setTestNow('2026-09-08 02:00:00', 'UTC');
246+
247+
$nyTomorrow = Time::tomorrow('America/New_York');
248+
$this->assertSame('2026-09-08 00:00:00', $nyTomorrow->toDateTimeString());
249+
// Moving forward must select March 28 without normalizing into March 29.
250+
Time::setTestNow('2026-03-27 23:30:00', 'America/Nuuk');
251+
252+
$nuukTomorrow = Time::tomorrow('America/Nuuk');
253+
$this->assertSame('2026-03-28 00:00:00', $nuukTomorrow->toDateTimeString());
254+
Time::setTestNow();
255+
}
256+
193257
public function testCreateFromDate(): void
194258
{
195259
$time = Time::createFromDate(2017, 03, 05, 'America/Chicago');

‎user_guide_src/source/changelogs/v4.7.5.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Bugs Fixed
5050
- **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()``.
5151
- **Helpers:** Fixed a bug where ``get_dir_file_info()`` returned incomplete entries for subdirectories and missing files instead of omitting them.
5252
- **Honeypot:** Fixed a bug where bot detection returned an HTTP 500 response instead of 403 (Forbidden).
53+
- **I18n:** Fixed a bug where ``Time::today()``, ``Time::yesterday()``, and ``Time::tomorrow()`` ignored the specified ``$timezone`` and ``setTestNow()`` when calculating the day.
5354
- **Logger:** Fixed a bug where interpolating a log message with array or non-stringable context values could raise PHP warnings or errors.
5455
- **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.
5556

0 commit comments

Comments
 (0)