From 5327982b73390df3d5075b497aff630b646b54e7 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 a81c2355a5..e86830366b 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -906,3 +906,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 e86830366b..9b4ec37ba6 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -950,7 +950,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. @@ -1018,7 +1018,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 ad1319055d17d7c2679629c9d913076afc37571c 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 9b4ec37ba6..da0f48b1f1 100644 --- a/docs/extensions.md +++ b/docs/extensions.md @@ -958,7 +958,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 @@ -1051,7 +1051,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 aa0c3e7584f262508cdcaa3bfa34d0c8bd87bf6e 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 0000000000..91105c483b --- /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 91105c483b..dd8527d272 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