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:
ServerContext– per-request state (method, URI, POST body, headers).Sapi– the SAPI itself: output, logging, header sending.- 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");
}
}