From 3f03ed29a7c4d8020fe8aa555ff42c93f8807c16 Mon Sep 17 00:00:00 2001 From: Francesco Manicardi Date: Tue, 16 Jun 2026 10:42:35 +0200 Subject: [PATCH 1/2] Make between/unlessBetween timezone-independent of call order The between/unlessBetween schedule filters evaluated the timezone eagerly when the method was called, so chaining timezone() after between() silently used the wrong timezone. Defer the time interval computation into the filter closure so $this->timezone is read at run time, making the chain order commutative (consistent with other frequency methods). Co-Authored-By: Claude Opus 4.8 --- .../Console/Scheduling/ManagesFrequencies.php | 28 ++++++++++--------- tests/Console/ConsoleScheduledEventTest.php | 21 ++++++++++++++ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php index a8f27262a38c..3d1126c423c1 100644 --- a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php +++ b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php @@ -55,21 +55,23 @@ public function unlessBetween($startTime, $endTime) */ private function inTimeInterval($startTime, $endTime) { - [$now, $startTime, $endTime] = [ - Carbon::now($this->timezone), - Carbon::parse($startTime, $this->timezone), - Carbon::parse($endTime, $this->timezone), - ]; - - if ($endTime->lessThan($startTime)) { - if ($startTime->greaterThan($now)) { - $startTime = $startTime->subDay(); - } else { - $endTime = $endTime->addDay(); + return function () use ($startTime, $endTime) { + [$now, $startTime, $endTime] = [ + Carbon::now($this->timezone), + Carbon::parse($startTime, $this->timezone), + Carbon::parse($endTime, $this->timezone), + ]; + + if ($endTime->lessThan($startTime)) { + if ($startTime->greaterThan($now)) { + $startTime = $startTime->subDay(); + } else { + $endTime = $endTime->addDay(); + } } - } - return fn () => $now->between($startTime, $endTime); + return $now->between($startTime, $endTime); + }; } /** diff --git a/tests/Console/ConsoleScheduledEventTest.php b/tests/Console/ConsoleScheduledEventTest.php index 0be6d04efbb1..8d11c9719659 100644 --- a/tests/Console/ConsoleScheduledEventTest.php +++ b/tests/Console/ConsoleScheduledEventTest.php @@ -119,6 +119,27 @@ public function testTimeBetweenChecks() $this->assertFalse($event->between('10:00', '8:00')->filtersPass($app)); } + public function testTimeBetweenChecksTimezoneCallOrder() + { + $app = m::mock(Application::class.'[isDownForMaintenance,environment]'); + $app->shouldReceive('isDownForMaintenance')->andReturn(false); + $app->shouldReceive('environment')->andReturn('production'); + + Carbon::setTestNow(Carbon::parse('2024-07-01 09:00:00', 'UTC')); + + $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); + $this->assertTrue($event->timezone('Europe/Rome')->between('10:00', '12:00')->filtersPass($app)); + + $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); + $this->assertTrue($event->between('10:00', '12:00')->timezone('Europe/Rome')->filtersPass($app)); + + $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); + $this->assertFalse($event->timezone('Europe/Rome')->unlessBetween('10:00', '12:00')->filtersPass($app)); + + $event = new Event(m::mock(EventMutex::class), 'php foo', 'UTC'); + $this->assertFalse($event->unlessBetween('10:00', '12:00')->timezone('Europe/Rome')->filtersPass($app)); + } + public function testTimeUnlessBetweenChecks() { $app = m::mock(Application::class.'[isDownForMaintenance,environment]'); From 1f32b9047ad52b68b87ab3432a8fc95a03aabf65 Mon Sep 17 00:00:00 2001 From: Francesco Manicardi Date: Wed, 17 Jun 2026 09:51:57 +0200 Subject: [PATCH 2/2] Freeze now once per run in between/unlessBetween filter Deferring the time-interval computation into the filter closure also made Carbon::now() recompute on every filtersPass() call. ScheduleRunCommand calls filtersPass() in a loop throughout the minute for repeatable sub-minute events, so the window result could flip mid-run. Capture the comparison instant once (when the filter is defined, i.e. once per schedule:run) while still reading $this->timezone lazily inside the closure, restoring the pre-existing frozen-instant behavior and keeping the call-order independence. Co-Authored-By: Claude Opus 4.8 --- .../Console/Scheduling/ManagesFrequencies.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php index 3d1126c423c1..57b972323bc1 100644 --- a/src/Illuminate/Console/Scheduling/ManagesFrequencies.php +++ b/src/Illuminate/Console/Scheduling/ManagesFrequencies.php @@ -55,9 +55,16 @@ public function unlessBetween($startTime, $endTime) */ private function inTimeInterval($startTime, $endTime) { - return function () use ($startTime, $endTime) { - [$now, $startTime, $endTime] = [ - Carbon::now($this->timezone), + $now = Carbon::now(); + + return function () use ($startTime, $endTime, $now) { + $now = $now->copy(); + + if ($this->timezone) { + $now->setTimezone($this->timezone); + } + + [$startTime, $endTime] = [ Carbon::parse($startTime, $this->timezone), Carbon::parse($endTime, $this->timezone), ];