Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active February 24, 2026 21:58
Show Gist options
  • Select an option

  • Save masakielastic/a3f85fcaed2701c9a3f9cfb0605a5cfc to your computer and use it in GitHub Desktop.

Select an option

Save masakielastic/a3f85fcaed2701c9a3f9cfb0605a5cfc to your computer and use it in GitHub Desktop.
レスポンスを扱う | PHP 拡張 ext-php-rs

リクエストとレスポンスを扱うハンドラを登録する | PHP 拡張 ext-php-rs

ソースコード

Cargo.toml

[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"] }
hyper = { version = "1", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
bytes = "1.11.1"

src/lib.rs

use std::{cell::RefCell, collections::BTreeMap, convert::Infallible, net::SocketAddr};

use bytes::Bytes;
use ext_php_rs::prelude::*;
use ext_php_rs::types::{ArrayKey, ZendCallable, ZendHashTable};
use ext_php_rs::boxed::ZBox;
use ext_php_rs::types::Zval;

use hyper::server::conn::http1;
use hyper_util::rt::TokioIo;
use hyper_util::service::TowerToHyperService;
use tokio::net::TcpListener;
use warp::Filter;
use warp::Reply;

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())
}

fn call_php_handler(req: ZBox<ZendHashTable>) -> Result<warp::reply::Response, PhpException> {
    PHP_HANDLER.with(|h| {
        let h = h.borrow();
        let handler = h.as_ref().ok_or_else(|| php_err("handler is not set"))?;

        // ZendCallable::try_call は Vec<&dyn IntoZvalDyn> を受ける想定なので参照で渡す
        let resp = handler.try_call(vec![&req]).map_err(php_err)?;

        // 返り値が配列でなければ body 扱い
        let Some(resp_ht) = resp.array() else {
            let body = resp.str().unwrap_or("").to_string();
            return Ok(warp::reply::with_status(body, warp::http::StatusCode::OK).into_response());
        };

        let status = resp_ht.get("status").and_then(|z| z.long()).unwrap_or(200);
        let status = warp::http::StatusCode::from_u16(status as u16)
            .unwrap_or(warp::http::StatusCode::OK);

        let body = resp_ht
            .get("body")
            .and_then(|z| z.str())
            .unwrap_or("")
            .to_string();

        // まず status + body
        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 key = match k {
                    ArrayKey::String(s) => s.to_string(),
                    ArrayKey::Str(s) => s.to_string(),
                    ArrayKey::Long(i) => i.to_string(),
                };
                if let Some(val) = v.str() {
                    if let Ok(name) = warp::http::header::HeaderName::from_bytes(key.as_bytes()) {
                        if let Ok(value) = warp::http::header::HeaderValue::from_str(val) {
                            out.headers_mut().insert(name, value);
                        }
                    }
                }
            }
        }

        Ok(out)
    })
}

async fn serve_forever(
    host: String,
    port: u16,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    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(
            |method: warp::http::Method,
             full_path: warp::path::FullPath,
             query: String,
             headers: warp::http::HeaderMap,
             body: Bytes| async move {
                let internal = |msg: String| {
                    Ok::<warp::reply::Response, Infallible>(
                        warp::reply::with_status(
                            msg,
                            warp::http::StatusCode::INTERNAL_SERVER_ERROR,
                        )
                        .into_response(),
                    )
                };

                // req array
                let mut req = ZendHashTable::new();

                if let Err(e) = req.insert("method", method.as_str()) {
                    return internal(format!("insert method failed: {:?}", e));
                }
                if let Err(e) = req.insert("path", full_path.as_str()) {
                    return internal(format!("insert path failed: {:?}", e));
                }
                if let Err(e) = req.insert("query", query) {
                    return internal(format!("insert query failed: {:?}", e));
                }

                // headers を map で(IntoZval 的に安全)
                let mut hmap = BTreeMap::<String, String>::new();
                for (name, value) in headers.iter() {
                    hmap.insert(
                        name.as_str().to_ascii_lowercase(),
                        value.to_str().unwrap_or("").to_string(),
                    );
                }
                if let Err(e) = req.insert("headers", hmap) {
                    return internal(format!("insert headers failed: {:?}", e));
                }

                // body は生文字列(lossy UTF-8)
                let body_str = String::from_utf8_lossy(&body).to_string();
                if let Err(e) = req.insert("body", body_str) {
                    return internal(format!("insert body failed: {:?}", e));
                }

                match call_php_handler(req) {
                    Ok(resp) => Ok::<warp::reply::Response, Infallible>(resp),
                    Err(e) => internal(format!("PHP handler error: {:?}", e)),
                }
            },
        );

    let tower_svc = warp::service(route);
    let hyper_svc = TowerToHyperService::new(tower_svc);

    let addr: SocketAddr = format!("{host}:{port}").parse()?;
    eprintln!("listening on http://{addr}");

    let listener = TcpListener::bind(addr).await?;

    loop {
        let (stream, _peer) = listener.accept().await?;
        let io = TokioIo::new(stream);

        // 逐次処理:この接続が終わるまで次 accept しない
        if let Err(err) = http1::Builder::new()
            .serve_connection(io, hyper_svc.clone())
            .await
        {
            eprintln!("error serving connection: {err}");
        }
    }
}

#[php_function]
pub fn warp_http_serve(host: String, port: i64, handler: &mut Zval) -> PhpResult<()> {
    if port < 0 || port > 65535 {
        return Err(php_err("port must be in 0..65535"));
    }
    let port = port as u16;

    // handler を owned callable にして thread_local に保存(寿命問題を解消)
    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(php_err)?;

    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

server.php

final class Handler {
    public function __invoke(array $req): array {
        return ["status" => 200,
        "headers" => ["content-type" => "text/plain; charset=utf-8"],
        "body" => "Hello\n"];
    }
}

$h = new Handler();                      // 変数に入れる
warp_http_serve("127.0.0.1", 18080, $h); // OK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment