FrankenPHP

repository·main·Indexed 11 days ago

https://github.com/php/frankenphp

A modern application server for PHP built on top of the Caddy web server. It features worker mode, early hints, real-time capabilities via Mercure, automatic HTTPS/HTTP3 support, and hot reloading. It can be installed via RPM, DEB, APK, Homebrew, or Docker, and is configurable through Caddyfile directives like php_server and php_ini.

Tokens
68.8K
Snippets
234
Records
319
Agent score
95%

What's inside FrankenPHP

  1. Writing PHP extensions in Go with FrankenPHP

    main

    FrankenPHP allows you to write PHP extensions in Go instead of the traditional C. This enables you to create high-performance native functions that can be called directly from PHP, allowing your PHP application to leverage Go libraries and the goroutine concurrency model.

    There are two primary ways to implement these extensions:

    1. Using the Extension Generator: The recommended approach. It generates the necessary boilerplate, allowing you to focus on your Go logic.
    2. Manual Implementation: Provides full control over the extension structure for advanced use cases.
  2. Avoid using placeholders in `root` and `env`

    main
    While you can use Caddy placeholders in root and env directives, doing so prevents these values from being cached, which introduces a significant performance penalty. Avoid using placeholders in these specific directives whenever possible.
  3. Using FrankenPHP in classic mode

    main

    FrankenPHP operates in 'classic mode' by default without additional configuration. In this mode, it acts as a drop-in replacement for PHP-FPM or Apache mod_php by directly serving PHP files.

    Key Characteristics

    • Connection Handling: Similar to Caddy, it accepts an unlimited number of connections, limited only by system resources.
    • Thread Pool: Uses a PHP thread pool to serve requests. You can configure this pool to have a fixed number of threads (similar to PHP-FPM static mode) or allow threads to scale automatically at runtime (similar to PHP-FPM dynamic mode).
    • Shared Pool: Each Caddy instance spins up exactly one FrankenPHP thread pool, which is shared across all php_server blocks in your configuration.

    Managing Request Queues

    By default, queued connections will wait indefinitely for an available PHP thread. To prevent resource exhaustion or excessive latency, you should implement timeouts:

    1. max_wait_time: Use this in FrankenPHP's global configuration to limit how long a request waits for a free PHP thread before being rejected.
    2. Caddy Write Timeout: Set a reasonable write timeout in your Caddyfile to manage connection lifecycles.
  4. Enable full-duplex mode for HTTP/1.x

    main

    To allow writing a response before the entire body has been read (required for technologies like Mercure, WebSockets, or Server-Sent Events) when using HTTP/1.x, you must opt-in by enabling enable_full_duplex in the global server options.

    CAUTION: Enabling this may cause older HTTP/1.x clients that do not support full-duplex to deadlock.

    {
      servers {
        enable_full_duplex
      }
    }

    Or via environment variable:

    CADDY_GLOBAL_OPTIONS="servers { enable_full_duplex }"
  5. How FrankenPHP worker mode works

    main

    Worker mode allows you to boot your PHP application once and keep it in memory, avoiding the overhead of bootstrapping the application on every single HTTP request. This enables sub-millisecond response times.

    Key behaviors to note:

    • Superglobals: Most superglobals ($_GET, $_POST, $_COOKIE, $_FILES, $_SERVER, $_REQUEST) are automatically reset between requests. However, $_ENV is NOT reset. Modifications to $_ENV will persist across requests.
    • State Persistence: Because the process stays alive, static variables, class static properties, global variables, and in-memory caches persist between requests. You must manually reset request-specific state to avoid side effects.
    • Worker Lifecycle: FrankenPHP handles incoming requests by calling a provided handler function. Each call to frankenphp_handle_request() updates the superglobals to reflect the current request.
  6. FrankenPHP Thread Types

    main

    FrankenPHP utilizes three distinct types of threads to handle different execution models:

    Main Thread

    Initializes the PHP runtime, applies php.ini overrides, and takes an environment snapshot (main_thread_env) for sandboxing. It stays alive for the server's lifetime and must signal Ready before other threads start.

    Regular Threads

    Used for classic one-request-per-invocation scripts. The lifecycle follows:

    1. Receives a request via requestChan or regularRequestChan.
    2. Determines the script filename via beforeScriptExecution().
    3. The C layer executes the script.
    4. afterScriptExecution() closes the request context.

    Worker Threads

    Designed to keep a PHP script alive across multiple requests for high performance. The PHP script must call frankenphp_handle_request() in a loop.

    Worker Lifecycle:

    1. beforeScriptExecution() returns the worker script filename.
    2. The C layer starts execution.
    3. The PHP script calls frankenphp_handle_request(), which triggers waitForWorkerRequest() in Go.
    4. Go blocks until a request arrives, sets up the context, and executes the PHP callback.
    5. go_frankenphp_finish_worker_request() cleans up the context.
    6. The script loops back to step 3.

    Restart Behavior: If a worker script exits, it is restarted immediately if it reached frankenphp_handle_request() at least once. Exponential backoff is only applied if the script fails to start entirely (exits before reaching the handler).

  7. Interact with Workers using SendMessage and SendRequest

    main

    Once a worker pool is active, you can dispatch tasks from Go logic or via functions exported to PHP. There are two primary interaction modes:

    Headless Mode: SendMessage

    Use SendMessage to pass raw data directly to the worker script. This is ideal for queue systems or simple command processing. It does not populate standard PHP superglobals like $_GET or $_SERVER.

    HTTP Simulation: SendRequest

    Use SendRequest when the worker script expects a standard web environment. This method populates PHP superglobals ($_SERVER, $_GET, etc.) based on the provided http.Request.

    Note: When using SendMessage, the PHP worker receives the payload as the first argument to the handler function. When using SendRequest, the PHP worker behaves like a standard web request.

    // Example: SendMessage (Headless)
    _, err := worker.SendMessage(
    	context.Background(),
    	unsafe.Pointer(data), // data to pass to worker
    	nil,                 // optional http.ResponseWriter
    )
    
    // Example: SendRequest (HTTP Simulation)
    if err := worker.SendRequest(rr, req); err != nil {
    	return nil
    }
  8. Declare opaque PHP classes in Go

    main

    You can declare Go structs as opaque classes in PHP using the //export_php:class directive.

    Key Characteristics:

    • No direct property access: PHP cannot read or write properties directly (e.g., $user->name fails).
    • Method-only interface: All interactions must occur through methods defined with //export_php:method.
    • Encapsulation: Internal state is controlled entirely by Go code.

    Nullable Parameters: If a PHP parameter is marked as nullable (e.g., ?string $name), the Go function receives a pointer (e.g., *C.zend_string). You must check for nil before dereferencing to handle PHP null values.

    //export_php:class User
    type UserStruct struct {
        Name string
        Age  int
    }
    
    //export_php:method User::getName(): string
    func (us *UserStruct) GetUserName() unsafe.Pointer {
        return frankenphp.PHPString(us.Name, false)
    }
    
    //export_php:method User::updateInfo(?string $name, ?int $age, ?bool $active): void
    func (us *UserStruct) UpdateInfo(name *C.zend_string, age *int64, active *bool) {
        if name != nil {
            us.Name = frankenphp.GoString(unsafe.Pointer(name))
        }
        if age != nil {
            us.Age = int(*age)
        }
        if active != nil {
            us.Active = *active
        }
    }
  9. Work with PHP arrays in Go

    main

    FrankenPHP supports several ways to handle PHP arrays depending on whether you need to preserve order or optimize for performance.

    • Ordered Associative Arrays: Use frankenphp.AssociativeArray which contains a Map map[string]any and an Order []string field. Use frankenphp.GoAssociativeArray to convert from PHP and frankenphp.PHPAssociativeArray to return to PHP.
    • Unordered Maps: Use frankenphp.GoMap to convert a PHP array to a Go map[string]any. Use frankenphp.PHPMap to return a Go map as a PHP array.
    • Packed Arrays (Lists): Use frankenphp.GoPackedArray to convert a PHP indexed array to a Go slice []any. Use frankenphp.PHPPackedArray to return a Go slice as a PHP packed array.

    Helper Methods:

    • frankenphp.IsPacked(zval *C.zend_array) bool: Checks if a PHP array is packed (indexed) or associative.
    // Example: Converting PHP associative array to Go while keeping order
    associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
    
    // Example: Returning an ordered array to PHP
    return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
        Map: map[string]string{"key1": "value1"},
        Order: []string{"key1"},
    })
  10. Use the Mercure Hub endpoint

    main

    Once enabled, the Mercure hub is accessible at the following path:

    /.well-known/mercure

    If you are running FrankenPHP inside a Docker container, the full URL used to send updates will depend on your network setup. For example, if your FrankenPHP container is named php, the URL would be:

    http://php/.well-known/mercure

  11. Compatibility notes for Alpine-based Docker images and static binaries

    main

    FrankenPHP's fully static binaries and Alpine-based Docker images (dunglas/frankenphp:*-alpine) use musl libc instead of glibc.

    Known Limitation: The GLOB_BRACE flag is not available with musl libc (e.g., in the glob() function).

    Recommendation: If you encounter compatibility issues, use the GNU variant of the static binary or Debian-based Docker images.

  12. Handle PHP arrays in Go

    main

    PHP arrays can be converted into several Go representations depending on whether they are associative or packed (indexed).

    Associative Arrays (Ordered)

    Use frankenphp.AssociativeArray to preserve key order.

    • Go to PHP: frankenphp.PHPAssociativeArray(arr frankenphp.AssociativeArray)
    • PHP to Go: frankenphp.GoAssociativeArray(arr unsafe.Pointer, ordered bool)

    Unordered Maps

    For better performance when order doesn't matter:

    • Go to PHP: frankenphp.PHPMap(arr map[string]any)
    • PHP to Go: frankenphp.GoMap(arr unsafe.Pointer)

    Packed Arrays (Indexed)

    For simple lists:

    • Go to PHP: frankenphp.PHPPackedArray(slice []any)
    • PHP to Go: frankenphp.GoPackedArray(arr unsafe.Pointer)

    Use frankenphp.IsPacked(zval *C.zend_array) to check if a PHP array is packed or associative.

    // Example: Converting a PHP associative array to an ordered Go structure
    func process_data_ordered(arr *C.zend_array) unsafe.Pointer {
        associativeArray, err := frankenphp.GoAssociativeArray[any](unsafe.Pointer(arr))
        if err != nil {
            // handle error
        }
    
        for _, key := range associativeArray.Order {
            value, _ := associativeArray.Map[key]
            _ = value
        }
    
        return frankenphp.PHPAssociativeArray[string](frankenphp.AssociativeArray[string]{
            Map: map[string]string{"key1": "value1"},
            Order: []string{"key1"},
        })
    }