|
| 1 | +# Application Sidekicks |
| 2 | + |
| 3 | +Sidekicks are long-running PHP workers that run **outside the HTTP request cycle**. |
| 4 | +They observe their environment (Redis Sentinel, secret vaults, feature flag services, etc.) |
| 5 | +and publish configuration to HTTP workers in real time — without polling, approximate TTLs, or redeployment. |
| 6 | + |
| 7 | +## How It Works |
| 8 | + |
| 9 | +- A sidekick runs its own infinite loop (subscribe to Redis, watch files, poll an API, etc.) |
| 10 | +- It calls `frankenphp_set_server_var()` to publish key-value pairs |
| 11 | +- HTTP workers receive those values in `$_SERVER` at each `frankenphp_handle_request()` iteration |
| 12 | +- Values are **consistent for the entire request** — no mid-request mutation |
| 13 | + |
| 14 | +## Configuration |
| 15 | + |
| 16 | +Add a `sidekick_entrypoint` to your `php_server` block in the Caddyfile. |
| 17 | +This is the script that will be executed for every sidekick started by workers of this server: |
| 18 | + |
| 19 | +```caddyfile |
| 20 | +example.com { |
| 21 | + php_server { |
| 22 | + sidekick_entrypoint /app/bin/console |
| 23 | + } |
| 24 | +} |
| 25 | +``` |
| 26 | + |
| 27 | +Sidekicks are then started dynamically from PHP using `frankenphp_sidekick_start()`. |
| 28 | + |
| 29 | +## PHP API |
| 30 | + |
| 31 | +### `frankenphp_sidekick_start(string $name, array $argv = []): void` |
| 32 | + |
| 33 | +Starts a sidekick worker. The configured `sidekick_entrypoint` script is executed |
| 34 | +in a new PHP thread with `$name` as the first argument in `$_SERVER['argv']`. |
| 35 | + |
| 36 | +- **At-most-once by name**: calling it multiple times with the same `$name` is a no-op (safe with multiple HTTP workers). Only the first call determines the argv values |
| 37 | +- **`$_SERVER['argv']`** in the sidekick will be `[script_path, name, ...argv]` |
| 38 | +- Throws `ValueError` if `$name` is empty |
| 39 | +- Throws `InvalidArgumentException` if `$argv` contains non-string values |
| 40 | +- Throws `RuntimeException` if no `sidekick_entrypoint` is configured or no thread is available |
| 41 | + |
| 42 | +### `frankenphp_set_server_var(string $key, ?string $value): void` |
| 43 | + |
| 44 | +Sets a variable that will be injected into `$_SERVER` of all HTTP workers. |
| 45 | +Can be called from any worker (sidekick or HTTP). |
| 46 | + |
| 47 | +- Passing `null` as `$value` removes the key (it will no longer be injected) |
| 48 | +- Throws `ValueError` if `$key` is empty or starts with `HTTP_` (reserved for HTTP request headers) |
| 49 | + |
| 50 | +### `frankenphp_sidekick_should_stop(): bool` |
| 51 | + |
| 52 | +Returns `true` when FrankenPHP is shutting down. Sidekick scripts must poll this |
| 53 | +in their event loop to exit gracefully: |
| 54 | + |
| 55 | +```php |
| 56 | +while (!frankenphp_sidekick_should_stop()) { |
| 57 | + // do work |
| 58 | + usleep(100_000); |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +## Example |
| 63 | + |
| 64 | +### Sidekick Script |
| 65 | + |
| 66 | +```php |
| 67 | +<?php |
| 68 | +// bin/sidekick.php |
| 69 | + |
| 70 | +require __DIR__.'/../vendor/autoload.php'; |
| 71 | + |
| 72 | +$argv = $_SERVER['argv'] ?? []; |
| 73 | +array_shift($argv); // skip argv[0] (script name) |
| 74 | + |
| 75 | +// Set initial values |
| 76 | +frankenphp_set_server_var('REDIS_MASTER_HOST', '10.0.0.1'); |
| 77 | +frankenphp_set_server_var('REDIS_MASTER_PORT', '6379'); |
| 78 | + |
| 79 | +while (!frankenphp_sidekick_should_stop()) { |
| 80 | + // Watch for changes (e.g., Redis Sentinel, file watcher, API poll) |
| 81 | + $master = discoverRedisMaster(); |
| 82 | + frankenphp_set_server_var('REDIS_MASTER_HOST', $master['host']); |
| 83 | + frankenphp_set_server_var('REDIS_MASTER_PORT', (string) $master['port']); |
| 84 | + |
| 85 | + usleep(100_000); |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +### Starting from the HTTP Worker |
| 90 | + |
| 91 | +```php |
| 92 | +<?php |
| 93 | +// public/index.php |
| 94 | + |
| 95 | +// Start sidekicks before your application boots |
| 96 | +// At-most-once: safe to call from multiple HTTP worker threads |
| 97 | +if (function_exists('frankenphp_sidekick_start')) { |
| 98 | + frankenphp_sidekick_start('redis-watcher', ['--sentinel-host=10.0.0.1']); |
| 99 | +} |
| 100 | + |
| 101 | +// Your application entry point |
| 102 | +require __DIR__.'/../vendor/autoload.php'; |
| 103 | + |
| 104 | +$app = new App(); |
| 105 | +$app->boot(); |
| 106 | + |
| 107 | +$handler = static function () use ($app) { |
| 108 | + // $_SERVER['REDIS_MASTER_HOST'] contains the latest value |
| 109 | + // published by the sidekick — consistent for the entire request |
| 110 | + echo $app->handle($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER); |
| 111 | +}; |
| 112 | + |
| 113 | +while (frankenphp_handle_request($handler)) { |
| 114 | + $app->terminate(); |
| 115 | + gc_collect_cycles(); |
| 116 | +} |
| 117 | + |
| 118 | +$app->shutdown(); |
| 119 | +``` |
| 120 | + |
| 121 | +### Reading Sidekick Variables |
| 122 | + |
| 123 | +In your application code, read the values from `$_SERVER`: |
| 124 | + |
| 125 | +```php |
| 126 | +$redisHost = $_SERVER['REDIS_MASTER_HOST'] ?? '127.0.0.1'; |
| 127 | +$redisPort = $_SERVER['REDIS_MASTER_PORT'] ?? '6379'; |
| 128 | + |
| 129 | +$redis = new \Redis(); |
| 130 | +$redis->connect($redisHost, (int) $redisPort); |
| 131 | +``` |
| 132 | + |
| 133 | +## Runtime Behavior |
| 134 | + |
| 135 | +- **Execution timeout disabled**: sidekick threads automatically call `zend_unset_timeout()` |
| 136 | +- **Shebang support**: entrypoints with `#!/usr/bin/env php` are handled correctly |
| 137 | +- **Crash recovery**: if a sidekick script exits, FrankenPHP restarts it automatically (using the existing worker restart logic with exponential backoff) |
| 138 | +- **Graceful shutdown**: sidekicks detect shutdown via `frankenphp_sidekick_should_stop()` and exit their loop |
| 139 | +- **`SCRIPT_FILENAME`**: set to the entrypoint's full path, so `dirname(__DIR__)` works correctly |
| 140 | +- **`$_SERVER['argv']`**: follows CLI conventions — `argv[0]` is the script path, followed by the arguments passed to `frankenphp_sidekick_start()` |
| 141 | + |
| 142 | +## Debugging |
| 143 | + |
| 144 | +- Use `error_log()` or `frankenphp_log()` for structured output — these go through FrankenPHP's logger |
| 145 | +- Avoid `echo` in sidekicks — it produces unstructured, unattributed log entries |
| 146 | +- Sidekick scripts can be tested standalone: `php bin/sidekick.php` |
0 commit comments