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/Events/EventServiceProvider.php b/src/Events/EventServiceProvider.php index ac0ce1c5..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('events'); + $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/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 @@ +bind(Storage::class); + } + + public function boot(): void + { $this->bind(FileContract::class, File::class); } } 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; } } } diff --git a/src/Scheduling/Console/ScheduleRunCommand.php b/src/Scheduling/Console/ScheduleRunCommand.php new file mode 100644 index 00000000..69985630 --- /dev/null +++ b/src/Scheduling/Console/ScheduleRunCommand.php @@ -0,0 +1,36 @@ +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..862ed31a --- /dev/null +++ b/src/Scheduling/Scheduler.php @@ -0,0 +1,158 @@ +closure = weakClosure($closure); + } + + 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 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 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..486f7fc6 --- /dev/null +++ b/tests/Unit/Scheduling/SchedulerTest.php @@ -0,0 +1,245 @@ +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(); +}); + +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'); +}); diff --git a/tests/Unit/Scheduling/TimerTest.php b/tests/Unit/Scheduling/TimerTest.php new file mode 100644 index 00000000..19348d1f --- /dev/null +++ b/tests/Unit/Scheduling/TimerTest.php @@ -0,0 +1,276 @@ +timer(function () use (&$count): void { + $count++; + })->everySecond(); + + $timer->reference(); + + 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(); +}); + +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); +}); diff --git a/tests/fixtures/application/config/app.php b/tests/fixtures/application/config/app.php index f956ea40..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, @@ -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/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 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 @@ +