Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions app/Config/WorkerMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Config;

/**
* This configuration controls how CodeIgniter behaves when running
* in worker mode (with FrankenPHP).
*/
class WorkerMode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this config need to extend BaseConfig?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep in mind that these values will change in Registrars and env.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't think so. Although overrides from the .env file might be acceptable in theory, allowing Registrars would be misleading, as the values are only used once and not on a per-request basis. I would rather leave it as it is.

{
/**
* Persistent Services
*
* List of service names that should persist across requests.
* These services will NOT be reset between requests.
*
* Services not in this list will be reset for each request to prevent
* state leakage.
*
* Recommended persistent services:
* - `autoloader`: PSR-4 autoloading configuration
* - `locator`: File locator
* - `exceptions`: Exception handler
* - `commands`: CLI commands registry
* - `codeigniter`: Main application instance
* - `superglobals`: Superglobals wrapper
* - `routes`: Router configuration
* - `cache`: Cache instance
*
* @var list<string>
*/
public array $persistentServices = [
'autoloader',
'locator',
'exceptions',
'commands',
'codeigniter',
'superglobals',
'routes',
'cache',
];

/**
* Force Garbage Collection
*
* Whether to force garbage collection after each request.
* Helps prevent memory leaks at a small performance cost.
*/
public bool $forceGarbageCollection = true;
}
33 changes: 33 additions & 0 deletions system/Boot.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,39 @@ public static function bootWeb(Paths $paths): int
return EXIT_SUCCESS;
}

/**
* Bootstrap for FrankenPHP worker mode.
*
* This method performs one-time initialization for worker mode,
* loading everything except the CodeIgniter instance, which should
* be created fresh for each request.
*
* @used-by `public/frankenphp-worker.php`
*/
public static function bootWorker(Paths $paths): CodeIgniter
{
static::definePathConstants($paths);
if (! defined('APP_NAMESPACE')) {
static::loadConstants();
}
static::checkMissingExtensions();

static::loadDotEnv($paths);
static::defineEnvironment();
static::loadEnvironmentBootstrap($paths);

static::loadCommonFunctions();
static::loadAutoloader();
static::setExceptionHandler();
static::initializeKint();

// Note: We skip config caching in worker mode
// as it may cause issues with state between requests
static::autoloadHelpers();

return Boot::initializeCodeIgniter();
}

/**
* Used by command line scripts other than
* * `spark`
Expand Down
22 changes: 22 additions & 0 deletions system/Cache/Handlers/BaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,26 @@ public function remember(string $key, int $ttl, Closure $callback): mixed

return $value;
}

/**
* Check if connection is alive.
*
* Default implementation for handlers that don't require connection management.
* Handlers with persistent connections (Redis, Predis, Memcached) should override this.
*/
public function ping(): bool
{
return true;
}

/**
* Reconnect to the cache server.
*
* Default implementation for handlers that don't require connection management.
* Handlers with persistent connections (Redis, Predis, Memcached) should override this.
*/
public function reconnect(): bool
{
return true;
}
}
54 changes: 42 additions & 12 deletions system/Cache/Handlers/MemcachedHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,6 @@ public function __construct(Cache $config)
$this->config = array_merge($this->config, $config->memcached);
}

/**
* Closes the connection to Memcache(d) if present.
*/
public function __destruct()
{
if ($this->memcached instanceof Memcached) {
$this->memcached->quit();
} elseif ($this->memcached instanceof Memcache) {
$this->memcached->close();
}
}

