From 7381ff3428ab0d1a616c308e8fbc7ca1f945b2b6 Mon Sep 17 00:00:00 2001 From: y-l-g Date: Mon, 1 Dec 2025 19:45:25 +0100 Subject: [PATCH 1/5] docs: document the extensionworkers api --- docs/extensions.md | 173 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/docs/extensions.md b/docs/extensions.md index 2b9168880..35081a444 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -853,3 +853,176 @@ echo go_upper("hello world") . "\n"; ``` You can now run FrankenPHP with this file using `./frankenphp php-server`, and you should see your extension working. + +## Extension Workers + +Extension Workers enable your Go extension to manage a dedicated pool of PHP threads for executing background tasks, handling asynchronous events, or implementing custom protocols. Usefull 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) using `frankenphp.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, or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or a background 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 + Date: Mon, 1 Dec 2025 20:24:50 +0100 Subject: [PATCH 2/5] docs: fix typos --- docs/extensions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 35081a444..604743f90 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -897,7 +897,7 @@ If you plan to share your extension (like a generic queue or event listener), yo If you are embedding FrankenPHP in a standard Go application (without Caddy) using `frankenphp.ServeHTTP`, you can register extension workers using `frankenphp.WithExtensionWorkers` when initializing options. -## Interacting with Workers +### 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, or from any Go logic such as a cron scheduler, an event listener (MQTT, Kafka), or a background goroutine. @@ -965,7 +965,7 @@ func my_worker_http_request(path *C.zend_string) unsafe.Pointer { } ``` -#### Worker Script +### Worker Script The PHP worker script runs in a loop and can handle both raw messages and HTTP requests. From 2f3a654b92cd9b7f6cdbda97da3d628f445c6222 Mon Sep 17 00:00:00 2001 From: y-l-g Date: Mon, 1 Dec 2025 20:31:41 +0100 Subject: [PATCH 3/5] docs: fix linting --- docs/extensions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensions.md b/docs/extensions.md index 604743f90..1c0c31d16 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -905,7 +905,7 @@ Once the worker pool is active, you can dispatch tasks to it. This can be done i Use `SendMessage` to pass raw data directly to your worker script. This is ideal for queues or simple commands. -**Example: An Async Queue Extension** +##### Example: An Async Queue Extension ```go // #include @@ -998,7 +998,7 @@ FrankenPHP provides hooks to execute Go code at specific points in the lifecycle | **Thread** | `WithWorkerOnReady` | `func(threadID int)` | Per-thread setup. Called when a thread starts. Receives the Thread ID. | | **Thread** | `WithWorkerOnShutdown` | `func(threadID int)` | Per-thread cleanup. Receives the Thread ID. | -**Example:** +#### Example: ```go package myextension From b26ce6ccd8f43698a839537e68f3eacbdca8c535 Mon Sep 17 00:00:00 2001 From: y-l-g Date: Mon, 1 Dec 2025 22:15:12 +0100 Subject: [PATCH 4/5] docs: create a dedicated section for extension workers --- docs/extension-workers.md | 172 +++++++++++++++++++++++++++++++++++++ docs/extensions.md | 173 -------------------------------------- 2 files changed, 172 insertions(+), 173 deletions(-) create mode 100644 docs/extension-workers.md diff --git a/docs/extension-workers.md b/docs/extension-workers.md new file mode 100644 index 000000000..91105c483 --- /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. Usefull 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 + -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 - Date: Tue, 9 Dec 2025 13:26:17 +0100 Subject: [PATCH 5/5] doc: Fix typo in Extension Workers documentation --- docs/extension-workers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extension-workers.md b/docs/extension-workers.md index 91105c483..dd8527d27 100644 --- a/docs/extension-workers.md +++ b/docs/extension-workers.md @@ -1,6 +1,6 @@ # 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. Usefull for queue systems, event listeners, schedulers, etc. +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