diff --git a/src/Concerns/RegistersTickHandlers.php b/src/Concerns/RegistersTickHandlers.php index bfe9e12ce..f89cd5b67 100644 --- a/src/Concerns/RegistersTickHandlers.php +++ b/src/Concerns/RegistersTickHandlers.php @@ -6,25 +6,43 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Facades\Cache; use Laravel\Octane\Events\TickReceived; -use Laravel\Octane\Swoole\InvokeTickCallable; +use Laravel\Octane\FrankenPhp\InvokeTickCallable as FrankenPhpInvokeTickCallable; +use Laravel\Octane\FrankenPhp\ServerProcessInspector as FrankenPhpServerProcessInspector; +use Laravel\Octane\Swoole\InvokeTickCallable as SwooleInvokeTickCallable; +use Laravel\Octane\Swoole\ServerProcessInspector as SwooleServerProcessInspector; +use RuntimeException; trait RegistersTickHandlers { /** * Register a callback to be called every N seconds. * - * @return \Laravel\Octane\Swoole\InvokeTickCallable + * @return \Laravel\Octane\Swoole\InvokeTickCallable|\Laravel\Octane\FrankenPhp\InvokeTickCallable */ public function tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true) { - $listener = new InvokeTickCallable( - $key, - $callback, - $seconds, - $immediate, - Cache::store('octane'), - app(ExceptionHandler::class) - ); + $store = Cache::store('octane'); + $exceptionHandler = app(ExceptionHandler::class); + + $listener = match (true) { + $this->isSwooleServerRunning() => new SwooleInvokeTickCallable( + $key, + $callback, + $seconds, + $immediate, + $store, + $exceptionHandler + ), + $this->isFrankenPhpServerRunning() => new FrankenPhpInvokeTickCallable( + $key, + $callback, + $seconds, + $immediate, + $store, + $exceptionHandler + ), + default => throw new RuntimeException('Tick functionality is not supported in this environment.'), + }; app(Dispatcher::class)->listen( TickReceived::class, @@ -33,4 +51,22 @@ public function tick(string $key, callable $callback, int $seconds = 1, bool $im return $listener; } + + /** + * Check if the Swoole server is running. + */ + protected function isSwooleServerRunning(): bool + { + return app(SwooleServerProcessInspector::class) + ->serverIsRunning(); + } + + /** + * Check if the FrankenPHP server is running. + */ + protected function isFrankenPhpServerRunning(): bool + { + return app(FrankenPhpServerProcessInspector::class) + ->serverIsRunning(); + } } diff --git a/src/Facades/Octane.php b/src/Facades/Octane.php index 975c833c8..b723cf9ba 100644 --- a/src/Facades/Octane.php +++ b/src/Facades/Octane.php @@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Facade; /** - * @method static \Laravel\Octane\Swoole\InvokeTickCallable tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true) + * @method static \Laravel\Octane\Swoole\InvokeTickCallable|\Laravel\Octane\FrankenPhp\InvokeTickCallable tick(string $key, callable $callback, int $seconds = 1, bool $immediate = true) * @method static \Swoole\Table table(string $name) * @method static \Symfony\Component\HttpFoundation\Response invokeRoute(\Illuminate\Http\Request $request, string $method, string $uri) * @method static array concurrently(array $tasks, int $waitMilliseconds = 3000) diff --git a/src/FrankenPhp/InvokeTickCallable.php b/src/FrankenPhp/InvokeTickCallable.php new file mode 100644 index 000000000..bfcfb16d6 --- /dev/null +++ b/src/FrankenPhp/InvokeTickCallable.php @@ -0,0 +1,67 @@ +cache->get('tick-'.$this->key); + + if (! is_null($lastInvokedAt) && + (Carbon::now()->getTimestamp() - $lastInvokedAt) < $this->seconds) { + return; + } + + $this->cache->forever('tick-'.$this->key, Carbon::now()->getTimestamp()); + + if (is_null($lastInvokedAt) && ! $this->immediate) { + return; + } + + try { + call_user_func($this->callback); + } catch (Throwable $e) { + $this->exceptionHandler->report($e); + } + } + + /** + * Indicate how often the listener should be invoked. + */ + public function seconds(int $seconds): static + { + $this->seconds = $seconds; + + return $this; + } + + /** + * Indicate that the listener should be invoked on the first tick after the server starts. + */ + public function immediate(): static + { + $this->immediate = true; + + return $this; + } +} diff --git a/tests/FrankenPhpInvokeTickCallableTest.php b/tests/FrankenPhpInvokeTickCallableTest.php new file mode 100644 index 000000000..25cc59f47 --- /dev/null +++ b/tests/FrankenPhpInvokeTickCallableTest.php @@ -0,0 +1,106 @@ + $_SERVER['__test.invokeTickCallable'] = true, 1, true, + $cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class) + ); + + $cache->shouldReceive('get')->with('tick-key')->andReturn(time() - 100); + + $cache->shouldReceive('forever')->once()->with('tick-key', $now->getTimestamp()); + + $instance(); + + $this->assertTrue($_SERVER['__test.invokeTickCallable'] ?? false); + + Carbon::setTestNow(); + unset($_SERVER['__test.invokeTickCallable']); + } + + /** + * @throws \Throwable + */ + public function test_callable_is_not_invoked_when_not_due(): void + { + Carbon::setTestNow(now()); + + $_SERVER['__test.invokeTickCallable'] = false; + + $instance = new InvokeTickCallable( + 'key', fn () => $_SERVER['__test.invokeTickCallable'] = true, 30, true, + $cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class) + ); + + $cache->shouldReceive('get')->with('tick-key')->andReturn(time() - 10); + + $cache->shouldReceive('forever')->never(); + + $instance(); + + $this->assertFalse($_SERVER['__test.invokeTickCallable'] ?? false); + + Carbon::setTestNow(); + unset($_SERVER['__test.invokeTickCallable']); + } + + /** + * @throws \Throwable + */ + public function test_callable_is_invoked_when_first_run_and_immediate(): void + { + Carbon::setTestNow($now = now()); + + $instance = new InvokeTickCallable( + 'key', fn () => $_SERVER['__test.invokeTickCallable'] = true, 1, true, + $cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class) + ); + + $cache->shouldReceive('get')->with('tick-key')->andReturn(null); + + $cache->shouldReceive('forever')->once()->with('tick-key', $now->getTimestamp()); + + $instance(); + + $this->assertTrue($_SERVER['__test.invokeTickCallable'] ?? false); + + Carbon::setTestNow(); + unset($_SERVER['__test.invokeTickCallable']); + } + + public function test_callable_is_not_invoked_when_first_run_and_not_immediate() + { + Carbon::setTestNow($now = now()); + + $instance = new InvokeTickCallable( + 'key', fn () => $_SERVER['__test.invokeTickCallable'] = true, 1, false, + $cache = Mockery::mock('stdClass'), Mockery::mock(ExceptionHandler::class) + ); + + $cache->shouldReceive('get')->with('tick-key')->andReturn(null); + + $cache->shouldReceive('forever')->once()->with('tick-key', $now->getTimestamp()); + + $instance(); + + $this->assertFalse($_SERVER['__test.invokeTickCallable'] ?? false); + + Carbon::setTestNow(); + unset($_SERVER['__test.invokeTickCallable']); + } +} diff --git a/tests/InvokeTickCallableTest.php b/tests/SwooleInvokeTickCallableTest.php similarity index 98% rename from tests/InvokeTickCallableTest.php rename to tests/SwooleInvokeTickCallableTest.php index a984882bb..af9d0f5e3 100644 --- a/tests/InvokeTickCallableTest.php +++ b/tests/SwooleInvokeTickCallableTest.php @@ -7,7 +7,7 @@ use Laravel\Octane\Swoole\InvokeTickCallable; use Mockery; -class InvokeTickCallableTest extends TestCase +class SwooleInvokeTickCallableTest extends TestCase { public function test_callable_is_invoked_when_due() {