Files
archived-frankenphp/docs/extension-workers.md

5.8 KiB

Extension Workers

Extension Workers enable your FrankenPHP extension 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.

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).

In a Pure Go Application (Embedding)

If you are embedding FrankenPHP in a standard Go application without caddy, 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 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

// #include <Zend/zend_types.h>
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.).

// #include <Zend/zend_types.h>
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
// Handle both raw messages and HTTP requests in the same loop
$handler = function ($payload = null) {
    // Case 1: Message Mode
    if ($payload !== null) {
        return "Received payload: " . $payload;
    }

    // Case 2: HTTP Mode (standard PHP superglobals are populated)
    echo "Hello from page: " . $_SERVER['REQUEST_URI'];
};

while (frankenphp_handle_request($handler)) {
    gc_collect_cycles();
}

Lifecycle Hooks

FrankenPHP provides hooks to execute Go code at specific points in the lifecycle.

Hook Type Option Name Signature Context & Use Case
Server WithWorkerOnServerStartup func() Global setup. Run Once. Example: Connect to NATS/Redis.
Server WithWorkerOnServerShutdown func() Global cleanup. Run Once. Example: Close shared connections.
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

package myextension

import (
    "fmt"
    "github.com/dunglas/frankenphp"
    frankenphpCaddy "github.com/dunglas/frankenphp/caddy"
)

func init() {
    workerHandle = frankenphpCaddy.RegisterWorkers(
        "my-worker", "worker.php", 2,

        // Server Startup (Global)
        frankenphp.WithWorkerOnServerStartup(func() {
            fmt.Println("Extension: Server starting up...")
        }),

        // Thread Ready (Per Thread)
        // Note: The function accepts an integer representing the Thread ID
        frankenphp.WithWorkerOnReady(func(id int) {
            fmt.Printf("Extension: Worker thread #%d is ready.\n", id)
        }),
    )
}