├── 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 |

2 |
3 |
4 |
5 |
6 |
7 |
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 |
--------------------------------------------------------------------------------