Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Custom SAPI

ext-php-rs allows you to implement a fully custom SAPI (Server API) in Rust using the Sapi trait. This lets you embed the PHP engine inside your own server, such as an HTTP framework, a message queue consumer, or any other application that needs to drive PHP request processing.

Overview

A SAPI implementation consists of three parts:

  1. ServerContext – per-request state (method, URI, POST body, headers).
  2. Sapi – the SAPI itself: output, logging, header sending.
  3. Trampolines – generated automatically by Sapi::build_module().

Defining a ServerContext

Implement the ServerContext trait for your per-request type:

use ext_php_rs::embed::{ServerContext, RequestInfo};

struct MyContext {
    method: String,
    uri: String,
    body: Vec<u8>,
    body_offset: usize,
    finished: bool,
}

impl ServerContext for MyContext {
    fn init_request_info(&self, info: &mut RequestInfo) {
        info.request_method = Some(self.method.clone());
        info.request_uri = Some(self.uri.clone());
        info.content_length = self.body.len() as i64;
    }

    fn read_post(&mut self, buf: &mut [u8]) -> usize {
        let remaining = &self.body[self.body_offset..];
        let n = buf.len().min(remaining.len());
        buf[..n].copy_from_slice(&remaining[..n]);
        self.body_offset += n;
        n
    }

    fn read_cookies(&self) -> Option<&str> {
        None
    }

    fn finish_request(&mut self) -> bool {
        if self.finished {
            return false;
        }
        self.finished = true;
        true
    }

    fn is_request_finished(&self) -> bool {
        self.finished
    }
}

Implementing the Sapi trait

use ext_php_rs::embed::{Sapi, ServerContext, RequestInfo, ServerVarRegistrar};

struct MyContext { method: String, uri: String, body: Vec<u8>, body_offset: usize, finished: bool }
impl ServerContext for MyContext {
    fn init_request_info(&self, _: &mut RequestInfo) {}
    fn read_post(&mut self, _: &mut [u8]) -> usize { 0 }
    fn read_cookies(&self) -> Option<&str> { None }
    fn finish_request(&mut self) -> bool { true }
    fn is_request_finished(&self) -> bool { true }
}
struct MySapi;

impl Sapi for MySapi {
    type Context = MyContext;

    fn name() -> &'static str { "my-sapi" }
    fn pretty_name() -> &'static str { "My Custom SAPI" }

    fn ub_write(_ctx: &mut MyContext, buf: &[u8]) -> usize {
        // Forward output to your HTTP response
        print!("{}", String::from_utf8_lossy(buf));
        buf.len()
    }

    fn log_message(msg: &str, _syslog_type: i32) {
        eprintln!("[php] {msg}");
    }
}

Building and starting the SAPI

use ext_php_rs::embed::Sapi;
struct MySapi;
impl Sapi for MySapi {
    type Context = ();
    fn name() -> &'static str { "x" }
    fn pretty_name() -> &'static str { "x" }
    fn ub_write(_ctx: &mut (), _buf: &[u8]) -> usize { 0 }
    fn log_message(_: &str, _: i32) {}
}
impl ext_php_rs::embed::ServerContext for () {
    fn init_request_info(&self, _: &mut ext_php_rs::embed::RequestInfo) {}
    fn read_post(&mut self, _: &mut [u8]) -> usize { 0 }
    fn read_cookies(&self) -> Option<&str> { None }
    fn finish_request(&mut self) -> bool { true }
    fn is_request_finished(&self) -> bool { true }
}

let module = MySapi::build_module().expect("failed to build SAPI module");

The returned SapiModule can then be passed to sapi_startup() and php_module_startup() just like a manually-built one.

Registering $_SERVER variables

Override register_server_variables in the Sapi trait to populate $_SERVER:

use ext_php_rs::embed::{Sapi, ServerVarRegistrar};

struct MySapi;
struct Ctx;
impl ext_php_rs::embed::ServerContext for Ctx {
    fn init_request_info(&self, _: &mut ext_php_rs::embed::RequestInfo) {}
    fn read_post(&mut self, _: &mut [u8]) -> usize { 0 }
    fn read_cookies(&self) -> Option<&str> { None }
    fn finish_request(&mut self) -> bool { true }
    fn is_request_finished(&self) -> bool { true }
}
impl Sapi for MySapi {
    type Context = Ctx;
    fn name() -> &'static str { "x" }
    fn pretty_name() -> &'static str { "x" }
    fn ub_write(_: &mut Ctx, b: &[u8]) -> usize { b.len() }
    fn log_message(_: &str, _: i32) {}
    fn register_server_variables(
        _ctx: &mut Ctx,
        registrar: &mut ServerVarRegistrar,
    ) {
        registrar.register("SERVER_SOFTWARE", "my-server/1.0");
        registrar.register("SERVER_PROTOCOL", "HTTP/1.1");
    }
}