├── src ├── Commands │ ├── stubs │ │ ├── rr.yaml │ │ ├── frankenphp-worker.php │ │ └── Caddyfile │ ├── Command.php │ ├── Concerns │ │ ├── InteractsWithTerminal.php │ │ ├── InteractsWithEnvironmentVariables.php │ │ ├── InteractsWithServers.php │ │ ├── InstallsRoadRunnerDependencies.php │ │ └── InstallsFrankenPhpDependencies.php │ ├── StatusCommand.php │ ├── ReloadCommand.php │ ├── StopCommand.php │ ├── InstallCommand.php │ ├── StartCommand.php │ └── StartSwooleCommand.php ├── Exceptions │ ├── ServerShutdownException.php │ ├── ValueTooLargeForColumnException.php │ ├── WorkerException.php │ ├── TaskTimeoutException.php │ ├── DdException.php │ ├── TaskException.php │ └── TaskExceptionResult.php ├── Swoole │ ├── TaskResult.php │ ├── Handlers │ │ ├── OnManagerStart.php │ │ ├── OnServerStart.php │ │ └── OnWorkerStart.php │ ├── SignalDispatcher.php │ ├── SwooleCoroutineDispatcher.php │ ├── Actions │ │ ├── EnsureRequestsDontExceedMaxExecutionTime.php │ │ └── ConvertSwooleRequestToIlluminateRequest.php │ ├── SwooleExtension.php │ ├── InvokeTickCallable.php │ ├── ServerProcessInspector.php │ ├── ServerStateFile.php │ ├── SwooleTaskDispatcher.php │ └── SwooleHttpTaskDispatcher.php ├── Events │ ├── WorkerStarting.php │ ├── WorkerStopping.php │ ├── TickReceived.php │ ├── WorkerErrorOccurred.php │ ├── TaskReceived.php │ ├── RequestReceived.php │ ├── RequestHandled.php │ ├── TickTerminated.php │ ├── TaskTerminated.php │ ├── HasApplicationAndSandbox.php │ └── RequestTerminated.php ├── Contracts │ ├── StoppableClient.php │ ├── DispatchesCoroutines.php │ ├── OperationTerminated.php │ ├── ServerProcessInspector.php │ ├── ServesStaticFiles.php │ ├── DispatchesTasks.php │ ├── Worker.php │ └── Client.php ├── OctaneResponse.php ├── Exec.php ├── Listeners │ ├── GiveNewApplicationInstanceToSessionManager.php │ ├── FlushStrCache.php │ ├── CreateConfigurationSandbox.php │ ├── FlushOnce.php │ ├── FlushArrayCache.php │ ├── GiveNewRequestInstanceToPaginator.php │ ├── DisconnectFromDatabases.php │ ├── GiveNewRequestInstanceToApplication.php │ ├── FlushQueuedCookies.php │ ├── GiveNewApplicationInstanceToHttpKernel.php │ ├── CollectGarbage.php │ ├── GiveNewApplicationInstanceToRouter.php │ ├── EnsureUploadedFilesAreValid.php │ ├── FlushSessionState.php │ ├── EnforceRequestScheme.php │ ├── FlushDatabaseQueryLog.php │ ├── StopWorkerIfNecessary.php │ ├── GiveNewApplicationInstanceToAuthorizationGate.php │ ├── EnsureUploadedFilesCanBeMoved.php │ ├── FlushDatabaseRecordModificationState.php │ ├── GiveNewApplicationInstanceToQueueManager.php │ ├── GiveNewApplicationInstanceToValidationFactory.php │ ├── EnsureRequestServerPortMatchesScheme.php │ ├── GiveNewApplicationInstanceToFilesystemManager.php │ ├── GiveNewApplicationInstanceToPipelineHub.php │ ├── GiveNewApplicationInstanceToViewFactory.php │ ├── GiveNewApplicationInstanceToMailManager.php │ ├── PrepareInertiaForNextOperation.php │ ├── PrepareLivewireForNextOperation.php │ ├── FlushTranslatorCache.php │ ├── GiveNewApplicationInstanceToDatabaseManager.php │ ├── FlushMonologState.php │ ├── GiveNewApplicationInstanceToCacheManager.php │ ├── PrepareScoutForNextOperation.php │ ├── FlushUploadedFiles.php │ ├── FlushAuthenticationState.php │ ├── GiveNewApplicationInstanceToNotificationChannelManager.php │ ├── PrepareSocialiteForNextOperation.php │ ├── FlushTemporaryContainerInstances.php │ ├── GiveNewApplicationInstanceToDatabaseSessionHandler.php │ ├── GiveNewApplicationInstanceToBroadcastManager.php │ ├── FlushLogContext.php │ ├── RefreshQueryDurationHandling.php │ ├── ReportException.php │ └── FlushLocaleState.php ├── PosixExtension.php ├── FrankenPhp │ ├── Concerns │ │ └── FindsFrankenPhpBinary.php │ ├── FrankenPhpClient.php │ ├── ServerStateFile.php │ └── ServerProcessInspector.php ├── DispatchesEvents.php ├── SymfonyProcessFactory.php ├── SequentialCoroutineDispatcher.php ├── CurrentApplication.php ├── Cache │ ├── OctaneArrayStore.php │ └── OctaneStore.php ├── RoadRunner │ ├── Concerns │ │ └── FindsRoadRunnerBinary.php │ ├── ServerStateFile.php │ ├── ServerProcessInspector.php │ └── RoadRunnerClient.php ├── WorkerExceptionInspector.php ├── Testing │ └── Fakes │ │ ├── FakeWorker.php │ │ └── FakeClient.php ├── Facades │ └── Octane.php ├── Tables │ ├── OpenSwooleTable.php │ ├── TableFactory.php │ ├── Concerns │ │ └── EnsuresColumnSizes.php │ └── SwooleTable.php ├── RequestContext.php ├── Concerns │ ├── RegistersTickHandlers.php │ ├── ProvidesRouting.php │ ├── ProvidesConcurrencySupport.php │ └── ProvidesDefaultConfigurationOptions.php ├── Octane.php ├── SequentialTaskDispatcher.php ├── ApplicationGateway.php ├── Stream.php ├── MarshalsPsr7RequestsAndResponses.php └── ApplicationFactory.php ├── fixes ├── fix-symfony-file-moving.php └── fix-symfony-file-validation.php ├── bin ├── WorkerState.php ├── file-watcher.cjs ├── createSwooleCacheTable.php ├── createSwooleTimerTable.php ├── createSwooleServer.php ├── createSwooleTables.php ├── bootstrap.php ├── roadrunner-worker ├── frankenphp-worker.php └── swoole-server ├── LICENSE.md ├── README.md ├── composer.json └── art └── logo.svg /src/Commands/stubs/rr.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Commands/stubs/frankenphp-worker.php: -------------------------------------------------------------------------------- 1 | sandbox->instance('config', clone $event->sandbox['config']); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/WorkerException.php: -------------------------------------------------------------------------------- 1 | file = $file; 14 | $this->line = $line; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Listeners/FlushOnce.php: -------------------------------------------------------------------------------- 1 | sandbox->make('cache')->store('array')->flush(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Events/TickTerminated.php: -------------------------------------------------------------------------------- 1 | sandbox); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/OperationTerminated.php: -------------------------------------------------------------------------------- 1 | sandbox->make('db')->getConnections() as $connection) { 15 | $connection->disconnect(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewRequestInstanceToApplication.php: -------------------------------------------------------------------------------- 1 | app->instance('request', $event->request); 15 | $event->sandbox->instance('request', $event->request); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Listeners/FlushQueuedCookies.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('cookie')) { 15 | return; 16 | } 17 | 18 | $event->sandbox->make('cookie')->flushQueuedCookies(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToHttpKernel.php: -------------------------------------------------------------------------------- 1 | sandbox->make(Kernel::class)->setApplication($event->sandbox); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/FrankenPhp/Concerns/FindsFrankenPhpBinary.php: -------------------------------------------------------------------------------- 1 | find('frankenphp', null, [base_path()]); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Contracts/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | app->make('config')->get('octane.garbage'); 15 | 16 | if ($garbage && (memory_get_usage() / 1024 / 1024) > $garbage) { 17 | gc_collect_cycles(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToRouter.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('router')) { 15 | return; 16 | } 17 | 18 | $event->sandbox->make('router')->setContainer($event->sandbox); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Listeners/EnsureUploadedFilesAreValid.php: -------------------------------------------------------------------------------- 1 | console.log('File added...')) 13 | .on('change', () => console.log('File changed...')) 14 | .on('unlink', () => console.log('File deleted...')) 15 | .on('unlinkDir', () => console.log('Directory deleted...')); 16 | -------------------------------------------------------------------------------- /src/Listeners/FlushSessionState.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('session')) { 15 | return; 16 | } 17 | 18 | $driver = $event->sandbox->make('session')->driver(); 19 | 20 | $driver->flush(); 21 | $driver->regenerate(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Listeners/EnforceRequestScheme.php: -------------------------------------------------------------------------------- 1 | sandbox->make('config')->get('octane.https')) { 15 | return; 16 | } 17 | 18 | $event->sandbox->make('url')->forceScheme('https'); 19 | 20 | $event->request->server->set('HTTPS', 'on'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Listeners/FlushDatabaseQueryLog.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db')) { 15 | return; 16 | } 17 | 18 | foreach ($event->sandbox->make('db')->getConnections() as $connection) { 19 | $connection->flushQueryLog(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/DispatchesEvents.php: -------------------------------------------------------------------------------- 1 | bound(Dispatcher::class)) { 18 | $app[Dispatcher::class]->dispatch($event); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Events/HasApplicationAndSandbox.php: -------------------------------------------------------------------------------- 1 | app; 15 | } 16 | 17 | /** 18 | * Get the sandbox version of the application instance. 19 | */ 20 | public function sandbox(): Application 21 | { 22 | return $this->sandbox; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/StopWorkerIfNecessary.php: -------------------------------------------------------------------------------- 1 | sandbox->make(Client::class); 18 | 19 | if ($client instanceof StoppableClient) { 20 | $client->stop(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/SymfonyProcessFactory.php: -------------------------------------------------------------------------------- 1 | mapWithKeys( 15 | fn ($coroutine, $key) => [$key => $coroutine()] 16 | )->all(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToAuthorizationGate.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(Gate::class)) { 17 | return; 18 | } 19 | 20 | $event->sandbox->make(Gate::class)->setContainer($event->sandbox); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Listeners/EnsureUploadedFilesCanBeMoved.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db')) { 15 | return; 16 | } 17 | 18 | foreach ($event->sandbox->make('db')->getConnections() as $connection) { 19 | $connection->forgetRecordModificationState(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Events/RequestTerminated.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('queue')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('queue'), function ($manager) use ($event) { 19 | $manager->setApplication($event->sandbox); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bin/createSwooleCacheTable.php: -------------------------------------------------------------------------------- 1 | column('value', Table::TYPE_STRING, $serverState['octaneConfig']['cache']['bytes'] ?? 10000); 14 | $cacheTable->column('expiration', Table::TYPE_INT); 15 | 16 | $cacheTable->create(); 17 | 18 | return $cacheTable; 19 | } 20 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToValidationFactory.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('validator')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('validator'), function ($factory) use ($event) { 19 | $factory->setContainer($event->sandbox); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /bin/createSwooleTimerTable.php: -------------------------------------------------------------------------------- 1 | 0) { 9 | $timerTable = TableFactory::make($serverState['octaneConfig']['max_timer_table_size'] ?? 250); 10 | 11 | $timerTable->column('worker_pid', Table::TYPE_INT); 12 | $timerTable->column('time', Table::TYPE_INT); 13 | $timerTable->column('fd', Table::TYPE_INT); 14 | 15 | $timerTable->create(); 16 | 17 | return $timerTable; 18 | } 19 | 20 | return null; 21 | -------------------------------------------------------------------------------- /src/Listeners/EnsureRequestServerPortMatchesScheme.php: -------------------------------------------------------------------------------- 1 | request->getPort(); 15 | 16 | if (is_null($port) || $port === '') { 17 | $event->request->server->set( 18 | 'SERVER_PORT', 19 | $event->request->getScheme() === 'https' ? 443 : 80 20 | ); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToFilesystemManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('filesystem')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('filesystem'), function ($manager) use ($event) { 19 | $manager->setApplication($event->sandbox); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToPipelineHub.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(Hub::class)) { 17 | return; 18 | } 19 | 20 | with($event->sandbox->make(Hub::class), function ($hub) use ($event) { 21 | $hub->setContainer($event->sandbox); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/CurrentApplication.php: -------------------------------------------------------------------------------- 1 | instance('app', $app); 17 | $app->instance(Container::class, $app); 18 | 19 | Container::setInstance($app); 20 | 21 | Facade::clearResolvedInstances(); 22 | Facade::setFacadeApplication($app); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToViewFactory.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('view')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('view'), function ($view) use ($event) { 19 | $view->setContainer($event->sandbox); 20 | 21 | $view->share('app', $event->sandbox); 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToMailManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('mail.manager')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('mail.manager'), function ($manager) use ($event) { 19 | $manager->setApplication($event->sandbox); 20 | $manager->forgetMailers(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Listeners/PrepareInertiaForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(ResponseFactory::class)) { 17 | return; 18 | } 19 | 20 | $factory = $event->sandbox->make(ResponseFactory::class); 21 | 22 | if (method_exists($factory, 'flushShared')) { 23 | $factory->flushShared(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/PrepareLivewireForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(LivewireManager::class)) { 17 | return; 18 | } 19 | 20 | $manager = $event->sandbox->make(LivewireManager::class); 21 | 22 | if (method_exists($manager, 'flushState')) { 23 | $manager->flushState(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/FlushTranslatorCache.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('translator')) { 18 | return; 19 | } 20 | 21 | $translator = $event->sandbox->make('translator'); 22 | 23 | if ($translator instanceof NamespacedItemResolver) { 24 | $translator->flushParsedKeys(); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToDatabaseManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db') || 15 | ! method_exists($event->sandbox->make('db'), 'setApplication')) { 16 | return; 17 | } 18 | 19 | with($event->sandbox->make('db'), function ($manager) use ($event) { 20 | $manager->setApplication($event->sandbox); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Listeners/FlushMonologState.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('log')) { 17 | return; 18 | } 19 | 20 | collect($event->sandbox->make('log')->getChannels()) 21 | ->map->getLogger() 22 | ->filter(function ($logger) { 23 | return $logger instanceof ResettableInterface; 24 | })->each->reset(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToCacheManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('cache')) { 15 | return; 16 | } 17 | 18 | with($event->sandbox->make('cache'), function ($manager) use ($event) { 19 | if (method_exists($manager, 'setApplication')) { 20 | $manager->setApplication($event->sandbox); 21 | } 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Listeners/PrepareScoutForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(EngineManager::class)) { 17 | return; 18 | } 19 | 20 | $factory = $event->sandbox->make(EngineManager::class); 21 | 22 | if (! method_exists($factory, 'forgetEngines')) { 23 | return; 24 | } 25 | 26 | $factory->forgetEngines(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Swoole/Handlers/OnManagerStart.php: -------------------------------------------------------------------------------- 1 | shouldSetProcessName) { 24 | $this->extension->setProcessName($this->appName, 'manager process'); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Listeners/FlushUploadedFiles.php: -------------------------------------------------------------------------------- 1 | request->files->all() as $file) { 17 | if (! $file instanceof SplFileInfo || 18 | ! is_string($path = $file->getRealPath())) { 19 | continue; 20 | } 21 | 22 | clearstatcache(true, $path); 23 | 24 | if (is_file($path)) { 25 | unlink($path); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Listeners/FlushAuthenticationState.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('auth.driver')) { 15 | $event->sandbox->forgetInstance('auth.driver'); 16 | } 17 | 18 | if ($event->sandbox->resolved('auth')) { 19 | with($event->sandbox->make('auth'), function ($auth) use ($event) { 20 | $auth->setApplication($event->sandbox); 21 | $auth->forgetGuards(); 22 | }); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Cache/OctaneArrayStore.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(ChannelManager::class)) { 17 | return; 18 | } 19 | 20 | with($event->sandbox->make(ChannelManager::class), function ($manager) use ($event) { 21 | $manager->setContainer($event->sandbox); 22 | $manager->forgetDrivers(); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Listeners/PrepareSocialiteForNextOperation.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(Factory::class)) { 17 | return; 18 | } 19 | 20 | $factory = $event->sandbox->make(Factory::class); 21 | 22 | if (! method_exists($factory, 'forgetDrivers')) { 23 | return; 24 | } 25 | 26 | $factory->forgetDrivers(); 27 | $factory->setContainer($event->sandbox); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Listeners/FlushTemporaryContainerInstances.php: -------------------------------------------------------------------------------- 1 | app, 'resetScope')) { 15 | $event->app->resetScope(); 16 | } 17 | 18 | if (method_exists($event->app, 'forgetScopedInstances')) { 19 | $event->app->forgetScopedInstances(); 20 | } 21 | 22 | foreach ($event->sandbox->make('config')->get('octane.flush', []) as $binding) { 23 | $event->app->forgetInstance($binding); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/DispatchesTasks.php: -------------------------------------------------------------------------------- 1 | set(array_merge( 25 | $serverState['defaultServerOptions'], 26 | $config['swoole']['options'] ?? [] 27 | )); 28 | 29 | return $server; 30 | -------------------------------------------------------------------------------- /src/RoadRunner/Concerns/FindsRoadRunnerBinary.php: -------------------------------------------------------------------------------- 1 | find('rr', null, [base_path()]))) { 20 | if (! Str::contains($roadRunnerBinary, 'vendor/bin/rr')) { 21 | return $roadRunnerBinary; 22 | } 23 | } 24 | 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bin/createSwooleTables.php: -------------------------------------------------------------------------------- 1 | $columns) { 11 | $table = TableFactory::make(explode(':', $name)[1] ?? 1000); 12 | 13 | foreach ($columns ?? [] as $columnName => $column) { 14 | $table->column($columnName, match (explode(':', $column)[0] ?? 'string') { 15 | 'string' => Table::TYPE_STRING, 16 | 'int' => Table::TYPE_INT, 17 | 'float' => Table::TYPE_FLOAT, 18 | }, explode(':', $column)[1] ?? 1000); 19 | } 20 | 21 | $table->create(); 22 | 23 | $tables[explode(':', $name)[0]] = $table; 24 | } 25 | 26 | return $tables; 27 | -------------------------------------------------------------------------------- /src/Contracts/Client.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('session')) { 17 | return; 18 | } 19 | 20 | $handler = $event->sandbox->make('session')->driver()->getHandler(); 21 | 22 | if (! $handler instanceof DatabaseSessionHandler || 23 | ! method_exists($handler, 'setContainer')) { 24 | return; 25 | } 26 | 27 | $handler->setContainer($event->sandbox); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InteractsWithTerminal.php: -------------------------------------------------------------------------------- 1 | terminalWidth == null) { 24 | $this->terminalWidth = (new Terminal)->getWidth(); 25 | 26 | $this->terminalWidth = $this->terminalWidth >= 30 27 | ? $this->terminalWidth 28 | : 30; 29 | } 30 | 31 | return $this->terminalWidth; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Listeners/GiveNewApplicationInstanceToBroadcastManager.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved(BroadcastManager::class)) { 17 | return; 18 | } 19 | 20 | with($event->sandbox->make(BroadcastManager::class), function ($manager) use ($event) { 21 | $manager->setApplication($event->sandbox); 22 | 23 | // Forgetting drivers will flush all channel routes which is unwanted... 24 | // $manager->forgetDrivers(); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/WorkerExceptionInspector.php: -------------------------------------------------------------------------------- 1 | class; 23 | } 24 | 25 | /** 26 | * Get the worker exception trace. 27 | * 28 | * @param \Throwable $throwable 29 | * @return array 30 | */ 31 | public function getTrace($throwable) 32 | { 33 | return $this->trace; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Listeners/FlushLogContext.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('log')) { 15 | return; 16 | } 17 | 18 | if (method_exists($event->sandbox['log'], 'flushSharedContext')) { 19 | $event->sandbox['log']->flushSharedContext(); 20 | } 21 | 22 | if (method_exists($event->sandbox['log']->driver(), 'withoutContext')) { 23 | $event->sandbox['log']->withoutContext(); 24 | } 25 | 26 | if (method_exists($event->sandbox['log'], 'withoutContext')) { 27 | $event->sandbox['log']->withoutContext(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Listeners/RefreshQueryDurationHandling.php: -------------------------------------------------------------------------------- 1 | sandbox->resolved('db')) { 15 | return; 16 | } 17 | 18 | foreach ($event->sandbox->make('db')->getConnections() as $connection) { 19 | if ( 20 | method_exists($connection, 'resetTotalQueryDuration') 21 | && method_exists($connection, 'allowQueryDurationHandlersToRunAgain') 22 | ) { 23 | $connection->resetTotalQueryDuration(); 24 | $connection->allowQueryDurationHandlersToRunAgain(); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Testing/Fakes/FakeWorker.php: -------------------------------------------------------------------------------- 1 | client->requests as $request) { 13 | [$request, $context] = $this->client->marshalRequest( 14 | new RequestContext(['request' => $request]) 15 | ); 16 | 17 | $this->handle($request, $context); 18 | } 19 | } 20 | 21 | public function runTasks() 22 | { 23 | return collect($this->client->requests)->map(fn ($data) => $this->handleTask($data))->all(); 24 | } 25 | 26 | public function runTicks() 27 | { 28 | return collect($this->client->requests)->map(fn () => $this->handleTick())->all(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Facades/Octane.php: -------------------------------------------------------------------------------- 1 | columns[$name] = [$type, $size]; 24 | 25 | return parent::column($name, $type, $size); 26 | } 27 | 28 | /** 29 | * Update a row of the table. 30 | */ 31 | public function set(string $key, array $values): bool 32 | { 33 | collect($values) 34 | ->each($this->ensureColumnsSize()); 35 | 36 | return parent::set($key, $values); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Commands/stubs/Caddyfile: -------------------------------------------------------------------------------- 1 | { 2 | {$CADDY_GLOBAL_OPTIONS} 3 | 4 | admin localhost:{$CADDY_SERVER_ADMIN_PORT} 5 | 6 | frankenphp { 7 | worker "{$APP_PUBLIC_PATH}/frankenphp-worker.php" {$CADDY_SERVER_WORKER_COUNT} 8 | } 9 | } 10 | 11 | {$CADDY_SERVER_SERVER_NAME} { 12 | log { 13 | level {$CADDY_SERVER_LOG_LEVEL} 14 | 15 | # Redact the authorization query parameter that can be set by Mercure... 16 | format filter { 17 | wrap {$CADDY_SERVER_LOGGER} 18 | fields { 19 | uri query { 20 | replace authorization REDACTED 21 | } 22 | } 23 | } 24 | } 25 | 26 | route { 27 | root * "{$APP_PUBLIC_PATH}" 28 | encode zstd gzip 29 | 30 | # Mercure configuration is injected here... 31 | {$CADDY_SERVER_EXTRA_DIRECTIVES} 32 | 33 | php_server { 34 | index frankenphp-worker.php 35 | # Required for the public/storage/ directory... 36 | resolve_root_symlink 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Listeners/ReportException.php: -------------------------------------------------------------------------------- 1 | exception) { 19 | tap($event->sandbox, function ($sandbox) use ($event) { 20 | if ($event->exception instanceof DdException) { 21 | return; 22 | } 23 | 24 | if ($sandbox->environment('local', 'testing')) { 25 | Stream::throwable($event->exception); 26 | } 27 | 28 | $sandbox[ExceptionHandler::class]->report($event->exception); 29 | }); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Listeners/FlushLocaleState.php: -------------------------------------------------------------------------------- 1 | sandbox->make('config'); 17 | 18 | tap($event->sandbox->make('translator'), function ($translator) use ($config) { 19 | $translator->setLocale($config->get('app.locale')); 20 | $translator->setFallback($config->get('app.fallback_locale')); 21 | }); 22 | 23 | $provider = tap(new CarbonServiceProvider($event->app))->updateLocale(); 24 | 25 | collect($event->sandbox->getProviders($provider)) 26 | ->values() 27 | ->whenNotEmpty(fn ($providers) => $providers->first()->setAppGetter(fn () => $event->sandbox)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tables/TableFactory.php: -------------------------------------------------------------------------------- 1 | message = json_encode($vars); 15 | } 16 | 17 | /** 18 | * Get the evaluated contents of the object. 19 | * 20 | * @return string 21 | */ 22 | public function render() 23 | { 24 | $dump = function ($var) { 25 | $data = (new VarCloner())->cloneVar($var)->withMaxDepth(3); 26 | 27 | return (string) (new HtmlDumper(false))->dump($data, true, [ 28 | 'maxDepth' => 3, 29 | 'maxStringLength' => 160, 30 | ]); 31 | }; 32 | 33 | return collect($this->vars)->map($dump)->implode(''); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exceptions/TaskException.php: -------------------------------------------------------------------------------- 1 | class = $class; 31 | $this->file = $file; 32 | $this->line = $line; 33 | } 34 | 35 | /** 36 | * Returns the original throwable class name. 37 | * 38 | * @return string 39 | */ 40 | public function getClass() 41 | { 42 | return $this->class; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tables/Concerns/EnsuresColumnSizes.php: -------------------------------------------------------------------------------- 1 | columns, $column)) { 20 | return; 21 | } 22 | 23 | [$type, $size] = $this->columns[$column]; 24 | 25 | if ($type == Table::TYPE_STRING && strlen($value) > $size) { 26 | throw new ValueTooLargeForColumnException(sprintf( 27 | 'Value [%s...] is too large for [%s] column.', 28 | substr($value, 0, 20), 29 | $column, 30 | )); 31 | } 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/RequestContext.php: -------------------------------------------------------------------------------- 1 | data[$offset]); 17 | } 18 | 19 | #[\ReturnTypeWillChange] 20 | public function offsetGet($offset) 21 | { 22 | return $this->data[$offset]; 23 | } 24 | 25 | #[\ReturnTypeWillChange] 26 | public function offsetSet($offset, $value) 27 | { 28 | $this->data[$offset] = $value; 29 | } 30 | 31 | #[\ReturnTypeWillChange] 32 | public function offsetUnset($offset) 33 | { 34 | unset($this->data[$offset]); 35 | } 36 | 37 | public function __get($key) 38 | { 39 | return $this->data[$key]; 40 | } 41 | 42 | public function __set($key, $value) 43 | { 44 | $this->data[$key] = $value; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Concerns/RegistersTickHandlers.php: -------------------------------------------------------------------------------- 1 | listen( 30 | TickReceived::class, 31 | $listener 32 | ); 33 | 34 | return $listener; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InteractsWithEnvironmentVariables.php: -------------------------------------------------------------------------------- 1 | addPath(app()->environmentPath()) 24 | ->addName(app()->environmentFile()) 25 | ->make() 26 | ->read(); 27 | 28 | foreach ((new Parser())->parse($content) as $entry) { 29 | $variables->push($entry->getName()); 30 | } 31 | } catch (InvalidPathException $e) { 32 | // .. 33 | } 34 | 35 | $variables->each(fn ($name) => Env::getRepository()->clear($name)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Taylor Otwell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Testing/Fakes/FakeClient.php: -------------------------------------------------------------------------------- 1 | request, $context]; 27 | } 28 | 29 | public function respond(RequestContext $context, OctaneResponse $octaneResponse): void 30 | { 31 | $this->responses[] = $octaneResponse->response; 32 | } 33 | 34 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 35 | { 36 | $message = $app->make('config')->get('app.debug') ? (string) $e : 'Internal server error.'; 37 | 38 | $this->errors[] = $message; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Concerns/ProvidesRouting.php: -------------------------------------------------------------------------------- 1 | routes[$method.$uri] = $callback; 24 | } 25 | 26 | /** 27 | * Determine if a route exists for the given method and URI. 28 | */ 29 | public function hasRouteFor(string $method, string $uri): bool 30 | { 31 | return isset($this->routes[$method.$uri]); 32 | } 33 | 34 | /** 35 | * Invoke the route for the given method and URI. 36 | */ 37 | public function invokeRoute(Request $request, string $method, string $uri): Response 38 | { 39 | return call_user_func($this->routes[$method.$uri], $request); 40 | } 41 | 42 | /** 43 | * Get the registered Octane routes. 44 | */ 45 | public function getRoutes(): array 46 | { 47 | return $this->routes; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Swoole/SignalDispatcher.php: -------------------------------------------------------------------------------- 1 | signal($processId, 0); 17 | } 18 | 19 | /** 20 | * Send a SIGTERM signal to the given process. 21 | */ 22 | public function terminate(int $processId, int $wait = 0): bool 23 | { 24 | $this->extension->dispatchProcessSignal($processId, SIGTERM); 25 | 26 | if ($wait) { 27 | $start = time(); 28 | 29 | do { 30 | if (! $this->canCommunicateWith($processId)) { 31 | return true; 32 | } 33 | 34 | $this->extension->dispatchProcessSignal($processId, SIGTERM); 35 | 36 | sleep(1); 37 | } while (time() < $start + $wait); 38 | } 39 | 40 | return false; 41 | } 42 | 43 | /** 44 | * Send a signal to the given process. 45 | */ 46 | public function signal(int $processId, int $signal): bool 47 | { 48 | return $this->extension->dispatchProcessSignal($processId, $signal); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Swoole/SwooleCoroutineDispatcher.php: -------------------------------------------------------------------------------- 1 | $callback) { 26 | Coroutine::create(function () use ($key, $callback, $waitGroup, &$results) { 27 | $waitGroup->add(); 28 | 29 | $results[$key] = $callback(); 30 | 31 | $waitGroup->done(); 32 | }); 33 | } 34 | 35 | $waitGroup->wait($waitSeconds); 36 | }; 37 | 38 | if (! $this->withinCoroutineContext) { 39 | Coroutine\run($callback); 40 | } else { 41 | $callback(); 42 | } 43 | 44 | return $results; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Swoole/Actions/EnsureRequestsDontExceedMaxExecutionTime.php: -------------------------------------------------------------------------------- 1 | timerTable as $workerId => $row) { 27 | if ((time() - $row['time']) > $this->maxExecutionTime) { 28 | $this->timerTable->del($workerId); 29 | 30 | if ($this->server instanceof Server && ! $this->server->exists($row['fd'])) { 31 | continue; 32 | } 33 | 34 | $this->extension->dispatchProcessSignal($row['worker_pid'], SIGKILL); 35 | 36 | if ($this->server instanceof Server) { 37 | $response = Response::create($this->server, $row['fd']); 38 | 39 | if ($response) { 40 | $response->status(408); 41 | $response->end(); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Swoole/SwooleExtension.php: -------------------------------------------------------------------------------- 1 | response->send(); 33 | } 34 | 35 | /** 36 | * Send an error message to the server. 37 | */ 38 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 39 | { 40 | $response = new Response( 41 | Octane::formatExceptionForClient($e, $app->make('config')->get('app.debug')), 42 | 500, 43 | [ 44 | 'Status' => '500 Internal Server Error', 45 | 'Content-Type' => 'text/plain', 46 | ], 47 | ); 48 | 49 | $response->send(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Octane.php: -------------------------------------------------------------------------------- 1 | bound(Server::class)) { 24 | throw new Exception('Tables may only be accessed when using the Swoole server.'); 25 | } 26 | 27 | $tables = app(WorkerState::class)->tables; 28 | 29 | if (! isset($tables[$table])) { 30 | throw new Exception("Swoole table [{$table}] has not been configured."); 31 | } 32 | 33 | return $tables[$table]; 34 | } 35 | 36 | /** 37 | * Format an exception to a string that should be returned to the client. 38 | */ 39 | public static function formatExceptionForClient(Throwable $e, bool $debug = false): string 40 | { 41 | return $debug ? (string) $e : 'Internal server error.'; 42 | } 43 | 44 | /** 45 | * Write an error message to STDERR or to the SAPI logger if not in CLI mode. 46 | */ 47 | public static function writeError(string $message): void 48 | { 49 | if (defined('STDERR')) { 50 | fwrite(STDERR, $message.PHP_EOL); 51 | 52 | return; 53 | } 54 | 55 | error_log($message, 4); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/SequentialTaskDispatcher.php: -------------------------------------------------------------------------------- 1 | mapWithKeys( 23 | fn ($task, $key) => [$key => (function () use ($task) { 24 | try { 25 | return $task(); 26 | } catch (Throwable $e) { 27 | report($e); 28 | 29 | return TaskExceptionResult::from($e); 30 | } 31 | })()] 32 | )->each(function ($result) { 33 | if ($result instanceof TaskExceptionResult) { 34 | throw $result->getOriginal(); 35 | } 36 | })->all(); 37 | } 38 | 39 | /** 40 | * Concurrently dispatch the given callbacks via background tasks. 41 | */ 42 | public function dispatch(array $tasks): void 43 | { 44 | try { 45 | $this->resolve($tasks); 46 | } catch (Throwable) { 47 | // .. 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Swoole/Handlers/OnServerStart.php: -------------------------------------------------------------------------------- 1 | serverStateFile->writeProcessIds( 32 | $server->master_pid, 33 | $server->manager_pid 34 | ); 35 | 36 | if ($this->shouldSetProcessName) { 37 | $this->extension->setProcessName($this->appName, 'master process'); 38 | } 39 | 40 | if ($this->shouldTick) { 41 | Timer::tick(1000, function () use ($server) { 42 | $server->task('octane-tick'); 43 | }); 44 | } 45 | 46 | if ($this->maxExecutionTime > 0) { 47 | Timer::tick(1000, function () use ($server) { 48 | (new EnsureRequestsDontExceedMaxExecutionTime( 49 | $this->extension, $this->timerTable, $this->maxExecutionTime, $server 50 | ))(); 51 | }); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Concerns/ProvidesConcurrencySupport.php: -------------------------------------------------------------------------------- 1 | tasks()->resolve($tasks, $waitMilliseconds); 27 | } 28 | 29 | /** 30 | * Get the task dispatcher. 31 | * 32 | * @return \Laravel\Octane\Contracts\DispatchesTasks 33 | */ 34 | public function tasks() 35 | { 36 | return match (true) { 37 | app()->bound(DispatchesTasks::class) => app(DispatchesTasks::class), 38 | app()->bound(Server::class) => new SwooleTaskDispatcher, 39 | class_exists(Server::class) => (fn (array $serverState) => new SwooleHttpTaskDispatcher( 40 | $serverState['state']['host'] ?? '127.0.0.1', 41 | $serverState['state']['port'] ?? '8000', 42 | new SequentialTaskDispatcher 43 | ))(app(ServerStateFile::class)->read()), 44 | default => new SequentialTaskDispatcher, 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /bin/bootstrap.php: -------------------------------------------------------------------------------- 1 | cache->get('tick-'.$this->key); 29 | 30 | if (! is_null($lastInvokedAt) && 31 | (Carbon::now()->getTimestamp() - $lastInvokedAt) < $this->seconds) { 32 | return; 33 | } 34 | 35 | $this->cache->forever('tick-'.$this->key, Carbon::now()->getTimestamp()); 36 | 37 | if (is_null($lastInvokedAt) && ! $this->immediate) { 38 | return; 39 | } 40 | 41 | try { 42 | call_user_func($this->callback); 43 | } catch (Throwable $e) { 44 | $this->exceptionHandler->report($e); 45 | } 46 | } 47 | 48 | /** 49 | * Indicate how often the listener should be invoked. 50 | * 51 | * @return $this 52 | */ 53 | public function seconds(int $seconds) 54 | { 55 | $this->seconds = $seconds; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Indicate that the listener should be invoked on the first tick after the server starts. 62 | * 63 | * @return $this 64 | */ 65 | public function immediate() 66 | { 67 | $this->immediate = true; 68 | 69 | return $this; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Exceptions/TaskExceptionResult.php: -------------------------------------------------------------------------------- 1 | getFile(), ClosureStream::STREAM_PROTO.'://') 27 | ? collect($throwable->getTrace())->whereNotNull('file')->first() 28 | : null; 29 | 30 | return new static( 31 | $throwable::class, 32 | $throwable->getMessage(), 33 | (int) $throwable->getCode(), 34 | $fallbackTrace['file'] ?? $throwable->getFile(), 35 | $fallbackTrace['line'] ?? (int) $throwable->getLine(), 36 | ); 37 | } 38 | 39 | /** 40 | * Gets the original throwable. 41 | * 42 | * @return \Laravel\Octane\Exceptions\TaskException|\Laravel\Octane\Exceptions\DdException 43 | */ 44 | public function getOriginal() 45 | { 46 | if ($this->class == DdException::class) { 47 | return new DdException( 48 | json_decode($this->message, true) 49 | ); 50 | } 51 | 52 | return new TaskException( 53 | $this->class, 54 | $this->message, 55 | (int) $this->code, 56 | $this->file, 57 | (int) $this->line, 58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Logo Laravel Octane

2 | 3 |

4 | Build Status 5 | Total Downloads 6 | Latest Stable Version 7 | License 8 |

9 | 10 | ## Introduction 11 | 12 | Laravel Octane supercharges your application's performance by serving your application using high-powered application servers, including [FrankenPHP](https://frankenphp.dev), [Open Swoole](https://openswoole.com), [Swoole](https://github.com/swoole/swoole-src), and [RoadRunner](https://roadrunner.dev). Octane boots your application once, keeps it in memory, and then feeds it requests at supersonic speeds. 13 | 14 | ## Official Documentation 15 | 16 | Documentation for Octane can be found on the [Laravel website](https://laravel.com/docs/octane). 17 | 18 | ## Contributing 19 | 20 | Thank you for considering contributing to Octane! You can read the contribution guide [here](.github/CONTRIBUTING.md). 21 | 22 | ## Code of Conduct 23 | 24 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 25 | 26 | ## Security Vulnerabilities 27 | 28 | Please review [our security policy](https://github.com/laravel/octane/security/policy) on how to report security vulnerabilities. 29 | 30 | ## License 31 | 32 | Laravel Octane is open-sourced software licensed under the [MIT license](LICENSE.md). 33 | -------------------------------------------------------------------------------- /src/ApplicationGateway.php: -------------------------------------------------------------------------------- 1 | enableHttpMethodParameterOverride(); 29 | 30 | $this->dispatchEvent($this->sandbox, new RequestReceived($this->app, $this->sandbox, $request)); 31 | 32 | if (Octane::hasRouteFor($request->getMethod(), '/'.$request->path())) { 33 | return Octane::invokeRoute($request, $request->getMethod(), '/'.$request->path()); 34 | } 35 | 36 | return tap($this->sandbox->make(Kernel::class)->handle($request), function ($response) use ($request) { 37 | $this->dispatchEvent($this->sandbox, new RequestHandled($this->sandbox, $request, $response)); 38 | }); 39 | } 40 | 41 | /** 42 | * "Shut down" the application after a request. 43 | */ 44 | public function terminate(Request $request, Response $response): void 45 | { 46 | $this->sandbox->make(Kernel::class)->terminate($request, $response); 47 | 48 | $this->dispatchEvent($this->sandbox, new RequestTerminated($this->app, $this->sandbox, $request, $response)); 49 | 50 | $route = $request->route(); 51 | 52 | if ($route instanceof Route && method_exists($route, 'flushController')) { 53 | $route->flushController(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Swoole/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | $masterProcessId, 24 | 'managerProcessId' => $managerProcessId 25 | ] = $this->serverStateFile->read(); 26 | 27 | return $managerProcessId 28 | ? $masterProcessId && $managerProcessId && $this->dispatcher->canCommunicateWith((int) $managerProcessId) 29 | : $masterProcessId && $this->dispatcher->canCommunicateWith((int) $masterProcessId); 30 | } 31 | 32 | /** 33 | * Reload the Swoole workers. 34 | */ 35 | public function reloadServer(): void 36 | { 37 | [ 38 | 'masterProcessId' => $masterProcessId, 39 | ] = $this->serverStateFile->read(); 40 | 41 | $this->dispatcher->signal((int) $masterProcessId, SIGUSR1); 42 | } 43 | 44 | /** 45 | * Stop the Swoole server. 46 | */ 47 | public function stopServer(): bool 48 | { 49 | [ 50 | 'masterProcessId' => $masterProcessId, 51 | 'managerProcessId' => $managerProcessId 52 | ] = $this->serverStateFile->read(); 53 | 54 | $workerProcessIds = $this->exec->run('pgrep -P '.$managerProcessId); 55 | 56 | foreach ([$masterProcessId, $managerProcessId, ...$workerProcessIds] as $processId) { 57 | $this->dispatcher->signal((int) $processId, SIGKILL); 58 | } 59 | 60 | return true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tables/SwooleTable.php: -------------------------------------------------------------------------------- 1 | = 50000) { 8 | class SwooleTable extends Table 9 | { 10 | use Concerns\EnsuresColumnSizes; 11 | 12 | /** 13 | * The table columns. 14 | * 15 | * @var array 16 | */ 17 | protected $columns; 18 | 19 | /** 20 | * Set the data type and size of the columns. 21 | */ 22 | public function column(string $name, int $type, int $size = 0): bool 23 | { 24 | $this->columns[$name] = [$type, $size]; 25 | 26 | return parent::column($name, $type, $size); 27 | } 28 | 29 | /** 30 | * Update a row of the table. 31 | */ 32 | public function set(string $key, array $values): bool 33 | { 34 | collect($values) 35 | ->each($this->ensureColumnsSize()); 36 | 37 | return parent::set($key, $values); 38 | } 39 | } 40 | } else { 41 | class SwooleTable extends Table 42 | { 43 | use Concerns\EnsuresColumnSizes; 44 | 45 | /** 46 | * The table columns. 47 | * 48 | * @var array 49 | */ 50 | protected $columns; 51 | 52 | /** 53 | * Set the data type and size of the columns. 54 | * 55 | * @param string $name 56 | * @param int $type 57 | * @param int $size 58 | * @return void 59 | */ 60 | public function column($name, $type, $size = 0) 61 | { 62 | $this->columns[$name] = [$type, $size]; 63 | 64 | parent::column($name, $type, $size); 65 | } 66 | 67 | /** 68 | * Update a row of the table. 69 | * 70 | * @param string $key 71 | * @return void 72 | */ 73 | public function set($key, array $values) 74 | { 75 | collect($values) 76 | ->each($this->ensureColumnsSize()); 77 | 78 | parent::set($key, $values); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Stream.php: -------------------------------------------------------------------------------- 1 | 'request', 19 | 'method' => $method, 20 | 'url' => $url, 21 | 'memory' => memory_get_usage(), 22 | 'statusCode' => $statusCode, 23 | 'duration' => $duration, 24 | ])."\n"); 25 | } 26 | 27 | /** 28 | * Stream the given throwable to stderr. 29 | * 30 | * @return void 31 | */ 32 | public static function throwable(Throwable $throwable) 33 | { 34 | $fallbackTrace = str_starts_with($throwable->getFile(), ClosureStream::STREAM_PROTO.'://') 35 | ? collect($throwable->getTrace())->whereNotNull('file')->first() 36 | : null; 37 | 38 | Octane::writeError(json_encode([ 39 | 'type' => 'throwable', 40 | 'class' => $throwable::class, 41 | 'code' => $throwable->getCode(), 42 | 'file' => $fallbackTrace['file'] ?? $throwable->getFile(), 43 | 'line' => $fallbackTrace['line'] ?? (int) $throwable->getLine(), 44 | 'message' => $throwable->getMessage(), 45 | 'trace' => array_slice($throwable->getTrace(), 0, 2), 46 | ])); 47 | } 48 | 49 | /** 50 | * Stream the given shutdown throwable to stderr. 51 | * 52 | * @return void 53 | */ 54 | public static function shutdown(Throwable $throwable) 55 | { 56 | Octane::writeError(json_encode([ 57 | 'type' => 'shutdown', 58 | 'class' => $throwable::class, 59 | 'code' => $throwable->getCode(), 60 | 'file' => $throwable->getFile(), 61 | 'line' => $throwable->getLine(), 62 | 'message' => $throwable->getMessage(), 63 | 'trace' => array_slice($throwable->getTrace(), 0, 2), 64 | ])); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/FrankenPhp/ServerStateFile.php: -------------------------------------------------------------------------------- 1 | path) 19 | ? json_decode(file_get_contents($this->path), true) 20 | : []; 21 | 22 | return [ 23 | 'masterProcessId' => $state['masterProcessId'] ?? null, 24 | 'state' => $state['state'] ?? [], 25 | ]; 26 | } 27 | 28 | /** 29 | * Write the given process ID to the server state file. 30 | */ 31 | public function writeProcessId(int $masterProcessId): void 32 | { 33 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 34 | throw new RuntimeException('Unable to write to process ID file.'); 35 | } 36 | 37 | file_put_contents($this->path, json_encode( 38 | array_merge($this->read(), ['masterProcessId' => $masterProcessId]), 39 | JSON_PRETTY_PRINT 40 | )); 41 | } 42 | 43 | /** 44 | * Write the given state array to the server state file. 45 | */ 46 | public function writeState(array $newState): void 47 | { 48 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 49 | throw new RuntimeException('Unable to write to process ID file.'); 50 | } 51 | 52 | file_put_contents($this->path, json_encode( 53 | array_merge($this->read(), ['state' => $newState]), 54 | JSON_PRETTY_PRINT 55 | )); 56 | } 57 | 58 | /** 59 | * Delete the process ID file. 60 | */ 61 | public function delete(): bool 62 | { 63 | if (is_writable($this->path)) { 64 | return unlink($this->path); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /** 71 | * Get the path to the process ID file. 72 | */ 73 | public function path(): string 74 | { 75 | return $this->path; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/RoadRunner/ServerStateFile.php: -------------------------------------------------------------------------------- 1 | path) 19 | ? json_decode(file_get_contents($this->path), true) 20 | : []; 21 | 22 | return [ 23 | 'masterProcessId' => $state['masterProcessId'] ?? null, 24 | 'state' => $state['state'] ?? [], 25 | ]; 26 | } 27 | 28 | /** 29 | * Write the given process ID to the server state file. 30 | */ 31 | public function writeProcessId(int $masterProcessId): void 32 | { 33 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 34 | throw new RuntimeException('Unable to write to process ID file.'); 35 | } 36 | 37 | file_put_contents($this->path, json_encode( 38 | array_merge($this->read(), ['masterProcessId' => $masterProcessId]), 39 | JSON_PRETTY_PRINT 40 | )); 41 | } 42 | 43 | /** 44 | * Write the given state array to the server state file. 45 | */ 46 | public function writeState(array $newState): void 47 | { 48 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 49 | throw new RuntimeException('Unable to write to process ID file.'); 50 | } 51 | 52 | file_put_contents($this->path, json_encode( 53 | array_merge($this->read(), ['state' => $newState]), 54 | JSON_PRETTY_PRINT 55 | )); 56 | } 57 | 58 | /** 59 | * Delete the process ID file. 60 | */ 61 | public function delete(): bool 62 | { 63 | if (is_writable($this->path)) { 64 | return unlink($this->path); 65 | } 66 | 67 | return false; 68 | } 69 | 70 | /** 71 | * Get the path to the process ID file. 72 | */ 73 | public function path(): string 74 | { 75 | return $this->path; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /bin/roadrunner-worker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | waitRequest()) { 42 | $worker = $worker ?: tap((new Worker( 43 | new ApplicationFactory($basePath), $roadRunnerClient 44 | )))->boot(); 45 | 46 | if (! $psr7Request instanceof ServerRequestInterface) { 47 | break; 48 | } 49 | 50 | [$request, $context] = $roadRunnerClient->marshalRequest(new RequestContext([ 51 | 'psr7Request' => $psr7Request, 52 | ])); 53 | 54 | $worker->handle($request, $context); 55 | } 56 | } catch (Throwable $e) { 57 | if (! $e instanceof RelayException) { 58 | $worker ? report($e) : Stream::shutdown($e); 59 | } 60 | 61 | exit(1); 62 | } finally { 63 | if (! is_null($worker)) { 64 | $worker->terminate(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/RoadRunner/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | $masterProcessId, 30 | ] = $this->serverStateFile->read(); 31 | 32 | return $masterProcessId && $this->posix->kill($masterProcessId, 0); 33 | } 34 | 35 | /** 36 | * Reload the RoadRunner workers. 37 | */ 38 | public function reloadServer(): void 39 | { 40 | [ 41 | 'state' => [ 42 | 'host' => $host, 43 | 'rpcPort' => $rpcPort, 44 | ], 45 | ] = $this->serverStateFile->read(); 46 | 47 | tap($this->processFactory->createProcess([ 48 | $this->findRoadRunnerBinary(), 49 | 'reset', 50 | '-o', "rpc.listen=tcp://$host:$rpcPort", 51 | '-s', 52 | ], base_path()))->start()->waitUntil(function ($type, $buffer) { 53 | if ($type === Process::ERR) { 54 | throw new RuntimeException('Cannot reload RoadRunner: '.$buffer); 55 | } 56 | 57 | return true; 58 | }); 59 | } 60 | 61 | /** 62 | * Stop the RoadRunner server. 63 | */ 64 | public function stopServer(): bool 65 | { 66 | [ 67 | 'masterProcessId' => $masterProcessId, 68 | ] = $this->serverStateFile->read(); 69 | 70 | return (bool) $this->posix->kill($masterProcessId, SIGTERM); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/RoadRunner/RoadRunnerClient.php: -------------------------------------------------------------------------------- 1 | toHttpFoundationRequest($context->psr7Request), 33 | $context, 34 | ]; 35 | } 36 | 37 | /** 38 | * Send the response to the server. 39 | */ 40 | public function respond(RequestContext $context, OctaneResponse $octaneResponse): void 41 | { 42 | if ($octaneResponse->outputBuffer && 43 | ! $octaneResponse->response instanceof StreamedResponse && 44 | ! $octaneResponse->response instanceof BinaryFileResponse) { 45 | $octaneResponse->response->setContent( 46 | $octaneResponse->outputBuffer.$octaneResponse->response->getContent() 47 | ); 48 | } 49 | 50 | $this->client->respond($this->toPsr7Response($octaneResponse->response)); 51 | } 52 | 53 | /** 54 | * Send an error message to the server. 55 | */ 56 | public function error(Throwable $e, Application $app, Request $request, RequestContext $context): void 57 | { 58 | $this->client->getWorker()->error(Octane::formatExceptionForClient( 59 | $e, 60 | $app->make('config')->get('app.debug') 61 | )); 62 | } 63 | 64 | /** 65 | * Stop the underlying server / worker. 66 | */ 67 | public function stop(): void 68 | { 69 | $this->client->getWorker()->stop(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/FrankenPhp/ServerProcessInspector.php: -------------------------------------------------------------------------------- 1 | serverStateFile->read()['masterProcessId'] ?? null)) { 26 | return false; 27 | } 28 | 29 | try { 30 | return Http::get($this->adminConfigUrl())->successful(); 31 | } catch (ConnectionException $_) { 32 | return false; 33 | } 34 | } 35 | 36 | /** 37 | * Reload the FrankenPHP workers. 38 | */ 39 | public function reloadServer(): void 40 | { 41 | try { 42 | Http::withBody(Http::get($this->adminConfigUrl())->body(), 'application/json') 43 | ->withHeaders(['Cache-Control' => 'must-revalidate']) 44 | ->patch($this->adminConfigUrl()); 45 | } catch (ConnectionException $_) { 46 | // 47 | } 48 | } 49 | 50 | /** 51 | * Stop the FrankenPHP server. 52 | */ 53 | public function stopServer(): bool 54 | { 55 | try { 56 | return Http::post($this->adminUrl().'/stop')->successful(); 57 | } catch (ConnectionException $_) { 58 | return false; 59 | } 60 | } 61 | 62 | /** 63 | * Get the URL to the FrankenPHP admin panel. 64 | */ 65 | protected function adminUrl(): string 66 | { 67 | $adminPort = $this->serverStateFile->read()['state']['adminPort'] ?? 2019; 68 | 69 | return "http://localhost:{$adminPort}"; 70 | } 71 | 72 | /** 73 | * Get the URL to the FrankenPHP admin panel's configuration endpoint. 74 | */ 75 | protected function adminConfigUrl(): string 76 | { 77 | return "{$this->adminUrl()}/config/apps/frankenphp"; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Swoole/ServerStateFile.php: -------------------------------------------------------------------------------- 1 | path) 19 | ? json_decode(file_get_contents($this->path), true) 20 | : []; 21 | 22 | return [ 23 | 'masterProcessId' => $state['masterProcessId'] ?? null, 24 | 'managerProcessId' => $state['managerProcessId'] ?? null, 25 | 'state' => $state['state'] ?? [], 26 | ]; 27 | } 28 | 29 | /** 30 | * Write the given process IDs to the server state file. 31 | */ 32 | public function writeProcessIds(int $masterProcessId, int $managerProcessId): void 33 | { 34 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 35 | throw new RuntimeException('Unable to write to process ID file.'); 36 | } 37 | 38 | file_put_contents($this->path, json_encode( 39 | array_merge($this->read(), [ 40 | 'masterProcessId' => $masterProcessId, 41 | 'managerProcessId' => $managerProcessId, 42 | ]), 43 | JSON_PRETTY_PRINT 44 | )); 45 | } 46 | 47 | /** 48 | * Write the given state array to the server state file. 49 | */ 50 | public function writeState(array $newState): void 51 | { 52 | if (! is_writable($this->path) && ! is_writable(dirname($this->path))) { 53 | throw new RuntimeException('Unable to write to process ID file.'); 54 | } 55 | 56 | file_put_contents($this->path, json_encode( 57 | array_merge($this->read(), ['state' => $newState]), 58 | JSON_PRETTY_PRINT 59 | )); 60 | } 61 | 62 | /** 63 | * Delete the process ID file. 64 | */ 65 | public function delete(): bool 66 | { 67 | if (is_writable($this->path)) { 68 | return unlink($this->path); 69 | } 70 | 71 | return false; 72 | } 73 | 74 | /** 75 | * Get the path to the process ID file. 76 | */ 77 | public function path(): string 78 | { 79 | return $this->path; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel/octane", 3 | "description": "Supercharge your Laravel application's performance.", 4 | "keywords": ["laravel", "octane", "roadrunner", "swoole", "frankenphp"], 5 | "license": "MIT", 6 | "support": { 7 | "issues": "https://github.com/laravel/octane/issues", 8 | "source": "https://github.com/laravel/octane" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Taylor Otwell", 13 | "email": "taylor@laravel.com" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.1.0", 18 | "laravel/framework": "^10.10.1|^11.0", 19 | "laminas/laminas-diactoros": "^3.0", 20 | "laravel/serializable-closure": "^1.3.0", 21 | "nesbot/carbon": "^2.66.0|^3.0", 22 | "symfony/psr-http-message-bridge": "^2.2.0|^6.4|^7.0" 23 | }, 24 | "require-dev": { 25 | "guzzlehttp/guzzle": "^7.6.1", 26 | "inertiajs/inertia-laravel": "^0.6.9|^1.0", 27 | "laravel/scout": "^10.2.1", 28 | "laravel/socialite": "^5.6.1", 29 | "livewire/livewire": "^2.12.3|^3.0", 30 | "mockery/mockery": "^1.5.1", 31 | "nunomaduro/collision": "^6.4.0|^7.5.2|^8.0", 32 | "orchestra/testbench": "^8.5.2|^9.0", 33 | "phpstan/phpstan": "^1.10.15", 34 | "phpunit/phpunit": "^10.4", 35 | "spiral/roadrunner-http": "^3.3.0", 36 | "spiral/roadrunner-cli": "^2.6.0" 37 | }, 38 | "bin": [ 39 | "bin/roadrunner-worker", 40 | "bin/swoole-server" 41 | ], 42 | "conflict": { 43 | "spiral/roadrunner": "<2023.1.0", 44 | "spiral/roadrunner-cli": "<2.6.0", 45 | "spiral/roadrunner-http": "<3.3.0" 46 | }, 47 | "autoload": { 48 | "psr-4": { 49 | "Laravel\\Octane\\": "src" 50 | } 51 | }, 52 | "autoload-dev": { 53 | "psr-4": { 54 | "Laravel\\Octane\\Tests\\": "tests" 55 | } 56 | }, 57 | "scripts": { 58 | "post-autoload-dump": [ 59 | "@php vendor/bin/testbench package:discover --ansi" 60 | ] 61 | }, 62 | "extra": { 63 | "branch-alias": { 64 | "dev-master": "2.x-dev" 65 | }, 66 | "laravel": { 67 | "providers": [ 68 | "Laravel\\Octane\\OctaneServiceProvider" 69 | ], 70 | "aliases": { 71 | "Octane": "Laravel\\Octane\\Facades\\Octane" 72 | } 73 | } 74 | }, 75 | "config": { 76 | "sort-packages": true 77 | }, 78 | "minimum-stability": "dev", 79 | "prefer-stable": true 80 | } 81 | -------------------------------------------------------------------------------- /bin/frankenphp-worker.php: -------------------------------------------------------------------------------- 1 | boot(); 50 | 51 | [$request, $context] = $frankenPhpClient->marshalRequest(new RequestContext()); 52 | 53 | $worker->handle($request, $context); 54 | } catch (Throwable $e) { 55 | if ($worker) { 56 | report($e); 57 | } 58 | 59 | $response = new Response( 60 | 'Internal Server Error', 61 | 500, 62 | [ 63 | 'Status' => '500 Internal Server Error', 64 | 'Content-Type' => 'text/plain', 65 | ], 66 | ); 67 | 68 | $response->send(); 69 | 70 | Stream::shutdown($e); 71 | } 72 | }; 73 | 74 | while ($requestCount < $maxRequests && frankenphp_handle_request($handleRequest)) { 75 | $requestCount++; 76 | } 77 | } finally { 78 | $worker?->terminate(); 79 | } 80 | -------------------------------------------------------------------------------- /src/Swoole/SwooleTaskDispatcher.php: -------------------------------------------------------------------------------- 1 | bound(Server::class)) { 27 | throw new InvalidArgumentException('Tasks can only be resolved within a Swoole server context / web request.'); 28 | } 29 | 30 | $results = app(Server::class)->taskWaitMulti(collect($tasks)->mapWithKeys(function ($task, $key) { 31 | return [$key => $task instanceof Closure 32 | ? new SerializableClosure($task) 33 | : $task, ]; 34 | })->all(), $waitMilliseconds / 1000); 35 | 36 | if ($results === false) { 37 | throw TaskTimeoutException::after($waitMilliseconds); 38 | } 39 | 40 | $i = 0; 41 | 42 | foreach ($tasks as $key => $task) { 43 | if (isset($results[$i])) { 44 | if ($results[$i] instanceof TaskExceptionResult) { 45 | throw $results[$i]->getOriginal(); 46 | } 47 | 48 | $tasks[$key] = $results[$i]->result; 49 | } else { 50 | $tasks[$key] = false; 51 | } 52 | 53 | $i++; 54 | } 55 | 56 | return $tasks; 57 | } 58 | 59 | /** 60 | * Concurrently dispatch the given callbacks via background tasks. 61 | */ 62 | public function dispatch(array $tasks): void 63 | { 64 | if (! app()->bound(Server::class)) { 65 | throw new InvalidArgumentException('Tasks can only be dispatched within a Swoole server context / web request.'); 66 | } 67 | 68 | $server = app(Server::class); 69 | 70 | collect($tasks)->each(function ($task) use ($server) { 71 | $server->task($task instanceof Closure ? new SerializableClosure($task) : $task); 72 | }); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/MarshalsPsr7RequestsAndResponses.php: -------------------------------------------------------------------------------- 1 | httpFoundationRequestFactory()->createRequest($request)); 40 | } 41 | 42 | /** 43 | * Convert the given HttpFoundation response into a PSR-7 response. 44 | */ 45 | protected function toPsr7Response(Response $response): ResponseInterface 46 | { 47 | return $this->psr7ResponseFactory()->createResponse($response); 48 | } 49 | 50 | /** 51 | * Create the Symfony HttpFoundation factory. 52 | * 53 | * This instance can turn a PSR-7 request into an HttpFoundation request. 54 | */ 55 | protected function httpFoundationRequestFactory(): HttpFoundationFactoryInterface 56 | { 57 | return $this->httpFoundationFactory ?: ( 58 | $this->httpFoundationFactory = new HttpFoundationFactory 59 | ); 60 | } 61 | 62 | /** 63 | * Create the Symfony PSR-7 factory. 64 | * 65 | * This instance can turn an HTTP Foundation response into a PSR-7 response. 66 | */ 67 | protected function psr7ResponseFactory(): HttpMessageFactoryInterface 68 | { 69 | return $this->psrHttpFactory ?: ($this->psrHttpFactory = new PsrHttpFactory( 70 | new ServerRequestFactory, 71 | new StreamFactory, 72 | new UploadedFileFactory, 73 | new ResponseFactory 74 | )); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Commands/StatusCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 33 | 34 | $isRunning = match ($server) { 35 | 'swoole' => $this->isSwooleServerRunning(), 36 | 'roadrunner' => $this->isRoadRunnerServerRunning(), 37 | 'frankenphp' => $this->isFrankenPhpServerRunning(), 38 | default => $this->invalidServer($server), 39 | }; 40 | 41 | return ! tap($isRunning, function ($isRunning) { 42 | $isRunning 43 | ? $this->info('Octane server is running.') 44 | : $this->info('Octane server is not running.'); 45 | }); 46 | } 47 | 48 | /** 49 | * Check if the Swoole server is running. 50 | * 51 | * @return bool 52 | */ 53 | protected function isSwooleServerRunning() 54 | { 55 | return app(SwooleServerProcessInspector::class) 56 | ->serverIsRunning(); 57 | } 58 | 59 | /** 60 | * Check if the RoadRunner server is running. 61 | * 62 | * @return bool 63 | */ 64 | protected function isRoadRunnerServerRunning() 65 | { 66 | return app(RoadRunnerServerProcessInspector::class) 67 | ->serverIsRunning(); 68 | } 69 | 70 | /** 71 | * Check if the FrankenPHP server is running. 72 | * 73 | * @return bool 74 | */ 75 | protected function isFrankenPhpServerRunning() 76 | { 77 | return app(FrankenPhpServerProcessInspector::class) 78 | ->serverIsRunning(); 79 | } 80 | 81 | /** 82 | * Inform the user that the server type is invalid. 83 | * 84 | * @return bool 85 | */ 86 | protected function invalidServer(string $server) 87 | { 88 | $this->error("Invalid server: {$server}."); 89 | 90 | return false; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Swoole/SwooleHttpTaskDispatcher.php: -------------------------------------------------------------------------------- 1 | mapWithKeys(function ($task, $key) { 36 | return [$key => $task instanceof Closure 37 | ? new SerializableClosure($task) 38 | : $task, ]; 39 | })->all(); 40 | 41 | try { 42 | $response = Http::timeout(($waitMilliseconds / 1000) + 5)->post("http://{$this->host}:{$this->port}/octane/resolve-tasks", [ 43 | 'tasks' => Crypt::encryptString(serialize($tasks)), 44 | 'wait' => $waitMilliseconds, 45 | ]); 46 | 47 | return match ($response->status()) { 48 | 200 => unserialize($response), 49 | 504 => throw TaskTimeoutException::after($waitMilliseconds), 50 | default => throw TaskExceptionResult::from( 51 | new Exception('Invalid response from task server.'), 52 | )->getOriginal(), 53 | }; 54 | } catch (ConnectionException) { 55 | return $this->fallbackDispatcher->resolve($tasks, $waitMilliseconds); 56 | } 57 | } 58 | 59 | /** 60 | * Concurrently dispatch the given callbacks via background tasks. 61 | */ 62 | public function dispatch(array $tasks): void 63 | { 64 | $tasks = collect($tasks)->mapWithKeys(function ($task, $key) { 65 | return [$key => $task instanceof Closure 66 | ? new SerializableClosure($task) 67 | : $task, ]; 68 | })->all(); 69 | 70 | try { 71 | Http::post("http://{$this->host}:{$this->port}/octane/dispatch-tasks", [ 72 | 'tasks' => Crypt::encryptString(serialize($tasks)), 73 | ]); 74 | } catch (ConnectionException) { 75 | $this->fallbackDispatcher->dispatch($tasks); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ApplicationFactory.php: -------------------------------------------------------------------------------- 1 | basePath.'/.laravel/app.php', 25 | $this->basePath.'/bootstrap/app.php', 26 | ]; 27 | 28 | foreach ($paths as $path) { 29 | if (file_exists($path)) { 30 | return $this->warm($this->bootstrap(require $path, $initialInstances)); 31 | } 32 | } 33 | 34 | throw new RuntimeException("Application bootstrap file not found in 'bootstrap' or '.laravel' directory."); 35 | } 36 | 37 | /** 38 | * Bootstrap the given application. 39 | */ 40 | public function bootstrap(Application $app, array $initialInstances = []): Application 41 | { 42 | foreach ($initialInstances as $key => $value) { 43 | $app->instance($key, $value); 44 | } 45 | 46 | $app->bootstrapWith($this->getBootstrappers($app)); 47 | 48 | $app->loadDeferredProviders(); 49 | 50 | return $app; 51 | } 52 | 53 | /** 54 | * Get the application's HTTP kernel bootstrappers. 55 | */ 56 | protected function getBootstrappers(Application $app): array 57 | { 58 | $method = (new ReflectionObject( 59 | $kernel = $app->make(HttpKernelContract::class) 60 | ))->getMethod('bootstrappers'); 61 | 62 | $method->setAccessible(true); 63 | 64 | return $this->injectBootstrapperBefore( 65 | RegisterProviders::class, 66 | SetRequestForConsole::class, 67 | $method->invoke($kernel) 68 | ); 69 | } 70 | 71 | /** 72 | * Inject a given bootstrapper before another bootstrapper. 73 | */ 74 | protected function injectBootstrapperBefore(string $before, string $inject, array $bootstrappers): array 75 | { 76 | $injectIndex = array_search($before, $bootstrappers, true); 77 | 78 | if ($injectIndex !== false) { 79 | array_splice($bootstrappers, $injectIndex, 0, [$inject]); 80 | } 81 | 82 | return $bootstrappers; 83 | } 84 | 85 | /** 86 | * Warm the application with pre-resolved, cached services that persist across requests. 87 | */ 88 | public function warm(Application $app, array $services = []): Application 89 | { 90 | foreach ($services ?: $app->make('config')->get('octane.warm', []) as $service) { 91 | if (is_string($service) && $app->bound($service)) { 92 | $app->make($service); 93 | } 94 | } 95 | 96 | return $app; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Commands/ReloadCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 33 | 34 | return match ($server) { 35 | 'swoole' => $this->reloadSwooleServer(), 36 | 'roadrunner' => $this->reloadRoadRunnerServer(), 37 | 'frankenphp' => $this->reloadFrankenPhpServer(), 38 | default => $this->invalidServer($server), 39 | }; 40 | } 41 | 42 | /** 43 | * Reload the Swoole server for Octane. 44 | * 45 | * @return int 46 | */ 47 | protected function reloadSwooleServer() 48 | { 49 | $inspector = app(SwooleServerProcessInspector::class); 50 | 51 | if (! $inspector->serverIsRunning()) { 52 | $this->error('Octane server is not running.'); 53 | 54 | return 1; 55 | } 56 | 57 | $this->info('Reloading workers...'); 58 | 59 | $inspector->reloadServer(); 60 | 61 | return 0; 62 | } 63 | 64 | /** 65 | * Reload the RoadRunner server for Octane. 66 | * 67 | * @return int 68 | */ 69 | protected function reloadRoadRunnerServer() 70 | { 71 | $inspector = app(RoadRunnerServerProcessInspector::class); 72 | 73 | if (! $inspector->serverIsRunning()) { 74 | $this->error('Octane server is not running.'); 75 | 76 | return 1; 77 | } 78 | 79 | $this->info('Reloading workers...'); 80 | 81 | $inspector->reloadServer(); 82 | 83 | return 0; 84 | } 85 | 86 | /** 87 | * Reload the FrankenPHP server for Octane. 88 | * 89 | * @return int 90 | */ 91 | protected function reloadFrankenPhpServer() 92 | { 93 | $inspector = app(FrankenPhpServerProcessInspector::class); 94 | 95 | if (! $inspector->serverIsRunning()) { 96 | $this->error('Octane server is not running.'); 97 | 98 | return 1; 99 | } 100 | 101 | $this->info('Reloading workers...'); 102 | 103 | $inspector->reloadServer(); 104 | 105 | return 0; 106 | } 107 | 108 | /** 109 | * Inform the user that the server type is invalid. 110 | * 111 | * @return int 112 | */ 113 | protected function invalidServer(string $server) 114 | { 115 | $this->error("Invalid server: {$server}."); 116 | 117 | return 1; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Swoole/Actions/ConvertSwooleRequestToIlluminateRequest.php: -------------------------------------------------------------------------------- 1 | prepareServerVariables( 19 | $swooleRequest->server ?? [], 20 | $swooleRequest->header ?? [], 21 | $phpSapi 22 | ); 23 | 24 | $request = new SymfonyRequest( 25 | $swooleRequest->get ?? [], 26 | $swooleRequest->post ?? [], 27 | [], 28 | $swooleRequest->cookie ?? [], 29 | $swooleRequest->files ?? [], 30 | $serverVariables, 31 | $swooleRequest->rawContent(), 32 | ); 33 | 34 | if (str_starts_with((string) $request->headers->get('CONTENT_TYPE'), 'application/x-www-form-urlencoded') && 35 | in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'PATCH', 'DELETE'])) { 36 | parse_str($request->getContent(), $data); 37 | 38 | $request->request = new ParameterBag($data); 39 | } 40 | 41 | return Request::createFromBase($request); 42 | } 43 | 44 | /** 45 | * Parse the "server" variables and headers into a single array of $_SERVER variables. 46 | */ 47 | protected function prepareServerVariables(array $server, array $headers, string $phpSapi): array 48 | { 49 | $results = []; 50 | 51 | foreach ($server as $key => $value) { 52 | $results[strtoupper($key)] = $value; 53 | } 54 | 55 | $results = array_merge( 56 | $results, 57 | $this->formatHttpHeadersIntoServerVariables($headers) 58 | ); 59 | 60 | if (isset($results['REQUEST_URI'], $results['QUERY_STRING']) && 61 | strlen($results['QUERY_STRING']) > 0 && 62 | strpos($results['REQUEST_URI'], '?') === false) { 63 | $results['REQUEST_URI'] .= '?'.$results['QUERY_STRING']; 64 | } 65 | 66 | return $phpSapi === 'cli-server' 67 | ? $this->correctHeadersSetIncorrectlyByPhpDevServer($results) 68 | : $results; 69 | } 70 | 71 | /** 72 | * Format the given HTTP headers into properly formatted $_SERVER variables. 73 | */ 74 | protected function formatHttpHeadersIntoServerVariables(array $headers): array 75 | { 76 | $results = []; 77 | 78 | foreach ($headers as $key => $value) { 79 | $key = strtoupper(str_replace('-', '_', $key)); 80 | 81 | if (! in_array($key, ['HTTPS', 'REMOTE_ADDR', 'SERVER_PORT'])) { 82 | $key = 'HTTP_'.$key; 83 | } 84 | 85 | $results[$key] = $value; 86 | } 87 | 88 | return $results; 89 | } 90 | 91 | /** 92 | * Correct headers set incorrectly by built-in PHP development server. 93 | */ 94 | protected function correctHeadersSetIncorrectlyByPhpDevServer(array $headers): array 95 | { 96 | if (array_key_exists('HTTP_CONTENT_LENGTH', $headers)) { 97 | $headers['CONTENT_LENGTH'] = $headers['HTTP_CONTENT_LENGTH']; 98 | } 99 | 100 | if (array_key_exists('HTTP_CONTENT_TYPE', $headers)) { 101 | $headers['CONTENT_TYPE'] = $headers['HTTP_CONTENT_TYPE']; 102 | } 103 | 104 | return $headers; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Commands/StopCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 36 | 37 | return match ($server) { 38 | 'swoole' => $this->stopSwooleServer(), 39 | 'roadrunner' => $this->stopRoadRunnerServer(), 40 | 'frankenphp' => $this->stopFrankenPhpServer(), 41 | default => $this->invalidServer($server), 42 | }; 43 | } 44 | 45 | /** 46 | * Stop the Swoole server for Octane. 47 | * 48 | * @return int 49 | */ 50 | protected function stopSwooleServer() 51 | { 52 | $inspector = app(SwooleServerProcessInspector::class); 53 | 54 | if (! $inspector->serverIsRunning()) { 55 | app(SwooleServerStateFile::class)->delete(); 56 | 57 | $this->error('Swoole server is not running.'); 58 | 59 | return 1; 60 | } 61 | 62 | $this->info('Stopping server...'); 63 | 64 | if (! $inspector->stopServer()) { 65 | $this->error('Failed to stop Swoole server.'); 66 | 67 | return 1; 68 | } 69 | 70 | app(SwooleServerStateFile::class)->delete(); 71 | 72 | return 0; 73 | } 74 | 75 | /** 76 | * Stop the RoadRunner server for Octane. 77 | * 78 | * @return int 79 | */ 80 | protected function stopRoadRunnerServer() 81 | { 82 | $inspector = app(RoadRunnerServerProcessInspector::class); 83 | 84 | if (! $inspector->serverIsRunning()) { 85 | app(RoadRunnerServerStateFile::class)->delete(); 86 | 87 | $this->error('RoadRunner server is not running.'); 88 | 89 | return 1; 90 | } 91 | 92 | $this->info('Stopping server...'); 93 | 94 | $inspector->stopServer(); 95 | 96 | app(RoadRunnerServerStateFile::class)->delete(); 97 | 98 | return 0; 99 | } 100 | 101 | /** 102 | * Stop the FrankenPHP server for Octane. 103 | * 104 | * @return int 105 | */ 106 | protected function stopFrankenPhpServer() 107 | { 108 | $inspector = app(FrankenPhpProcessInspector::class); 109 | 110 | if (! $inspector->serverIsRunning()) { 111 | app(FrankenPhpStateFile::class)->delete(); 112 | 113 | $this->error('FrankenPHP server is not running.'); 114 | 115 | return 1; 116 | } 117 | 118 | $this->info('Stopping server...'); 119 | 120 | $inspector->stopServer(); 121 | 122 | app(FrankenPhpStateFile::class)->delete(); 123 | 124 | return 0; 125 | } 126 | 127 | /** 128 | * Inform the user that the server type is invalid. 129 | * 130 | * @return int 131 | */ 132 | protected function invalidServer(string $server) 133 | { 134 | $this->error("Invalid server: {$server}."); 135 | 136 | return 1; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Swoole/Handlers/OnWorkerStart.php: -------------------------------------------------------------------------------- 1 | clearOpcodeCache(); 34 | 35 | $this->workerState->server = $server; 36 | $this->workerState->workerId = $workerId; 37 | $this->workerState->workerPid = posix_getpid(); 38 | $this->workerState->worker = $this->bootWorker($server); 39 | 40 | $this->dispatchServerTickTaskEverySecond($server); 41 | $this->streamRequestsToConsole($server); 42 | 43 | if ($this->shouldSetProcessName) { 44 | $isTaskWorker = $workerId >= $server->setting['worker_num']; 45 | 46 | $this->extension->setProcessName( 47 | $this->serverState['appName'], 48 | $isTaskWorker ? 'task worker process' : 'worker process', 49 | ); 50 | } 51 | } 52 | 53 | /** 54 | * Boot the Octane worker and application. 55 | * 56 | * @param \Swoole\Http\Server $server 57 | * @return \Laravel\Octane\Worker|null 58 | */ 59 | protected function bootWorker($server) 60 | { 61 | try { 62 | return tap(new Worker( 63 | new ApplicationFactory($this->basePath), 64 | $this->workerState->client = new SwooleClient 65 | ))->boot([ 66 | 'octane.cacheTable' => $this->workerState->cacheTable, 67 | Server::class => $server, 68 | WorkerState::class => $this->workerState, 69 | ]); 70 | } catch (Throwable $e) { 71 | Stream::shutdown($e); 72 | 73 | $server->shutdown(); 74 | } 75 | } 76 | 77 | /** 78 | * Start the Octane server tick to dispatch the tick task every second. 79 | * 80 | * @param \Swoole\Http\Server $server 81 | * @return void 82 | */ 83 | protected function dispatchServerTickTaskEverySecond($server) 84 | { 85 | // ... 86 | } 87 | 88 | /** 89 | * Register the request handled listener that will output request information per request. 90 | * 91 | * @param \Swoole\Http\Server $server 92 | * @return void 93 | */ 94 | protected function streamRequestsToConsole($server) 95 | { 96 | $this->workerState->worker->onRequestHandled(function ($request, $response, $sandbox) { 97 | if (! $sandbox->environment('local', 'testing')) { 98 | return; 99 | } 100 | 101 | Stream::request( 102 | $request->getMethod(), 103 | $request->fullUrl(), 104 | $response->getStatusCode(), 105 | (microtime(true) - $this->workerState->lastRequestTime) * 1000, 106 | ); 107 | }); 108 | } 109 | 110 | /** 111 | * Clear the APCu and Opcache caches. 112 | * 113 | * @return void 114 | */ 115 | protected function clearOpcodeCache() 116 | { 117 | foreach (['apcu_clear_cache', 'opcache_reset'] as $function) { 118 | if (function_exists($function)) { 119 | $function(); 120 | } 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Concerns/ProvidesDefaultConfigurationOptions.php: -------------------------------------------------------------------------------- 1 | isStarted()) { 23 | sleep(1); 24 | } 25 | 26 | $this->writeServerRunningMessage(); 27 | 28 | $watcher = $this->startServerWatcher(); 29 | 30 | try { 31 | while ($server->isRunning()) { 32 | $this->writeServerOutput($server); 33 | 34 | if ($watcher->isRunning() && 35 | $watcher->getIncrementalOutput()) { 36 | $this->info('Application change detected. Restarting workers…'); 37 | 38 | $inspector->reloadServer(); 39 | } elseif ($watcher->isTerminated()) { 40 | $this->error( 41 | 'Watcher process has terminated. Please ensure Node and chokidar are installed.'.PHP_EOL. 42 | $watcher->getErrorOutput() 43 | ); 44 | 45 | return 1; 46 | } 47 | 48 | usleep(500 * 1000); 49 | } 50 | 51 | $this->writeServerOutput($server); 52 | } catch (ServerShutdownException) { 53 | return 1; 54 | } finally { 55 | $this->stopServer(); 56 | } 57 | 58 | return $server->getExitCode(); 59 | } 60 | 61 | /** 62 | * Start the watcher process for the server. 63 | * 64 | * @return \Symfony\Component\Process\Process|object 65 | */ 66 | protected function startServerWatcher() 67 | { 68 | if (! $this->option('watch')) { 69 | return new class 70 | { 71 | public function __call($method, $parameters) 72 | { 73 | return null; 74 | } 75 | }; 76 | } 77 | 78 | if (empty($paths = config('octane.watch'))) { 79 | throw new InvalidArgumentException( 80 | 'List of directories/files to watch not found. Please update your "config/octane.php" configuration file.', 81 | ); 82 | } 83 | 84 | return tap(new Process([ 85 | (new ExecutableFinder)->find('node'), 86 | 'file-watcher.cjs', 87 | json_encode(collect(config('octane.watch'))->map(fn ($path) => base_path($path))), 88 | $this->option('poll'), 89 | ], realpath(__DIR__.'/../../../bin'), null, null, null))->start(); 90 | } 91 | 92 | /** 93 | * Write the server start "message" to the console. 94 | * 95 | * @return void 96 | */ 97 | protected function writeServerRunningMessage() 98 | { 99 | $this->info('Server running…'); 100 | 101 | $this->output->writeln([ 102 | '', 103 | ' Local: '.($this->hasOption('https') && $this->option('https') ? 'https://' : 'http://').$this->getHost().':'.$this->getPort().' ', 104 | '', 105 | ' Press Ctrl+C to stop the server', 106 | '', 107 | ]); 108 | } 109 | 110 | /** 111 | * Retrieve the given server output and flush it. 112 | * 113 | * @return array 114 | */ 115 | protected function getServerOutput($server) 116 | { 117 | $output = [ 118 | $server->getIncrementalOutput(), 119 | $server->getIncrementalErrorOutput(), 120 | ]; 121 | 122 | $server->clearOutput()->clearErrorOutput(); 123 | 124 | return $output; 125 | } 126 | 127 | /** 128 | * Get the Octane HTTP server host IP to bind on. 129 | * 130 | * @return string 131 | */ 132 | protected function getHost() 133 | { 134 | return $this->option('host') ?? config('octane.host') ?? $_ENV['OCTANE_HOST'] ?? '127.0.0.1'; 135 | } 136 | 137 | /** 138 | * Get the Octane HTTP server port. 139 | * 140 | * @return string 141 | */ 142 | protected function getPort() 143 | { 144 | return $this->option('port') ?? config('octane.port') ?? $_ENV['OCTANE_PORT'] ?? '8000'; 145 | } 146 | 147 | /** 148 | * Returns the list of signals to subscribe. 149 | */ 150 | public function getSubscribedSignals(): array 151 | { 152 | return [SIGINT, SIGTERM]; 153 | } 154 | 155 | /** 156 | * The method will be called when the application is signaled. 157 | */ 158 | public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false 159 | { 160 | $this->stopServer(); 161 | 162 | exit(0); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Commands/InstallCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: $this->choice( 38 | 'Which application server you would like to use?', 39 | ['roadrunner', 'swoole', 'frankenphp'], 40 | ); 41 | 42 | return (int) ! tap(match ($server) { 43 | 'swoole' => $this->installSwooleServer(), 44 | 'roadrunner' => $this->installRoadRunnerServer(), 45 | 'frankenphp' => $this->installFrankenPhpServer(), 46 | default => $this->invalidServer($server), 47 | }, function ($installed) use ($server) { 48 | if ($installed) { 49 | $this->updateEnvironmentFile($server); 50 | 51 | $this->callSilent('vendor:publish', ['--tag' => 'octane-config', '--force' => true]); 52 | 53 | $this->info('Octane installed successfully.'); 54 | $this->newLine(); 55 | } 56 | }); 57 | } 58 | 59 | /** 60 | * Updates the environment file with the given server. 61 | * 62 | * @param string $server 63 | * @return void 64 | */ 65 | public function updateEnvironmentFile($server) 66 | { 67 | if (File::exists($env = app()->environmentFile())) { 68 | $contents = File::get($env); 69 | 70 | if (! Str::contains($contents, 'OCTANE_SERVER=')) { 71 | File::append( 72 | $env, 73 | PHP_EOL.'OCTANE_SERVER='.$server.PHP_EOL, 74 | ); 75 | } else { 76 | $this->warn('Please adjust the `OCTANE_SERVER` environment variable.'); 77 | } 78 | } 79 | } 80 | 81 | /** 82 | * Install the RoadRunner dependencies. 83 | * 84 | * @return bool 85 | */ 86 | public function installRoadRunnerServer() 87 | { 88 | if (! $this->ensureRoadRunnerPackageIsInstalled()) { 89 | return false; 90 | } 91 | 92 | if (File::exists(base_path('.gitignore'))) { 93 | collect(['rr', '.rr.yaml']) 94 | ->each(function ($file) { 95 | $contents = File::get(base_path('.gitignore')); 96 | if (! Str::contains($contents, $file.PHP_EOL)) { 97 | File::append( 98 | base_path('.gitignore'), 99 | $file.PHP_EOL 100 | ); 101 | } 102 | }); 103 | } 104 | 105 | return $this->ensureRoadRunnerBinaryIsInstalled(); 106 | } 107 | 108 | /** 109 | * Install the Swoole dependencies. 110 | * 111 | * @return bool 112 | */ 113 | public function installSwooleServer() 114 | { 115 | if (! resolve(SwooleExtension::class)->isInstalled()) { 116 | $this->warn('The Swoole extension is missing.'); 117 | } 118 | 119 | return true; 120 | } 121 | 122 | /** 123 | * Install the FrankenPHP server. 124 | * 125 | * @return bool 126 | */ 127 | public function installFrankenPhpServer() 128 | { 129 | if ($this->option('no-interaction')) { 130 | $this->info("FrankenPHP's Octane integration is in beta and should be used with caution in production."); 131 | $this->newLine(); 132 | } elseif (! $this->confirm("FrankenPHP's Octane integration is in beta and should be used with caution in production. Do you wish to continue?", true)) { 133 | return false; 134 | } 135 | 136 | $gitIgnorePath = base_path('.gitignore'); 137 | 138 | if (File::exists($gitIgnorePath)) { 139 | $contents = File::get($gitIgnorePath); 140 | 141 | $filesToAppend = collect(['/caddy', 'frankenphp', 'frankenphp-worker.php']) 142 | ->filter(fn ($file) => ! str_contains($contents, $file.PHP_EOL)) 143 | ->implode(PHP_EOL); 144 | 145 | if ($filesToAppend !== '') { 146 | File::append($gitIgnorePath, PHP_EOL.$filesToAppend.PHP_EOL); 147 | } 148 | } 149 | 150 | $this->ensureFrankenPhpWorkerIsInstalled(); 151 | 152 | try { 153 | $this->ensureFrankenPhpBinaryIsInstalled(); 154 | } catch (Throwable $e) { 155 | $this->error($e->getMessage()); 156 | 157 | return false; 158 | } 159 | 160 | return true; 161 | } 162 | 163 | /** 164 | * Inform the user that the server type is invalid. 165 | * 166 | * @return bool 167 | */ 168 | protected function invalidServer(string $server) 169 | { 170 | $this->error("Invalid server: {$server}."); 171 | 172 | return false; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/Commands/StartCommand.php: -------------------------------------------------------------------------------- 1 | option('server') ?: config('octane.server'); 49 | 50 | return match ($server) { 51 | 'swoole' => $this->startSwooleServer(), 52 | 'roadrunner' => $this->startRoadRunnerServer(), 53 | 'frankenphp' => $this->startFrankenPhpServer(), 54 | default => $this->invalidServer($server), 55 | }; 56 | } 57 | 58 | /** 59 | * Start the Swoole server for Octane. 60 | * 61 | * @return int 62 | */ 63 | protected function startSwooleServer() 64 | { 65 | return $this->call('octane:swoole', [ 66 | '--host' => $this->getHost(), 67 | '--port' => $this->getPort(), 68 | '--workers' => $this->option('workers'), 69 | '--task-workers' => $this->option('task-workers'), 70 | '--max-requests' => $this->option('max-requests'), 71 | '--watch' => $this->option('watch'), 72 | '--poll' => $this->option('poll'), 73 | ]); 74 | } 75 | 76 | /** 77 | * Start the RoadRunner server for Octane. 78 | * 79 | * @return int 80 | */ 81 | protected function startRoadRunnerServer() 82 | { 83 | return $this->call('octane:roadrunner', [ 84 | '--host' => $this->getHost(), 85 | '--port' => $this->getPort(), 86 | '--rpc-host' => $this->option('rpc-host'), 87 | '--rpc-port' => $this->option('rpc-port'), 88 | '--workers' => $this->option('workers'), 89 | '--max-requests' => $this->option('max-requests'), 90 | '--rr-config' => $this->option('rr-config'), 91 | '--watch' => $this->option('watch'), 92 | '--poll' => $this->option('poll'), 93 | '--log-level' => $this->option('log-level'), 94 | ]); 95 | } 96 | 97 | /** 98 | * Start the FrankenPHP server for Octane. 99 | * 100 | * @return int 101 | */ 102 | protected function startFrankenPhpServer() 103 | { 104 | return $this->call('octane:frankenphp', [ 105 | '--host' => $this->getHost(), 106 | '--port' => $this->getPort(), 107 | '--admin-port' => $this->option('admin-port'), 108 | '--workers' => $this->option('workers'), 109 | '--max-requests' => $this->option('max-requests'), 110 | '--caddyfile' => $this->option('caddyfile'), 111 | '--https' => $this->option('https'), 112 | '--http-redirect' => $this->option('http-redirect'), 113 | '--watch' => $this->option('watch'), 114 | '--poll' => $this->option('poll'), 115 | '--log-level' => $this->option('log-level'), 116 | ]); 117 | } 118 | 119 | /** 120 | * Inform the user that the server type is invalid. 121 | * 122 | * @return int 123 | */ 124 | protected function invalidServer(string $server) 125 | { 126 | $this->error("Invalid server: {$server}."); 127 | 128 | return 1; 129 | } 130 | 131 | /** 132 | * Stop the server. 133 | * 134 | * @return void 135 | */ 136 | protected function stopServer() 137 | { 138 | $server = $this->option('server') ?: config('octane.server'); 139 | 140 | $this->callSilent('octane:stop', [ 141 | '--server' => $server, 142 | ]); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /art/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InstallsRoadRunnerDependencies.php: -------------------------------------------------------------------------------- 1 | isRoadRunnerInstalled()) { 44 | return true; 45 | } 46 | 47 | if (! $this->confirm('Octane requires "spiral/roadrunner-http:^3.3.0" and "spiral/roadrunner-cli:^2.6.0". Do you wish to install them as a dependencies?')) { 48 | $this->error('Octane requires "spiral/roadrunner-http" and "spiral/roadrunner-cli".'); 49 | 50 | return false; 51 | } 52 | 53 | $command = $this->findComposer().' require spiral/roadrunner-http:^3.3.0 spiral/roadrunner-cli:^2.6.0 --with-all-dependencies'; 54 | 55 | $process = Process::fromShellCommandline($command, null, null, null, null); 56 | 57 | if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { 58 | try { 59 | $process->setTty(true); 60 | } catch (RuntimeException $e) { 61 | $this->output->writeln('Warning: '.$e->getMessage()); 62 | } 63 | } 64 | 65 | try { 66 | $process->run(function ($type, $line) { 67 | $this->output->write($line); 68 | }); 69 | } catch (ProcessSignaledException $e) { 70 | if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) { 71 | throw $e; 72 | } 73 | } 74 | 75 | return true; 76 | } 77 | 78 | /** 79 | * Get the composer command for the environment. 80 | * 81 | * @return string 82 | */ 83 | protected function findComposer() 84 | { 85 | $composerPath = getcwd().'/composer.phar'; 86 | 87 | $phpPath = (new PhpExecutableFinder)->find(); 88 | 89 | if (! file_exists($composerPath)) { 90 | $composerPath = (new ExecutableFinder())->find('composer'); 91 | } 92 | 93 | return '"'.$phpPath.'" "'.$composerPath.'"'; 94 | } 95 | 96 | /** 97 | * Ensure the RoadRunner binary is installed into the project. 98 | */ 99 | protected function ensureRoadRunnerBinaryIsInstalled(): string 100 | { 101 | if (! is_null($roadRunnerBinary = $this->findRoadRunnerBinary())) { 102 | return $roadRunnerBinary; 103 | } 104 | 105 | if ($this->confirm('Unable to locate RoadRunner binary. Should Octane download the binary for your operating system?', true)) { 106 | $this->downloadRoadRunnerBinary(); 107 | 108 | copy(__DIR__.'/../stubs/rr.yaml', base_path('.rr.yaml')); 109 | } 110 | 111 | return base_path('rr'); 112 | } 113 | 114 | /** 115 | * Ensure the RoadRunner binary installed in your project meets Octane requirements. 116 | * 117 | * @param string $roadRunnerBinary 118 | * @return void 119 | */ 120 | protected function ensureRoadRunnerBinaryMeetsRequirements($roadRunnerBinary) 121 | { 122 | $version = tap(new Process([$roadRunnerBinary, '--version'], base_path())) 123 | ->run() 124 | ->getOutput(); 125 | 126 | if (! Str::startsWith($version, 'rr version')) { 127 | return $this->warn( 128 | 'Unable to determine the current RoadRunner binary version. Please report this issue: https://github.com/laravel/octane/issues/new.' 129 | ); 130 | } 131 | 132 | $version = explode(' ', $version)[2]; 133 | 134 | if (version_compare($version, $this->requiredRoadRunnerVersion, '>=')) { 135 | return; 136 | } 137 | 138 | $this->warn("Your RoadRunner binary version ($version) may be incompatible with Octane."); 139 | 140 | if ($this->confirm('Should Octane download the latest RoadRunner binary version for your operating system?', true)) { 141 | rename($roadRunnerBinary, "$roadRunnerBinary.backup"); 142 | 143 | try { 144 | $this->downloadRoadRunnerBinary(); 145 | } catch (Throwable $e) { 146 | report($e); 147 | 148 | rename("$roadRunnerBinary.backup", $roadRunnerBinary); 149 | 150 | return $this->warn('Unable to download RoadRunner binary. The HTTP request exception has been logged.'); 151 | } 152 | 153 | unlink("$roadRunnerBinary.backup"); 154 | } 155 | } 156 | 157 | /** 158 | * Download the latest version of the RoadRunner binary. 159 | * 160 | * @return void 161 | */ 162 | protected function downloadRoadRunnerBinary() 163 | { 164 | $installed = false; 165 | 166 | tap(new Process(array_filter([ 167 | (new PhpExecutableFinder)->find(), 168 | './vendor/bin/rr', 169 | 'get-binary', 170 | '-n', 171 | '--ansi', 172 | ]), base_path(), null, null, null))->mustRun(function (string $type, string $buffer) use (&$installed) { 173 | if (! $installed) { 174 | $this->output->write($buffer); 175 | 176 | $installed = str_contains($buffer, 'has been installed into'); 177 | } 178 | }); 179 | 180 | chmod(base_path('rr'), 0755); 181 | 182 | $this->line(''); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Commands/Concerns/InstallsFrankenPhpDependencies.php: -------------------------------------------------------------------------------- 1 | findFrankenPhpBinary())) { 44 | return $frankenphpBinary; 45 | } 46 | 47 | if ($this->confirm('Unable to locate FrankenPHP binary. Should Octane download the binary for your operating system?', true)) { 48 | $this->downloadFrankenPhpBinary(); 49 | } 50 | 51 | return base_path('frankenphp'); 52 | } 53 | 54 | /** 55 | * Download the latest version of the FrankenPHP binary. 56 | * 57 | * @return string 58 | */ 59 | protected function downloadFrankenPhpBinary() 60 | { 61 | $arch = php_uname('m'); 62 | 63 | $assetName = match (true) { 64 | PHP_OS_FAMILY === 'Linux' && $arch === 'x86_64' => 'frankenphp-linux-x86_64', 65 | PHP_OS_FAMILY === 'Linux' && $arch === 'aarch64' => 'frankenphp-linux-aarch64', 66 | PHP_OS_FAMILY === 'Darwin' => "frankenphp-mac-$arch", 67 | default => null, 68 | }; 69 | 70 | if ($assetName === null) { 71 | throw new RuntimeException('FrankenPHP binaries are currently only available for Linux (x86_64, aarch64) and macOS. Other systems should use the Docker images or compile FrankenPHP manually.'); 72 | } 73 | 74 | $assets = Http::accept('application/vnd.github+json') 75 | ->withHeaders(['X-GitHub-Api-Version' => '2022-11-28']) 76 | ->get('https://api.github.com/repos/dunglas/frankenphp/releases/latest')['assets']; 77 | 78 | foreach ($assets as $asset) { 79 | if ($asset['name'] !== $assetName) { 80 | continue; 81 | } 82 | 83 | $path = base_path('frankenphp'); 84 | 85 | $progressBar = null; 86 | 87 | (new Client)->get( 88 | $asset['browser_download_url'], 89 | [ 90 | 'sink' => $path, 91 | 'progress' => function ($downloadTotal, $downloadedBytes) use (&$progressBar) { 92 | if ($downloadTotal === 0) { 93 | return; 94 | } 95 | 96 | if ($progressBar === null) { 97 | $progressBar = $this->output->createProgressBar($downloadTotal); 98 | $progressBar->start($downloadTotal, $downloadedBytes); 99 | 100 | return; 101 | } 102 | 103 | $progressBar->setProgress($downloadedBytes); 104 | }, 105 | ] 106 | ); 107 | 108 | chmod($path, 0755); 109 | 110 | $progressBar->finish(); 111 | 112 | $this->newLine(); 113 | 114 | return $path; 115 | } 116 | 117 | throw new RuntimeException('FrankenPHP asset not found.'); 118 | } 119 | 120 | /** 121 | * Ensure the installed FrankenPHP binary meets Octane's requirements. 122 | * 123 | * @param string $frankenPhpBinary 124 | * @return void 125 | */ 126 | protected function ensureFrankenPhpBinaryMeetsRequirements($frankenPhpBinary) 127 | { 128 | $buildInfo = tap(new Process([$frankenPhpBinary, 'build-info'], base_path())) 129 | ->run() 130 | ->getOutput(); 131 | 132 | $lineWithVersion = collect(explode("\n", $buildInfo)) 133 | ->first(function ($line) { 134 | return str_starts_with($line, 'dep') && str_contains($line, 'github.com/dunglas/frankenphp'); 135 | }); 136 | 137 | if ($lineWithVersion === null) { 138 | return $this->warn( 139 | 'Unable to determine the current FrankenPHP binary version. Please report this issue: https://github.com/laravel/octane/issues/new.', 140 | ); 141 | } 142 | 143 | $version = Str::of($lineWithVersion)->trim()->afterLast('v')->value(); 144 | 145 | if (preg_match('/\d+\.\d+\.\d+/', $version) !== 1) { 146 | return $this->warn( 147 | 'Unable to determine the current FrankenPHP binary version. Please report this issue: https://github.com/laravel/octane/issues/new.', 148 | ); 149 | } 150 | 151 | if (version_compare($version, $this->requiredFrankenPhpVersion, '>=')) { 152 | return; 153 | } 154 | 155 | $this->warn("Your FrankenPHP binary version ($version) may be incompatible with Octane."); 156 | 157 | if ($this->confirm('Should Octane download the latest FrankenPHP binary version for your operating system?', true)) { 158 | rename($frankenPhpBinary, "$frankenPhpBinary.backup"); 159 | 160 | try { 161 | $this->downloadFrankenPhpBinary(); 162 | } catch (Throwable $e) { 163 | report($e); 164 | 165 | rename("$frankenPhpBinary.backup", $frankenPhpBinary); 166 | 167 | return $this->warn('Unable to download FrankenPHP binary. The underlying error has been logged.'); 168 | } 169 | 170 | unlink("$frankenPhpBinary.backup"); 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /bin/swoole-server: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | require __DIR__.'/bootstrap.php'; 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Create The Swoole Server 23 | |-------------------------------------------------------------------------- 24 | | 25 | | First, we will load the server state file from disk. This file contains 26 | | various information we need to boot Swoole such as the configuration 27 | | and application name. We can use this data to start up our server. 28 | | 29 | */ 30 | 31 | $serverState = json_decode(file_get_contents( 32 | $serverStateFile = $_SERVER['argv'][1] 33 | ), true)['state']; 34 | 35 | $server = require __DIR__.'/createSwooleServer.php'; 36 | 37 | $timerTable = require __DIR__.'/createSwooleTimerTable.php'; 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Handle Server & Manager Start 42 | |-------------------------------------------------------------------------- 43 | | 44 | | The following callbacks manage the master process and manager process 45 | | start events. These handlers primarily are responsible for writing 46 | | the process ID to the server state file so we can remember them. 47 | | 48 | */ 49 | 50 | $server->on('start', fn (Server $server) => $bootstrap($serverState) && (new OnServerStart( 51 | new ServerStateFile($serverStateFile), 52 | new SwooleExtension, 53 | $serverState['appName'], 54 | $serverState['octaneConfig']['max_execution_time'] ?? 0, 55 | $timerTable, 56 | $serverState['octaneConfig']['tick'] ?? true 57 | ))($server)); 58 | 59 | $server->on('managerstart', function () use ($serverState) { 60 | // Don't bootstrap entire application before server / worker start. Otherwise, files can't be gracefully reloaded... #632 61 | require_once __DIR__.'/../src/Swoole/Handlers/OnManagerStart.php'; 62 | require_once __DIR__.'/../src/Swoole/SwooleExtension.php'; 63 | 64 | (new OnManagerStart( 65 | new SwooleExtension, $serverState['appName'] 66 | ))(); 67 | }); 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Handle Worker Start 72 | |-------------------------------------------------------------------------- 73 | | 74 | | Swoole will start multiple worker processes and the following callback 75 | | will handle their state events. When a worker starts we will create 76 | | a new Octane worker and inform it to start handling our requests. 77 | | 78 | | We will also create a "workerState" variable which will maintain state 79 | | and allow us to access the worker and client from the callback that 80 | | will handle incoming requests. Basically this works like a cache. 81 | | 82 | */ 83 | 84 | require_once __DIR__.'/WorkerState.php'; 85 | 86 | $workerState = new WorkerState; 87 | 88 | $workerState->cacheTable = require __DIR__.'/createSwooleCacheTable.php'; 89 | $workerState->timerTable = $timerTable; 90 | $workerState->tables = require __DIR__.'/createSwooleTables.php'; 91 | 92 | $server->on('workerstart', fn (Server $server, $workerId) => 93 | (fn ($basePath) => (new OnWorkerStart( 94 | new SwooleExtension, $basePath, $serverState, $workerState 95 | ))($server, $workerId))($bootstrap($serverState)) 96 | ); 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Handle Incoming Requests 101 | |-------------------------------------------------------------------------- 102 | | 103 | | The following callback will handle all incoming requests plus send them 104 | | the worker. The worker will send the request through the application 105 | | and ask the client to send the response back to the Swoole server. 106 | | 107 | */ 108 | 109 | $server->on('request', function ($request, $response) use ($server, $workerState, $serverState) { 110 | $workerState->lastRequestTime = microtime(true); 111 | 112 | if ($workerState->timerTable) { 113 | $workerState->timerTable->set($workerState->workerId, [ 114 | 'worker_pid' => $workerState->workerPid, 115 | 'time' => time(), 116 | 'fd' => $request->fd, 117 | ]); 118 | } 119 | 120 | $workerState->worker->handle(...$workerState->client->marshalRequest(new RequestContext([ 121 | 'swooleRequest' => $request, 122 | 'swooleResponse' => $response, 123 | 'publicPath' => $serverState['publicPath'], 124 | 'octaneConfig' => $serverState['octaneConfig'], 125 | ]))); 126 | 127 | if ($workerState->timerTable) { 128 | $workerState->timerTable->del($workerState->workerId); 129 | } 130 | }); 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Handle Tasks 135 | |-------------------------------------------------------------------------- 136 | | 137 | | Swoole tasks can be used to offload concurrent work onto a group of 138 | | background processes which handle the work in isolation and with 139 | | separate application state. We should handle these tasks below. 140 | | 141 | */ 142 | 143 | $server->on('task', fn (Server $server, int $taskId, int $fromWorkerId, $data) => 144 | $data === 'octane-tick' 145 | ? $workerState->worker->handleTick() 146 | : $workerState->worker->handleTask($data) 147 | ); 148 | 149 | $server->on('finish', fn (Server $server, int $taskId, $result) => $result); 150 | 151 | /* 152 | |-------------------------------------------------------------------------- 153 | | Handle Worker & Server Shutdown 154 | |-------------------------------------------------------------------------- 155 | | 156 | | The following callbacks handle the master and worker shutdown events so 157 | | we can clean up any state, including the server state file. An event 158 | | will be dispatched by the worker so the developer can take action. 159 | | 160 | */ 161 | 162 | $server->on('workerstop', function () use ($workerState) { 163 | if ($workerState->tickTimerId) { 164 | Timer::clear($workerState->tickTimerId); 165 | } 166 | 167 | $workerState->worker->terminate(); 168 | }); 169 | 170 | $server->start(); 171 | -------------------------------------------------------------------------------- /src/Commands/StartSwooleCommand.php: -------------------------------------------------------------------------------- 1 | isInstalled()) { 56 | $this->error('The Swoole extension is missing.'); 57 | 58 | return 1; 59 | } 60 | 61 | if ($inspector->serverIsRunning()) { 62 | $this->error('Server is already running.'); 63 | 64 | return 1; 65 | } 66 | 67 | if (config('octane.swoole.ssl', false) === true && ! defined('SWOOLE_SSL')) { 68 | $this->error('You must configure Swoole with `--enable-openssl` to support ssl.'); 69 | 70 | return 1; 71 | } 72 | 73 | $this->writeServerStateFile($serverStateFile, $extension); 74 | 75 | $this->forgetEnvironmentVariables(); 76 | 77 | $server = tap(new Process([ 78 | (new PhpExecutableFinder)->find(), 79 | ...config('octane.swoole.php_options', []), 80 | config('octane.swoole.command', 'swoole-server'), 81 | $serverStateFile->path(), 82 | ], realpath(__DIR__.'/../../bin'), [ 83 | 'APP_ENV' => app()->environment(), 84 | 'APP_BASE_PATH' => base_path(), 85 | 'LARAVEL_OCTANE' => 1, 86 | ]))->start(); 87 | 88 | return $this->runServer($server, $inspector, 'swoole'); 89 | } 90 | 91 | /** 92 | * Write the Swoole server state file. 93 | * 94 | * @return void 95 | */ 96 | protected function writeServerStateFile( 97 | ServerStateFile $serverStateFile, 98 | SwooleExtension $extension 99 | ) { 100 | $serverStateFile->writeState([ 101 | 'appName' => config('app.name', 'Laravel'), 102 | 'host' => $this->getHost(), 103 | 'port' => $this->getPort(), 104 | 'workers' => $this->workerCount($extension), 105 | 'taskWorkers' => $this->taskWorkerCount($extension), 106 | 'maxRequests' => $this->option('max-requests'), 107 | 'publicPath' => public_path(), 108 | 'storagePath' => storage_path(), 109 | 'defaultServerOptions' => $this->defaultServerOptions($extension), 110 | 'octaneConfig' => config('octane'), 111 | ]); 112 | } 113 | 114 | /** 115 | * Get the default Swoole server options. 116 | * 117 | * @return array 118 | */ 119 | protected function defaultServerOptions(SwooleExtension $extension) 120 | { 121 | return [ 122 | 'enable_coroutine' => false, 123 | 'daemonize' => false, 124 | 'log_file' => storage_path('logs/swoole_http.log'), 125 | 'log_level' => app()->environment('local') ? SWOOLE_LOG_INFO : SWOOLE_LOG_ERROR, 126 | 'max_request' => $this->option('max-requests'), 127 | 'package_max_length' => 10 * 1024 * 1024, 128 | 'reactor_num' => $this->workerCount($extension), 129 | 'send_yield' => true, 130 | 'socket_buffer_size' => 10 * 1024 * 1024, 131 | 'task_max_request' => $this->option('max-requests'), 132 | 'task_worker_num' => $this->taskWorkerCount($extension), 133 | 'worker_num' => $this->workerCount($extension), 134 | ]; 135 | } 136 | 137 | /** 138 | * Get the number of workers that should be started. 139 | * 140 | * @return int 141 | */ 142 | protected function workerCount(SwooleExtension $extension) 143 | { 144 | return $this->option('workers') === 'auto' 145 | ? $extension->cpuCount() 146 | : $this->option('workers'); 147 | } 148 | 149 | /** 150 | * Get the number of task workers that should be started. 151 | * 152 | * @return int 153 | */ 154 | protected function taskWorkerCount(SwooleExtension $extension) 155 | { 156 | return $this->option('task-workers') === 'auto' 157 | ? $extension->cpuCount() 158 | : $this->option('task-workers'); 159 | } 160 | 161 | /** 162 | * Write the server process output ot the console. 163 | * 164 | * @param \Symfony\Component\Process\Process $server 165 | * @return void 166 | */ 167 | protected function writeServerOutput($server) 168 | { 169 | [$output, $errorOutput] = $this->getServerOutput($server); 170 | 171 | Str::of($output) 172 | ->explode("\n") 173 | ->filter() 174 | ->each(fn ($output) => is_array($stream = json_decode($output, true)) 175 | ? $this->handleStream($stream) 176 | : $this->info($output) 177 | ); 178 | 179 | Str::of($errorOutput) 180 | ->explode("\n") 181 | ->filter() 182 | ->groupBy(fn ($output) => $output) 183 | ->each(function ($group) { 184 | is_array($stream = json_decode($output = $group->first(), true)) && isset($stream['type']) 185 | ? $this->handleStream($stream) 186 | : $this->raw($output); 187 | }); 188 | } 189 | 190 | /** 191 | * Stop the server. 192 | * 193 | * @return void 194 | */ 195 | protected function stopServer() 196 | { 197 | $this->callSilent('octane:stop', [ 198 | '--server' => 'swoole', 199 | ]); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Cache/OctaneStore.php: -------------------------------------------------------------------------------- 1 | table->get($key); 41 | 42 | if (! $this->recordIsFalseOrExpired($record)) { 43 | return unserialize($record['value']); 44 | } 45 | 46 | if (in_array($key, $this->intervals) && 47 | ! is_null($interval = $this->getInterval($key))) { 48 | return $interval['resolver'](); 49 | } 50 | } 51 | 52 | /** 53 | * Retrieve an interval item from the cache. 54 | * 55 | * @param string $key 56 | * @return array|null 57 | */ 58 | protected function getInterval($key) 59 | { 60 | $interval = $this->get('interval-'.$key); 61 | 62 | return $interval ? unserialize($interval) : null; 63 | } 64 | 65 | /** 66 | * Retrieve multiple items from the cache by key. 67 | * 68 | * Items not found in the cache will have a null value. 69 | * 70 | * @return array 71 | */ 72 | public function many(array $keys) 73 | { 74 | return collect($keys)->mapWithKeys(fn ($key) => [$key => $this->get($key)])->all(); 75 | } 76 | 77 | /** 78 | * Store an item in the cache for a given number of seconds. 79 | * 80 | * @param string $key 81 | * @param mixed $value 82 | * @param int $seconds 83 | * @return bool 84 | */ 85 | public function put($key, $value, $seconds) 86 | { 87 | return $this->table->set($key, [ 88 | 'value' => serialize($value), 89 | 'expiration' => Carbon::now()->getTimestamp() + $seconds, 90 | ]); 91 | } 92 | 93 | /** 94 | * Store multiple items in the cache for a given number of seconds. 95 | * 96 | * @param int $seconds 97 | * @return bool 98 | */ 99 | public function putMany(array $values, $seconds) 100 | { 101 | foreach ($values as $key => $value) { 102 | $this->put($key, $value, $seconds); 103 | } 104 | 105 | return true; 106 | } 107 | 108 | /** 109 | * Increment the value of an item in the cache. 110 | * 111 | * @param string $key 112 | * @param mixed $value 113 | * @return int|bool 114 | */ 115 | public function increment($key, $value = 1) 116 | { 117 | $record = $this->table->get($key); 118 | 119 | if ($this->recordIsFalseOrExpired($record)) { 120 | return tap($value, fn ($value) => $this->put($key, $value, static::ONE_YEAR)); 121 | } 122 | 123 | return tap((int) (unserialize($record['value']) + $value), function ($value) use ($key, $record) { 124 | $this->put($key, $value, $record['expiration'] - Carbon::now()->getTimestamp()); 125 | }); 126 | } 127 | 128 | /** 129 | * Decrement the value of an item in the cache. 130 | * 131 | * @param string $key 132 | * @param mixed $value 133 | * @return int|bool 134 | */ 135 | public function decrement($key, $value = 1) 136 | { 137 | return $this->increment($key, $value * -1); 138 | } 139 | 140 | /** 141 | * Store an item in the cache indefinitely. 142 | * 143 | * @param string $key 144 | * @param mixed $value 145 | * @return bool 146 | */ 147 | public function forever($key, $value) 148 | { 149 | return $this->put($key, $value, static::ONE_YEAR); 150 | } 151 | 152 | /** 153 | * Register a cache key that should be refreshed at a given interval (in minutes). 154 | * 155 | * @param string $key 156 | * @param int $seconds 157 | * @return void 158 | */ 159 | public function interval($key, Closure $resolver, $seconds) 160 | { 161 | if (! is_null($this->getInterval($key))) { 162 | $this->intervals[] = $key; 163 | 164 | return; 165 | } 166 | 167 | $this->forever('interval-'.$key, serialize([ 168 | 'resolver' => new SerializableClosure($resolver), 169 | 'lastRefreshedAt' => null, 170 | 'refreshInterval' => $seconds, 171 | ])); 172 | 173 | $this->intervals[] = $key; 174 | } 175 | 176 | /** 177 | * Refresh all of the applicable interval caches. 178 | * 179 | * @return void 180 | */ 181 | public function refreshIntervalCaches() 182 | { 183 | foreach ($this->intervals as $key) { 184 | if (! $this->intervalShouldBeRefreshed($interval = $this->getInterval($key))) { 185 | continue; 186 | } 187 | 188 | try { 189 | $this->forever('interval-'.$key, serialize(array_merge( 190 | $interval, ['lastRefreshedAt' => Carbon::now()->getTimestamp()], 191 | ))); 192 | 193 | $this->forever($key, $interval['resolver']()); 194 | } catch (Throwable $e) { 195 | report($e); 196 | } 197 | } 198 | } 199 | 200 | /** 201 | * Determine if the given interval record should be refreshed. 202 | * 203 | * @return bool 204 | */ 205 | protected function intervalShouldBeRefreshed(array $interval) 206 | { 207 | return is_null($interval['lastRefreshedAt']) || 208 | (Carbon::now()->getTimestamp() - $interval['lastRefreshedAt']) >= $interval['refreshInterval']; 209 | } 210 | 211 | /** 212 | * Remove an item from the cache. 213 | * 214 | * @param string $key 215 | * @return bool 216 | */ 217 | public function forget($key) 218 | { 219 | return $this->table->del($key); 220 | } 221 | 222 | /** 223 | * Remove all items from the cache. 224 | * 225 | * @return bool 226 | */ 227 | public function flush() 228 | { 229 | foreach ($this->table as $key => $record) { 230 | if (str_starts_with($key, 'interval-')) { 231 | continue; 232 | } 233 | 234 | $this->forget($key); 235 | } 236 | 237 | return true; 238 | } 239 | 240 | /** 241 | * Determine if the record is missing or expired. 242 | * 243 | * @param array|null $record 244 | * @return bool 245 | */ 246 | protected function recordIsFalseOrExpired($record) 247 | { 248 | return $record === false || $record['expiration'] <= Carbon::now()->getTimestamp(); 249 | } 250 | 251 | /** 252 | * Get the cache key prefix. 253 | * 254 | * @return string 255 | */ 256 | public function getPrefix() 257 | { 258 | return ''; 259 | } 260 | } 261 | --------------------------------------------------------------------------------