Skip to content

Commit 0dd485c

Browse files
authored
docs: document the extensionworkers api (#2055)
1 parent 6d86ea8 commit 0dd485c

File tree

1 file changed

+172
-0
lines changed

1 file changed

+172
-0
lines changed

docs/extension-workers.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Extension Workers
2+
3+
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.
4+
5+
## Registering the Worker
6+
7+
### Static Registration
8+
9+
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.
10+
11+
```go
12+
package myextension
13+
14+
import (
15+
"github.com/dunglas/frankenphp"
16+
"github.com/dunglas/frankenphp/caddy"
17+
)
18+
19+
// Global handle to communicate with the worker pool
20+
var worker frankenphp.Workers
21+
22+
func init() {
23+
// Register the worker when the module is loaded.
24+
worker = caddy.RegisterWorkers(
25+
"my-internal-worker", // Unique name
26+
"worker.php", // Script path (relative to execution or absolute)
27+
2, // Fixed Thread count
28+
// Optional Lifecycle Hooks
29+
frankenphp.WithWorkerOnServerStartup(func() {
30+
// Global setup logic...
31+
}),
32+
)
33+
}
34+
```
35+
36+
### In a Caddy Module (Configurable by the user)
37+
38+
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)).
39+
40+
### In a Pure Go Application (Embedding)
41+
42+
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.
43+
44+
## Interacting with Workers
45+
46+
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.
47+
48+
### Headless Mode : `SendMessage`
49+
50+
Use `SendMessage` to pass raw data directly to your worker script. This is ideal for queues or simple commands.
51+
52+
#### Example: An Async Queue Extension
53+
54+
```go
55+
// #include <Zend/zend_types.h>
56+
import "C"
57+
import (
58+
"context"
59+
"unsafe"
60+
"github.com/dunglas/frankenphp"
61+
)
62+
63+
//export_php:function my_queue_push(mixed $data): bool
64+
func my_queue_push(data *C.zval) bool {
65+
// 1. Ensure worker is ready
66+
if worker == nil {
67+
return false
68+
}
69+
70+
// 2. Dispatch to the background worker
71+
_, err := worker.SendMessage(
72+
context.Background(), // Standard Go context
73+
unsafe.Pointer(data), // Data to pass to the worker
74+
nil, // Optional http.ResponseWriter
75+
)
76+
77+
return err == nil
78+
}
79+
```
80+
81+
### HTTP Emulation :`SendRequest`
82+
83+
Use `SendRequest` if your extension needs to invoke a PHP script that expects a standard web environment (populating `$_SERVER`, `$_GET`, etc.).
84+
85+
```go
86+
// #include <Zend/zend_types.h>
87+
import "C"
88+
import (
89+
"net/http"
90+
"net/http/httptest"
91+
"unsafe"
92+
"github.com/dunglas/frankenphp"
93+
)
94+
95+
//export_php:function my_worker_http_request(string $path): string
96+
func my_worker_http_request(path *C.zend_string) unsafe.Pointer {
97+
// 1. Prepare the request and recorder
98+
url := frankenphp.GoString(unsafe.Pointer(path))
99+
req, _ := http.NewRequest("GET", url, http.NoBody)
100+
rr := httptest.NewRecorder()
101+
102+
// 2. Dispatch to the worker
103+
if err := worker.SendRequest(rr, req); err != nil {
104+
return nil
105+
}
106+
107+
// 3. Return the captured response
108+
return frankenphp.PHPString(rr.Body.String(), false)
109+
}
110+
```
111+
112+
## Worker Script
113+
114+
The PHP worker script runs in a loop and can handle both raw messages and HTTP requests.
115+
116+
```php
117+
<?php
118+
// Handle both raw messages and HTTP requests in the same loop
119+
$handler = function ($payload = null) {
120+
// Case 1: Message Mode
121+
if ($payload !== null) {
122+
return "Received payload: " . $payload;
123+
}
124+
125+
// Case 2: HTTP Mode (standard PHP superglobals are populated)
126+
echo "Hello from page: " . $_SERVER['REQUEST_URI'];
127+
};
128+
129+
while (frankenphp_handle_request($handler)) {
130+
gc_collect_cycles();
131+
}
132+
```
133+
134+
## Lifecycle Hooks
135+
136+
FrankenPHP provides hooks to execute Go code at specific points in the lifecycle.
137+
138+
| Hook Type | Option Name | Signature | Context & Use Case |
139+
| :--------- | :--------------------------- | :------------------- | :--------------------------------------------------------------------- |
140+
| **Server** | `WithWorkerOnServerStartup` | `func()` | Global setup. Run **Once**. Example: Connect to NATS/Redis. |
141+
| **Server** | `WithWorkerOnServerShutdown` | `func()` | Global cleanup. Run **Once**. Example: Close shared connections. |
142+
| **Thread** | `WithWorkerOnReady` | `func(threadID int)` | Per-thread setup. Called when a thread starts. Receives the Thread ID. |
143+
| **Thread** | `WithWorkerOnShutdown` | `func(threadID int)` | Per-thread cleanup. Receives the Thread ID. |
144+
145+
### Example
146+
147+
```go
148+
package myextension
149+
150+
import (
151+
"fmt"
152+
"github.com/dunglas/frankenphp"
153+
frankenphpCaddy "github.com/dunglas/frankenphp/caddy"
154+
)
155+
156+
func init() {
157+
workerHandle = frankenphpCaddy.RegisterWorkers(
158+
"my-worker", "worker.php", 2,
159+
160+
// Server Startup (Global)
161+
frankenphp.WithWorkerOnServerStartup(func() {
162+
fmt.Println("Extension: Server starting up...")
163+
}),
164+
165+
// Thread Ready (Per Thread)
166+
// Note: The function accepts an integer representing the Thread ID
167+
frankenphp.WithWorkerOnReady(func(id int) {
168+
fmt.Printf("Extension: Worker thread #%d is ready.\n", id)
169+
}),
170+
)
171+
}
172+
```

0 commit comments

Comments
 (0)