diff --git a/docs/extension-workers.md b/docs/extension-workers.md new file mode 100644 index 000000000..dd8527d27 --- /dev/null +++ b/docs/extension-workers.md @@ -0,0 +1,172 @@ +# Extension Workers + +Extension Workers enable your [FrankenPHP extension](https://frankenphp.dev/docs/extensions/) to manage a dedicated pool of PHP threads for executing background tasks, handling asynchronous events, or implementing custom protocols. Useful for queue systems, event listeners, schedulers, etc. + +## Registering the Worker + +### Static Registration + +If you don't need to make the worker configurable by the user (fixed script path, fixed number of threads), you can simply register the worker in the `init()` function. + +```go +package myextension + +import ( + "github.com/dunglas/frankenphp" + "github.com/dunglas/frankenphp/caddy" +) + +// Global handle to communicate with the worker pool +var worker frankenphp.Workers + +func init() { + // Register the worker when the module is loaded. + worker = caddy.RegisterWorkers( + "my-internal-worker", // Unique name + "worker.php", // Script path (relative to execution or absolute) + 2, // Fixed Thread count + // Optional Lifecycle Hooks + frankenphp.WithWorkerOnServerStartup(func() { + // Global setup logic... + }), + ) +} +``` + +### In a Caddy Module (Configurable by the user) + +If you plan to share your extension (like a generic queue or event listener), you should wrap it in a Caddy module. This allows users to configure the script path and thread count via their `Caddyfile`. This requires implementing the `caddy.Provisioner` interface and parsing the Caddyfile ([see an example](https://github.com/dunglas/frankenphp-queue/blob/989120d394d66dd6c8e2101cac73dd622fade334/caddy.go)). + +### In a Pure Go Application (Embedding) + +If you are [embedding FrankenPHP in a standard Go application without caddy](https://pkg.go.dev/github.com/dunglas/frankenphp#example-ServeHTTP), you can register extension workers using `frankenphp.WithExtensionWorkers` when initializing options. + +## Interacting with Workers + +Once the worker pool is active, you can dispatch tasks to it. This can be done inside [native functions exported to PHP](https://frankenphp.dev/docs/extensions/#writing-the-extension), or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or a any other goroutine. + +### Headless Mode : `SendMessage` + +Use `SendMessage` to pass raw data directly to your worker script. This is ideal for queues or simple commands. + +#### Example: An Async Queue Extension + +```go +// #include +import "C" +import ( + "context" + "unsafe" + "github.com/dunglas/frankenphp" +) + +//export_php:function my_queue_push(mixed $data): bool +func my_queue_push(data *C.zval) bool { + // 1. Ensure worker is ready + if worker == nil { + return false + } + + // 2. Dispatch to the background worker + _, err := worker.SendMessage( + context.Background(), // Standard Go context + unsafe.Pointer(data), // Data to pass to the worker + nil, // Optional http.ResponseWriter + ) + + return err == nil +} +``` + +### HTTP Emulation :`SendRequest` + +Use `SendRequest` if your extension needs to invoke a PHP script that expects a standard web environment (populating `$_SERVER`, `$_GET`, etc.). + +```go +// #include +import "C" +import ( + "net/http" + "net/http/httptest" + "unsafe" + "github.com/dunglas/frankenphp" +) + +//export_php:function my_worker_http_request(string $path): string +func my_worker_http_request(path *C.zend_string) unsafe.Pointer { + // 1. Prepare the request and recorder + url := frankenphp.GoString(unsafe.Pointer(path)) + req, _ := http.NewRequest("GET", url, http.NoBody) + rr := httptest.NewRecorder() + + // 2. Dispatch to the worker + if err := worker.SendRequest(rr, req); err != nil { + return nil + } + + // 3. Return the captured response + return frankenphp.PHPString(rr.Body.String(), false) +} +``` + +## Worker Script + +The PHP worker script runs in a loop and can handle both raw messages and HTTP requests. + +```php +