src/main.rs
[package]
name = "php_warp_server"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
ext-php-rs = "0.15"
warp = { version = "0.4", default-features = false, features = ["server"] }
tokio = { version = "1", features = ["full"] }
# warp::body::bytes() の型として Bytes を使うので残す
bytes = "1.11.1"src/lib.rs
use std::{cell::RefCell, convert::Infallible, net::SocketAddr};
use bytes::Bytes;
use ext_php_rs::prelude::*;
use ext_php_rs::types::{ArrayKey, ZendCallable, ZendHashTable, Zval};
use warp::{Filter, Reply};
use ext_php_rs::boxed::ZBox;
thread_local! {
static PHP_HANDLER: RefCell<Option<ZendCallable<'static>>> = const { RefCell::new(None) };
}
fn php_err<E: std::fmt::Display>(e: E) -> PhpException {
PhpException::default(e.to_string())
}
/// 1) HTTP -> PHP request (array)
fn build_php_request(
method: &warp::http::Method,
full_path: &warp::path::FullPath,
query: String,
headers: warp::http::HeaderMap,
body: Bytes,
) -> Result<ZBox<ZendHashTable>, PhpException> {
let mut req = ZendHashTable::new();
req.insert("method", method.as_str()).map_err(php_err)?;
req.insert("path", full_path.as_str()).map_err(php_err)?;
req.insert("query", query).map_err(php_err)?;
// headers: 連想配列(小文字に正規化)
let mut h = ZendHashTable::new();
for (name, value) in headers.iter() {
let k = name.as_str().to_ascii_lowercase();
let v = value.to_str().unwrap_or("").to_string();
h.insert(k, v).map_err(php_err)?;
}
req.insert("headers", h).map_err(php_err)?;
// body: 教材として string 固定(UTF-8 lossy)
let body_str = String::from_utf8_lossy(&body).to_string();
req.insert("body", body_str).map_err(php_err)?;
Ok(req)
}
/// 2) PHP handler invoke
fn invoke_php_handler(req: &ZBox<ZendHashTable>) -> Result<Zval, PhpException> {
PHP_HANDLER.with(|h| {
let h = h.borrow();
let handler = h.as_ref().ok_or_else(|| php_err("handler is not set"))?;
handler.try_call(vec![req]).map_err(php_err)
})
}
/// PHP response 仕様:
/// - string を返した場合: body 扱い(status=200, headersなし)
/// - array を返した場合:
/// - status: int (省略時 200)
/// - body: string (省略時 "")
/// - headers: array (省略可)
/// - 連想配列: ["content-type" => "text/plain"]
/// - 数値キー配列: ["X-Foo: bar", "X-Baz: qux"]
fn parse_php_response(resp: &Zval) -> Result<warp::reply::Response, PhpException> {
// string なら body として返す(互換維持)
if resp.array().is_none() {
let body = resp.str().unwrap_or("").to_string();
return Ok(warp::reply::with_status(body, warp::http::StatusCode::OK).into_response());
}
let resp_ht = resp
.array()
.ok_or_else(|| php_err("handler returned invalid response"))?;
// status
let status_i = resp_ht.get("status").and_then(|z| z.long()).unwrap_or(200);
let status_u16 = if (100..=999).contains(&status_i) {
status_i as u16
} else {
500
};
let status =
warp::http::StatusCode::from_u16(status_u16).unwrap_or(warp::http::StatusCode::OK);
// body
let body = resp_ht
.get("body")
.and_then(|z| z.str())
.unwrap_or("")
.to_string();
let mut out = warp::reply::with_status(body, status).into_response();
// headers
if let Some(hz) = resp_ht.get("headers").and_then(|z| z.array()) {
for (k, v) in hz.iter() {
let (name, value) = match k {
// 連想配列: key(string) => value(string)
ArrayKey::String(s) => (s.clone(), v.str().unwrap_or("").to_string()),
ArrayKey::Str(s) => (s.to_string(), v.str().unwrap_or("").to_string()),
// 数値キー配列: "Name: value"
ArrayKey::Long(_) => {
let line = v.str().unwrap_or("").to_string();
let Some((n, val)) = line.split_once(':') else { continue; };
(n.trim().to_string(), val.trim().to_string())
}
};
if name.eq_ignore_ascii_case("content-length") {
continue; // 事故防止
}
if let Ok(hname) = warp::http::header::HeaderName::from_bytes(name.as_bytes()) {
if let Ok(hval) = warp::http::header::HeaderValue::from_str(&value) {
out.headers_mut().insert(hname, hval);
}
}
}
}
Ok(out)
}
/// 3) warp route handler: build request -> call PHP -> parse response
async fn handle_request(
method: warp::http::Method,
full_path: warp::path::FullPath,
query: String,
headers: warp::http::HeaderMap,
body: Bytes,
) -> Result<warp::reply::Response, Infallible> {
let internal = |msg: String| {
Ok::<warp::reply::Response, Infallible>(
warp::reply::with_status(msg, warp::http::StatusCode::INTERNAL_SERVER_ERROR)
.into_response(),
)
};
let req = match build_php_request(&method, &full_path, query, headers, body) {
Ok(r) => r,
Err(e) => return internal(format!("build request failed: {e:?}")),
};
let resp_z = match invoke_php_handler(&req) {
Ok(z) => z,
Err(e) => return internal(format!("PHP handler error: {e:?}")),
};
match parse_php_response(&resp_z) {
Ok(r) => Ok::<warp::reply::Response, Infallible>(r),
Err(e) => internal(format!("parse response failed: {e:?}")),
}
}
async fn serve_forever(host: String, port: u16) -> Result<(), PhpException> {
let addr: SocketAddr = format!("{host}:{port}").parse().map_err(php_err)?;
eprintln!("listening on http://{addr}");
let route = warp::any()
.and(warp::method())
.and(warp::path::full())
.and(warp::query::raw().or_else(|_| async {
Ok::<(String,), Infallible>((String::new(),))
}))
.and(warp::header::headers_cloned())
.and(warp::body::bytes())
.and_then(handle_request);
warp::serve(route).run(addr).await;
Ok(())
}
#[php_function]
pub fn warp_http_serve(host: String, port: i64, handler: &mut Zval) -> PhpResult<()> {
if !(0..=65535).contains(&port) {
return Err(php_err("port must be in 0..65535"));
}
let port = port as u16;
let owned_callable = ZendCallable::new_owned(handler.shallow_clone()).map_err(php_err)?;
PHP_HANDLER.with(|h| *h.borrow_mut() = Some(owned_callable));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(php_err)?;
rt.block_on(async move { serve_forever(host, port).await })
.map_err(|e| php_err(format!("{:?}", e)))?;
Ok(())
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.function(wrap_function!(warp_http_serve))
}cargo build
php -d extension=target/debug/libphp_warp_server.so server.php
$f = function(array $req) {
return [
"status" => 200,
"headers" => [
"content-type" => "application/json; charset=utf-8",
],
"body" => json_encode([
"method" => $req["method"],
"path" => $req["path"],
"query" => $req["query"],
"headers" => $req["headers"],
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
];
};
warp_http_serve("127.0.0.1", 8080, $f);