From a8904684a413d39b3ee69f4af3aac6fe9de2399d Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Tue, 16 Dec 2025 20:56:39 -0500 Subject: [PATCH 1/9] feat(EventServiceProvider): update event loading path and add sample event listener --- src/Events/EventServiceProvider.php | 2 +- .../fixtures/application/{events/app.php => listen/events.php} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/fixtures/application/{events/app.php => listen/events.php} (100%) diff --git a/src/Events/EventServiceProvider.php b/src/Events/EventServiceProvider.php index ac0ce1c5..64e2fc4e 100644 --- a/src/Events/EventServiceProvider.php +++ b/src/Events/EventServiceProvider.php @@ -37,7 +37,7 @@ public function boot(): void private function loadEvents(): void { - $eventPath = base_path('events'); + $eventPath = base_path('listen'); if (File::exists($eventPath)) { foreach (File::listFilesRecursively($eventPath, '.php') as $file) { diff --git a/tests/fixtures/application/events/app.php b/tests/fixtures/application/listen/events.php similarity index 100% rename from tests/fixtures/application/events/app.php rename to tests/fixtures/application/listen/events.php From ffc421e040121ad47f1a7697240cdaaea2498424 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Wed, 17 Dec 2025 11:47:37 -0500 Subject: [PATCH 2/9] fix(EventServiceProvider, RouteServiceProvider): update event loading logic to require specific event and route files --- src/Events/EventServiceProvider.php | 8 +++----- src/Routing/RouteServiceProvider.php | 7 +++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Events/EventServiceProvider.php b/src/Events/EventServiceProvider.php index 64e2fc4e..1e1882ff 100644 --- a/src/Events/EventServiceProvider.php +++ b/src/Events/EventServiceProvider.php @@ -37,12 +37,10 @@ public function boot(): void private function loadEvents(): void { - $eventPath = base_path('listen'); + $eventsPath = base_path('listen' . DIRECTORY_SEPARATOR . 'events.php'); - if (File::exists($eventPath)) { - foreach (File::listFilesRecursively($eventPath, '.php') as $file) { - require $file; - } + if (File::exists($eventsPath)) { + require $eventsPath; } } } diff --git a/src/Routing/RouteServiceProvider.php b/src/Routing/RouteServiceProvider.php index ac2b23b2..caa759a6 100644 --- a/src/Routing/RouteServiceProvider.php +++ b/src/Routing/RouteServiceProvider.php @@ -4,6 +4,7 @@ namespace Phenix\Routing; +use Phenix\Facades\File; use Phenix\Providers\ServiceProvider; use Phenix\Routing\Console\RouteList; use Phenix\Util\Directory; @@ -41,8 +42,10 @@ private function getControllersPath(): string private function loadRoutes(): void { - foreach (Directory::all(base_path('routes')) as $file) { - require $file; + $routesPath = base_path('routes' . DIRECTORY_SEPARATOR . 'api.php'); + + if (File::exists($routesPath)) { + require $routesPath; } } } From 8ddec7ae4770bd38f103e4a87edb307780afccea Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 19 Dec 2025 09:05:51 -0500 Subject: [PATCH 3/9] feat(Scheduling): implement task scheduling system with timer and scheduler classes --- composer.json | 1 + src/App.php | 3 + src/Facades/Schedule.php | 24 +++ src/Scheduling/Console/ScheduleRunCommand.php | 36 ++++ .../Console/ScheduleWorkCommand.php | 36 ++++ src/Scheduling/Schedule.php | 44 +++++ src/Scheduling/ScheduleWorker.php | 77 ++++++++ src/Scheduling/Scheduler.php | 173 ++++++++++++++++++ src/Scheduling/SchedulingServiceProvider.php | 35 ++++ src/Scheduling/Timer.php | 158 ++++++++++++++++ src/Scheduling/TimerRegistry.php | 27 +++ .../Console/ScheduleRunCommandTest.php | 21 +++ .../Console/ScheduleWorkCommandTest.php | 47 +++++ tests/Unit/Scheduling/SchedulerTest.php | 113 ++++++++++++ tests/Unit/Scheduling/TimerTest.php | 141 ++++++++++++++ tests/fixtures/application/config/app.php | 1 + .../application/schedule/schedules.php | 3 + 17 files changed, 940 insertions(+) create mode 100644 src/Facades/Schedule.php create mode 100644 src/Scheduling/Console/ScheduleRunCommand.php create mode 100644 src/Scheduling/Console/ScheduleWorkCommand.php create mode 100644 src/Scheduling/Schedule.php create mode 100644 src/Scheduling/ScheduleWorker.php create mode 100644 src/Scheduling/Scheduler.php create mode 100644 src/Scheduling/SchedulingServiceProvider.php create mode 100644 src/Scheduling/Timer.php create mode 100644 src/Scheduling/TimerRegistry.php create mode 100644 tests/Unit/Scheduling/Console/ScheduleRunCommandTest.php create mode 100644 tests/Unit/Scheduling/Console/ScheduleWorkCommandTest.php create mode 100644 tests/Unit/Scheduling/SchedulerTest.php create mode 100644 tests/Unit/Scheduling/TimerTest.php create mode 100644 tests/fixtures/application/schedule/schedules.php diff --git a/composer.json b/composer.json index 285abb52..836b55eb 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "amphp/postgres": "v2.0.0", "amphp/redis": "^2.0", "amphp/socket": "^2.1.0", + "dragonmantank/cron-expression": "^3.6", "egulias/email-validator": "^4.0", "fakerphp/faker": "^1.23", "kelunik/rate-limit": "^3.0", diff --git a/src/App.php b/src/App.php index 9071f6fb..2ec1a551 100644 --- a/src/App.php +++ b/src/App.php @@ -35,6 +35,7 @@ use Phenix\Http\Constants\Protocol; use Phenix\Logging\LoggerFactory; use Phenix\Runtime\Log; +use Phenix\Scheduling\TimerRegistry; use Phenix\Session\SessionMiddlewareFactory; use function Amp\async; @@ -116,6 +117,8 @@ public function run(): void $this->isRunning = true; + TimerRegistry::run(); + if ($this->serverMode === ServerMode::CLUSTER && $this->signalTrapping) { async(function (): void { Cluster::awaitTermination(); diff --git a/src/Facades/Schedule.php b/src/Facades/Schedule.php new file mode 100644 index 00000000..64218c62 --- /dev/null +++ b/src/Facades/Schedule.php @@ -0,0 +1,24 @@ +run(); + + return Command::SUCCESS; + } +} diff --git a/src/Scheduling/Console/ScheduleWorkCommand.php b/src/Scheduling/Console/ScheduleWorkCommand.php new file mode 100644 index 00000000..f9040c7e --- /dev/null +++ b/src/Scheduling/Console/ScheduleWorkCommand.php @@ -0,0 +1,36 @@ +daemon($output); + + return Command::SUCCESS; + } +} diff --git a/src/Scheduling/Schedule.php b/src/Scheduling/Schedule.php new file mode 100644 index 00000000..3ca20366 --- /dev/null +++ b/src/Scheduling/Schedule.php @@ -0,0 +1,44 @@ + + */ + protected array $schedules = []; + + public function timer(Closure $closure): Timer + { + $timer = new Timer($closure); + + TimerRegistry::add($timer); + + return $timer; + } + + public function call(Closure $closure): Scheduler + { + $scheduler = new Scheduler($closure); + + $this->schedules[] = $scheduler; + + return $scheduler; + } + + public function run(): void + { + $now = null; + foreach ($this->schedules as $scheduler) { + $now ??= Date::now('UTC'); + + $scheduler->tick($now); + } + } +} diff --git a/src/Scheduling/ScheduleWorker.php b/src/Scheduling/ScheduleWorker.php new file mode 100644 index 00000000..c1fa4632 --- /dev/null +++ b/src/Scheduling/ScheduleWorker.php @@ -0,0 +1,77 @@ +writeln('Starting schedule worker...'); + + $this->listenSignals(); + + $lastRunKey = null; + + while (true) { + if ($this->shouldQuit()) { + break; + } + + $this->sleepMicroseconds(100_000); // 100ms + + $now = $this->now(); + + if ($now->second !== 0) { + continue; + } + + $currentKey = $now->format('Y-m-d H:i'); + + if ($currentKey === $lastRunKey) { + continue; + } + + Schedule::run(); + + $lastRunKey = $currentKey; + } + + $output?->writeln('Schedule worker stopped.'); + } + + public function shouldQuit(): bool + { + return $this->quit; + } + + protected function sleepMicroseconds(int $microseconds): void + { + usleep($microseconds); + } + + protected function now(): Date + { + return Date::now('UTC'); + } + + protected function listenSignals(): void + { + pcntl_async_signals(true); + + pcntl_signal(SIGINT, function (): void { + $this->quit = true; + }); + + pcntl_signal(SIGTERM, function (): void { + $this->quit = true; + }); + } +} diff --git a/src/Scheduling/Scheduler.php b/src/Scheduling/Scheduler.php new file mode 100644 index 00000000..605b3f46 --- /dev/null +++ b/src/Scheduling/Scheduler.php @@ -0,0 +1,173 @@ +closure = weakClosure($closure); + } + + public function setCron(string $expression): self + { + return $this->setExpressionString($expression); + } + + public function hourly(): self + { + return $this->setExpressionString('@hourly'); + } + + public function daily(): self + { + return $this->setExpressionString('@daily'); + } + + public function weekly(): self + { + return $this->setExpressionString('@weekly'); + } + + public function monthly(): self + { + return $this->setExpressionString('@monthly'); + } + + public function everyMinute(): self + { + return $this->setExpressionString('* * * * *'); + } + + public function everyFiveMinutes(): self + { + return $this->setExpressionString('*/5 * * * *'); + } + + public function everyTenMinutes(): self + { + return $this->setExpressionString('*/10 * * * *'); + } + + public function everyFifteenMinutes(): self + { + return $this->setExpressionString('*/15 * * * *'); + } + + public function everyThirtyMinutes(): self + { + return $this->setExpressionString('*/30 * * * *'); + } + + public function everyTwoHours(): self + { + return $this->setExpressionString('0 */2 * * *'); + } + + public function everyDay(): self + { + return $this->daily(); + } + + public function everyTwoDays(): self + { + return $this->setExpressionString('0 0 */2 * *'); + } + + public function everyWeekday(): self + { + return $this->setExpressionString('0 0 * * 1-5'); + } + + public function everyWeekend(): self + { + return $this->setExpressionString('0 0 * * 6,0'); + } + + public function mondays(): self + { + return $this->setExpressionString('0 0 * * 1'); + } + + public function fridays(): self + { + return $this->setExpressionString('0 0 * * 5'); + } + + public function dailyAt(string $time): self + { + return $this->daily()->at($time); + } + + public function weeklyAt(string $time): self + { + return $this->weekly()->at($time); + } + + public function everyWeekly(): self + { + return $this->weekly(); + } + + public function at(string $time): self + { + [$hour, $minute] = array_map('intval', explode(':', $time)); + + $expr = $this->expression?->getExpression() ?? '* * * * *'; + + $parts = explode(' ', $expr); + + if (count($parts) === 5) { + $parts[0] = (string) $minute; + $parts[1] = (string) $hour; + } + + $this->expression = new CronExpression(implode(' ', $parts)); + + return $this; + } + + public function timezone(string $tz): self + { + $this->timezone = $tz; + + return $this; + } + + protected function setExpressionString(string $expression): self + { + $this->expression = new CronExpression($expression); + + return $this; + } + + public function tick(Date|null $now = null): void + { + if (! $this->expression) { + return; + } + + $now ??= Date::now(); + $localNow = $now->copy()->timezone($this->timezone); + + if ($this->expression->isDue($localNow)) { + ($this->closure)(); + } + } +} diff --git a/src/Scheduling/SchedulingServiceProvider.php b/src/Scheduling/SchedulingServiceProvider.php new file mode 100644 index 00000000..e5ae6425 --- /dev/null +++ b/src/Scheduling/SchedulingServiceProvider.php @@ -0,0 +1,35 @@ +bind(Schedule::class)->setShared(true); + $this->bind(ScheduleWorker::class); + + $this->commands([ + ScheduleWorkCommand::class, + ScheduleRunCommand::class, + ]); + + $this->loadSchedules(); + } + + private function loadSchedules(): void + { + $schedulePath = base_path('schedule' . DIRECTORY_SEPARATOR . 'schedules.php'); + + if (File::exists($schedulePath)) { + require $schedulePath; + } + } +} diff --git a/src/Scheduling/Timer.php b/src/Scheduling/Timer.php new file mode 100644 index 00000000..ff122b47 --- /dev/null +++ b/src/Scheduling/Timer.php @@ -0,0 +1,158 @@ +closure = weakClosure($closure); + } + + public function seconds(float $seconds): self + { + $this->interval = max(0.001, $seconds); + + return $this; + } + + public function milliseconds(int $milliseconds): self + { + $this->interval = max(0.001, $milliseconds / 1000); + + return $this; + } + + public function everySecond(): self + { + return $this->seconds(1); + } + + public function everyTwoSeconds(): self + { + return $this->seconds(2); + } + + public function everyFiveSeconds(): self + { + return $this->seconds(5); + } + + public function everyTenSeconds(): self + { + return $this->seconds(10); + } + + public function everyFifteenSeconds(): self + { + return $this->seconds(15); + } + + public function everyThirtySeconds(): self + { + return $this->seconds(30); + } + + public function everyMinute(): self + { + return $this->seconds(60); + } + + public function everyTwoMinutes(): self + { + return $this->seconds(120); + } + + public function everyFiveMinutes(): self + { + return $this->seconds(300); + } + + public function everyTenMinutes(): self + { + return $this->seconds(600); + } + + public function everyFifteenMinutes(): self + { + return $this->seconds(900); + } + + public function everyThirtyMinutes(): self + { + return $this->seconds(1800); + } + + public function hourly(): self + { + return $this->seconds(3600); + } + + public function reference(): self + { + $this->reference = true; + + if ($this->timer) { + $this->timer->reference(); + } + + return $this; + } + + public function unreference(): self + { + $this->reference = false; + + if ($this->timer) { + $this->timer->unreference(); + } + + return $this; + } + + public function run(): self + { + $this->timer = new Interval($this->interval, $this->closure, $this->reference); + + return $this; + } + + public function enable(): self + { + if ($this->timer) { + $this->timer->enable(); + } + + return $this; + } + + public function disable(): self + { + if ($this->timer) { + $this->timer->disable(); + } + + return $this; + } + + public function isEnabled(): bool + { + return $this->timer?->isEnabled() ?? false; + } +} diff --git a/src/Scheduling/TimerRegistry.php b/src/Scheduling/TimerRegistry.php new file mode 100644 index 00000000..f8e6fc22 --- /dev/null +++ b/src/Scheduling/TimerRegistry.php @@ -0,0 +1,27 @@ + + */ + protected static array $timers = []; + + public static function add(Timer $timer): void + { + self::$timers[] = $timer; + } + + public static function run(): void + { + foreach (self::$timers as $timer) { + $timer->run(); + } + + self::$timers = []; + } +} diff --git a/tests/Unit/Scheduling/Console/ScheduleRunCommandTest.php b/tests/Unit/Scheduling/Console/ScheduleRunCommandTest.php new file mode 100644 index 00000000..a207fbca --- /dev/null +++ b/tests/Unit/Scheduling/Console/ScheduleRunCommandTest.php @@ -0,0 +1,21 @@ +everyMinute(); + + /** @var CommandTester $command */ + $command = $this->phenix('schedule:run'); + + $command->assertCommandIsSuccessful(); + + expect($executed)->toBeTrue(); +}); diff --git a/tests/Unit/Scheduling/Console/ScheduleWorkCommandTest.php b/tests/Unit/Scheduling/Console/ScheduleWorkCommandTest.php new file mode 100644 index 00000000..9dd7e4bd --- /dev/null +++ b/tests/Unit/Scheduling/Console/ScheduleWorkCommandTest.php @@ -0,0 +1,47 @@ +getMockBuilder(ScheduleWorker::class) + ->disableOriginalConstructor() + ->getMock(); + + $worker->expects($this->once()) + ->method('daemon'); + + $this->app->swap(ScheduleWorker::class, $worker); + + /** @var CommandTester $command */ + $command = $this->phenix('schedule:work'); + + $command->assertCommandIsSuccessful(); +}); + +it('breaks execution when quit signal is received', function (): void { + $worker = $this->getMockBuilder(ScheduleWorker::class) + ->onlyMethods(['shouldQuit', 'sleepMicroseconds', 'listenSignals', 'now']) + ->getMock(); + + $worker->expects($this->once()) + ->method('listenSignals'); + + $worker->expects($this->exactly(2)) + ->method('shouldQuit') + ->willReturnOnConsecutiveCalls(false, true); + + $worker->method('sleepMicroseconds'); + + $worker->method('now')->willReturn(Date::now('UTC')->startOfMinute()); + + $this->app->swap(ScheduleWorker::class, $worker); + + /** @var CommandTester $command */ + $command = $this->phenix('schedule:work'); + + $command->assertCommandIsSuccessful(); +}); diff --git a/tests/Unit/Scheduling/SchedulerTest.php b/tests/Unit/Scheduling/SchedulerTest.php new file mode 100644 index 00000000..3bd89373 --- /dev/null +++ b/tests/Unit/Scheduling/SchedulerTest.php @@ -0,0 +1,113 @@ +call(function () use (&$executed): void { + $executed = true; + })->everyMinute(); + + $now = Date::now('UTC')->startOfMinute()->addSeconds(30); + + $scheduler->tick($now); + + expect($executed)->toBeTrue(); +}); + +it('does not execute when not due (dailyAt time mismatch)', function (): void { + $schedule = new Schedule(); + + $executed = false; + + $scheduler = $schedule->call(function () use (&$executed): void { + $executed = true; + })->dailyAt('10:15'); + + $now = Date::now('UTC')->startOfMinute(); + + $scheduler->tick($now); + + expect($executed)->toBeFalse(); + + $now2 = Date::now('UTC')->startOfMinute()->addMinute(); + + $scheduler->tick($now2); + + expect($executed)->toBeFalse(); +}); + +it('executes exactly at matching dailyAt time', function (): void { + $schedule = new Schedule(); + + $executed = false; + + $scheduler = $schedule->call(function () use (&$executed): void { + $executed = true; + })->dailyAt('10:15'); + + $now = Date::now('UTC')->startOfMinute()->setTime(10, 15); + + $scheduler->tick($now); + + expect($executed)->toBeTrue(); +}); + +it('respects timezone when evaluating due', function (): void { + $schedule = new Schedule(); + + $executed = false; + + $scheduler = $schedule->call(function () use (&$executed): void { + $executed = true; + })->dailyAt('12:00')->timezone('America/New_York'); + + $now = Date::now('UTC')->startOfMinute()->setTime(17, 0); + + $scheduler->tick($now); + + expect($executed)->toBeTrue(); +}); + +it('supports */5 minutes schedule and only runs on multiples of five', function (): void { + $schedule = new Schedule(); + + $executed = false; + + $scheduler = $schedule->call(function () use (&$executed): void { + $executed = true; + })->everyFiveMinutes(); + + $notDue = Date::now('UTC')->startOfMinute()->setTime(10, 16); + + $scheduler->tick($notDue); + + expect($executed)->toBeFalse(); + + $due = Date::now('UTC')->startOfMinute()->setTime(10, 15); + + $scheduler->tick($due); + + expect($executed)->toBeTrue(); +}); + +it('does nothing when no expression is set', function (): void { + $executed = false; + + $scheduler = new Scheduler(function () use (&$executed): void { + $executed = true; + }); + + $now = Date::now('UTC')->startOfDay(); + + $scheduler->tick($now); + + expect($executed)->toBeFalse(); +}); diff --git a/tests/Unit/Scheduling/TimerTest.php b/tests/Unit/Scheduling/TimerTest.php new file mode 100644 index 00000000..35ad2598 --- /dev/null +++ b/tests/Unit/Scheduling/TimerTest.php @@ -0,0 +1,141 @@ +timer(function () use (&$count): void { + $count++; + })->everySecond(); + + TimerRegistry::run(); + + delay(2.2); + + expect($count)->toBeGreaterThanOrEqual(2); + + $timer->disable(); + + $afterDisable = $count; + + delay(1.5); + + expect($count)->toBe($afterDisable); +}); + +it('can be re-enabled after disable', function (): void { + $schedule = new Schedule(); + + $count = 0; + + $timer = $schedule->timer(function () use (&$count): void { + $count++; + })->everySecond(); + + TimerRegistry::run(); + + delay(1.1); + + expect($count)->toBeGreaterThanOrEqual(1); + + $timer->disable(); + + $paused = $count; + + delay(1.2); + + expect($count)->toBe($paused); + + $timer->enable(); + + delay(1.2); + + expect($count)->toBeGreaterThan($paused); + + $timer->disable(); +}); + +it('supports millisecond intervals', function (): void { + $schedule = new Schedule(); + + $count = 0; + + $timer = $schedule->timer(function () use (&$count): void { + $count++; + })->milliseconds(100); + + TimerRegistry::run(); + + delay(0.35); + + expect($count)->toBeGreaterThanOrEqual(2); + + $timer->disable(); +}); + +it('unreference does not prevent execution', function (): void { + $schedule = new Schedule(); + + $executed = false; + + $timer = $schedule->timer(function () use (&$executed): void { + $executed = true; + })->everySecond()->unreference(); + + TimerRegistry::run(); + + delay(1.2); + + expect($executed)->toBeTrue(); + + $timer->disable(); +}); + +it('reports enabled state correctly', function (): void { + $schedule = new Schedule(); + + $timer = $schedule->timer(function (): void { + // no-op + })->everySecond(); + + expect($timer->isEnabled())->toBeFalse(); + + TimerRegistry::run(); + + expect($timer->isEnabled())->toBeTrue(); + + $timer->disable(); + + expect($timer->isEnabled())->toBeFalse(); + + $timer->enable(); + + expect($timer->isEnabled())->toBeTrue(); + + $timer->disable(); +}); + +it('runs at given using facade', function (): void { + $timerExecuted = false; + + $timer = ScheduleFacade::timer(function () use (&$timerExecuted): void { + $timerExecuted = true; + })->everySecond(); + + TimerRegistry::run(); + + delay(2); + + expect($timerExecuted)->toBeTrue(); + + $timer->disable(); +}); diff --git a/tests/fixtures/application/config/app.php b/tests/fixtures/application/config/app.php index f956ea40..78ae3613 100644 --- a/tests/fixtures/application/config/app.php +++ b/tests/fixtures/application/config/app.php @@ -78,6 +78,7 @@ \Phenix\Queue\QueueServiceProvider::class, \Phenix\Events\EventServiceProvider::class, \Phenix\Translation\TranslationServiceProvider::class, + \Phenix\Scheduling\SchedulingServiceProvider::class, \Phenix\Validation\ValidationServiceProvider::class, ], 'response' => [ diff --git a/tests/fixtures/application/schedule/schedules.php b/tests/fixtures/application/schedule/schedules.php new file mode 100644 index 00000000..174d7fd7 --- /dev/null +++ b/tests/fixtures/application/schedule/schedules.php @@ -0,0 +1,3 @@ + Date: Fri, 19 Dec 2025 09:06:00 -0500 Subject: [PATCH 4/9] feat(Filesystem): update FilesystemServiceProvider registration in app config --- src/Filesystem/FilesystemServiceProvider.php | 4 ++++ tests/fixtures/application/config/app.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Filesystem/FilesystemServiceProvider.php b/src/Filesystem/FilesystemServiceProvider.php index 1e4576ef..9c56adab 100644 --- a/src/Filesystem/FilesystemServiceProvider.php +++ b/src/Filesystem/FilesystemServiceProvider.php @@ -19,6 +19,10 @@ public function provides(string $id): bool public function register(): void { $this->bind(Storage::class); + } + + public function boot(): void + { $this->bind(FileContract::class, File::class); } } diff --git a/tests/fixtures/application/config/app.php b/tests/fixtures/application/config/app.php index 78ae3613..616d6d63 100644 --- a/tests/fixtures/application/config/app.php +++ b/tests/fixtures/application/config/app.php @@ -64,12 +64,12 @@ ], ], 'providers' => [ + \Phenix\Filesystem\FilesystemServiceProvider::class, \Phenix\Console\CommandsServiceProvider::class, \Phenix\Routing\RouteServiceProvider::class, \Phenix\Database\DatabaseServiceProvider::class, \Phenix\Redis\RedisServiceProvider::class, \Phenix\Auth\AuthServiceProvider::class, - \Phenix\Filesystem\FilesystemServiceProvider::class, \Phenix\Tasks\TaskServiceProvider::class, \Phenix\Views\ViewServiceProvider::class, \Phenix\Cache\CacheServiceProvider::class, From fdabed69a1a862d71ff64c9f8488657af9dace12 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 19 Dec 2025 13:04:50 -0500 Subject: [PATCH 5/9] feat(TimerTest): add tests for various timer interval settings --- tests/Unit/Scheduling/TimerTest.php | 133 ++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/Unit/Scheduling/TimerTest.php b/tests/Unit/Scheduling/TimerTest.php index 35ad2598..6388d8d1 100644 --- a/tests/Unit/Scheduling/TimerTest.php +++ b/tests/Unit/Scheduling/TimerTest.php @@ -4,6 +4,7 @@ use Phenix\Facades\Schedule as ScheduleFacade; use Phenix\Scheduling\Schedule; +use Phenix\Scheduling\Timer; use Phenix\Scheduling\TimerRegistry; use function Amp\delay; @@ -139,3 +140,135 @@ $timer->disable(); }); + +it('sets interval for every two seconds', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyTwoSeconds(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(2.0); +}); + +it('sets interval for every five seconds', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyFiveSeconds(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(5.0); +}); + +it('sets interval for every ten seconds', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyTenSeconds(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(10.0); +}); + +it('sets interval for every fifteen seconds', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyFifteenSeconds(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(15.0); +}); + +it('sets interval for every thirty seconds', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyThirtySeconds(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(30.0); +}); + +it('sets interval for every minute', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyMinute(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(60.0); +}); + +it('sets interval for every two minutes', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyTwoMinutes(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(120.0); +}); + +it('sets interval for every five minutes', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyFiveMinutes(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(300.0); +}); + +it('sets interval for every ten minutes', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyTenMinutes(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(600.0); +}); + +it('sets interval for every fifteen minutes', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyFifteenMinutes(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(900.0); +}); + +it('sets interval for every thirty minutes', function (): void { + $timer = new Timer(function (): void {}); + $timer->everyThirtyMinutes(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(1800.0); +}); + +it('sets interval for hourly', function (): void { + $timer = new Timer(function (): void {}); + $timer->hourly(); + + $ref = new ReflectionClass($timer); + $prop = $ref->getProperty('interval'); + $prop->setAccessible(true); + + expect($prop->getValue($timer))->toBe(3600.0); +}); From 31a91cade96b688900f2b14503eb554a708c08d1 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 19 Dec 2025 13:05:56 -0500 Subject: [PATCH 6/9] feat(TimerTest): add reference call to timer in test for proper execution --- tests/Unit/Scheduling/TimerTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Unit/Scheduling/TimerTest.php b/tests/Unit/Scheduling/TimerTest.php index 6388d8d1..19348d1f 100644 --- a/tests/Unit/Scheduling/TimerTest.php +++ b/tests/Unit/Scheduling/TimerTest.php @@ -18,6 +18,8 @@ $count++; })->everySecond(); + $timer->reference(); + TimerRegistry::run(); delay(2.2); From 339f1bc872a0e314311c157859aea4978e3efdac Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 19 Dec 2025 13:15:08 -0500 Subject: [PATCH 7/9] refactor(Scheduler): remove unused cron methods for cleaner API --- src/Scheduling/Scheduler.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Scheduling/Scheduler.php b/src/Scheduling/Scheduler.php index 605b3f46..862ed31a 100644 --- a/src/Scheduling/Scheduler.php +++ b/src/Scheduling/Scheduler.php @@ -25,11 +25,6 @@ public function __construct( $this->closure = weakClosure($closure); } - public function setCron(string $expression): self - { - return $this->setExpressionString($expression); - } - public function hourly(): self { return $this->setExpressionString('@hourly'); @@ -80,11 +75,6 @@ public function everyTwoHours(): self return $this->setExpressionString('0 */2 * * *'); } - public function everyDay(): self - { - return $this->daily(); - } - public function everyTwoDays(): self { return $this->setExpressionString('0 0 */2 * *'); @@ -120,11 +110,6 @@ public function weeklyAt(string $time): self return $this->weekly()->at($time); } - public function everyWeekly(): self - { - return $this->weekly(); - } - public function at(string $time): self { [$hour, $minute] = array_map('intval', explode(':', $time)); From 6f2305ee9e60168920020e423186f7990bf50673 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 19 Dec 2025 13:15:14 -0500 Subject: [PATCH 8/9] feat(SchedulerTest): add tests for various cron expressions and scheduling intervals --- tests/Unit/Scheduling/SchedulerTest.php | 133 ++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/Unit/Scheduling/SchedulerTest.php b/tests/Unit/Scheduling/SchedulerTest.php index 3bd89373..d5e48e8e 100644 --- a/tests/Unit/Scheduling/SchedulerTest.php +++ b/tests/Unit/Scheduling/SchedulerTest.php @@ -111,3 +111,136 @@ expect($executed)->toBeFalse(); }); + +it('sets cron for weekly', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->weekly(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 0 * * 0'); +}); + +it('sets cron for monthly', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->monthly(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 0 1 * *'); +}); + +it('sets cron for every ten minutes', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->everyTenMinutes(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('*/10 * * * *'); +}); + +it('sets cron for every fifteen minutes', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->everyFifteenMinutes(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('*/15 * * * *'); +}); + +it('sets cron for every thirty minutes', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->everyThirtyMinutes(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('*/30 * * * *'); +}); + +it('sets cron for every two hours', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->everyTwoHours(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 */2 * * *'); +}); + +it('sets cron for every two days', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->everyTwoDays(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 0 */2 * *'); +}); + +it('sets cron for every weekday', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->everyWeekday(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 0 * * 1-5'); +}); + +it('sets cron for every weekend', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->everyWeekend(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 0 * * 6,0'); +}); + +it('sets cron for mondays', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->mondays(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 0 * * 1'); +}); + +it('sets cron for fridays', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->fridays(); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('0 0 * * 5'); +}); + +it('sets cron for weeklyAt at specific time', function (): void { + $scheduler = (new Schedule())->call(function (): void {})->weeklyAt('10:15'); + + $ref = new ReflectionClass($scheduler); + $prop = $ref->getProperty('expression'); + $prop->setAccessible(true); + $expr = $prop->getValue($scheduler); + + expect($expr->getExpression())->toBe('15 10 * * 0'); +}); + From 54e91e0fd545b9091a104a3865aa13521a32cbf1 Mon Sep 17 00:00:00 2001 From: barbosa89 Date: Fri, 19 Dec 2025 13:48:53 -0500 Subject: [PATCH 9/9] style: php cs --- tests/Unit/Scheduling/SchedulerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Unit/Scheduling/SchedulerTest.php b/tests/Unit/Scheduling/SchedulerTest.php index d5e48e8e..486f7fc6 100644 --- a/tests/Unit/Scheduling/SchedulerTest.php +++ b/tests/Unit/Scheduling/SchedulerTest.php @@ -243,4 +243,3 @@ expect($expr->getExpression())->toBe('15 10 * * 0'); }); -