public function initialize(): void
{
try {
Expand Down Expand Up @@ -230,4 +218,46 @@ public function isSupported(): bool
{
return extension_loaded('memcached') || extension_loaded('memcache');
}

public function ping(): bool
{
$version = $this->memcached->getVersion();

if ($this->memcached instanceof Memcached) {
// Memcached extension returns array with server:port => version
if (! is_array($version)) {
return false;
}

$serverKey = $this->config['host'] . ':' . $this->config['port'];

return isset($version[$serverKey]) && $version[$serverKey] !== false;
}

if ($this->memcached instanceof Memcache) {
// Memcache extension returns string version
return is_string($version) && $version !== '';
}

return false;
}

public function reconnect(): bool
{
if ($this->memcached instanceof Memcached) {
$this->memcached->quit();
} elseif ($this->memcached instanceof Memcache) {
$this->memcached->close();
}

try {
$this->initialize();

return true;
} catch (CriticalError $e) {
log_message('error', 'Cache: Memcached reconnection failed: ' . $e->getMessage());

return false;
}
}
}
34 changes: 34 additions & 0 deletions system/Cache/Handlers/PredisHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,38 @@ public function isSupported(): bool
{
return class_exists(Client::class);
}

public function ping(): bool
{
try {
$result = $this->redis->ping();

if (is_object($result)) {
return $result->getPayload() === 'PONG';
}

return $result === 'PONG';
} catch (Exception) {
return false;
}
}

public function reconnect(): bool
{
try {
$this->redis->disconnect();
} catch (Exception) {
// Connection already dead, that's fine
}

try {
$this->initialize();

return true;
} catch (CriticalError $e) {
log_message('error', 'Cache: Predis reconnection failed: ' . $e->getMessage());

return false;
}
}
}
46 changes: 36 additions & 10 deletions system/Cache/Handlers/RedisHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,6 @@ public function __construct(Cache $config)
$this->config = array_merge($this->config, $config->redis);
}

/**
* Closes the connection to Redis if present.
*/
public function __destruct()
{
if (isset($this->redis)) {
$this->redis->close();
}
}

public function initialize(): void
{
$config = $this->config;
Expand Down Expand Up @@ -229,4 +219,40 @@ public function isSupported(): bool
{
return extension_loaded('redis');
}

public function ping(): bool
{
if (! isset($this->redis)) {
return false;
}

try {
$result = $this->redis->ping();

return in_array($result, [true, '+PONG'], true);
} catch (RedisException) {
return false;
}
}

public function reconnect(): bool
{
if (isset($this->redis)) {
try {
$this->redis->close();
} catch (RedisException) {
// Connection already dead, that's fine
}
}

try {
$this->initialize();

return true;
} catch (CriticalError $e) {
log_message('error', 'Cache: Redis reconnection failed: ' . $e->getMessage());

return false;
}
}
}
26 changes: 22 additions & 4 deletions system/CodeIgniter.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,14 @@ class CodeIgniter
/**
* Current response.
*
* @var ResponseInterface
* @var ResponseInterface|null
*/
protected $response;

/**
* Router to use.
*
* @var Router
* @var Router|null
*/
protected $router;

Expand All @@ -116,14 +116,14 @@ class CodeIgniter
/**
* Controller method to invoke.
*
* @var string
* @var string|null
*/
protected $method;

/**
* Output handler to use.
*
* @var string
* @var string|null
*/
protected $output;

Expand Down Expand Up @@ -192,6 +192,24 @@ public function initialize()
date_default_timezone_set($this->config->appTimezone ?? 'UTC');
}

/**
* Reset request-specific state for worker mode.
* Clears all request/response data to prepare for the next request.
*/
public function resetForWorkerMode(): void
{
$this->request = null;
$this->response = null;
$this->router = null;
$this->controller = null;
$this->method = null;
$this->output = null;

// Reset timing
$this->startTime = null;
$this->totalTime = 0;
}

/**
* Initializes Kint
*
Expand Down
42 changes: 42 additions & 0 deletions system/Commands/Worker/Views/Caddyfile.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# CodeIgniter 4 - FrankenPHP Worker Mode Configuration
#
# This Caddyfile configures FrankenPHP to run CodeIgniter in worker mode.
# Adjust settings based on your server resources and application needs.
#
# Start with: frankenphp run

{
# FrankenPHP worker configuration
frankenphp {
# Worker configuration
worker {
# Path to the worker file
file public/frankenphp-worker.php

# Number of workers (default: 2x CPU cores)
# Adjust based on your server capacity
# num 16
}
}

# Disable admin API (recommended for production)
admin off
}

# HTTP server configuration
:8080 {
# Document root
root * public

# Enable compression
encode zstd br gzip

# Route all PHP requests through the worker
php_server {
# Route all requests through the worker
try_files {path} frankenphp-worker.php
}

# Serve static files
file_server
}
Loading
Loading