Skip to content

Instantly share code, notes, and snippets.

@masakielastic
Last active February 27, 2026 13:27
Show Gist options
  • Select an option

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

Select an option

Save masakielastic/a283da52dc2df82143f75f37f1f51b1b to your computer and use it in GitHub Desktop.
ripht_php_sapi と tiny_httpで embed PHP の HTTP サーバー CLI を作成する

ripht_php_sapi と tiny_http で embed PHP の HTTP サーバー CLI を作成する

コード

[package]
name = "mini_php_http"
version = "0.1.0"
edition = "2021"

[dependencies]
tiny_http = "0.12"
ripht-php-sapi = "0.1.0-rc.7"
use ripht_php_sapi::prelude::*;
use std::io::Read;
use tiny_http::{Header, Response, Server, StatusCode};

fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let mut addr = "127.0.0.1:8080".to_string();
    let mut script = std::path::PathBuf::from("index.php");
    let mut docroot: Option<std::path::PathBuf> = None;

    let mut it = std::env::args().skip(1);
    while let Some(a) = it.next() {
        match a.as_str() {
            "--addr" => addr = it.next().expect("--addr needs value"),
            "--script" => script = std::path::PathBuf::from(it.next().expect("--script needs value")),
            "--docroot" => docroot = Some(std::path::PathBuf::from(it.next().expect("--docroot needs value"))),
            "--help" | "-h" => {
                eprintln!("Usage: mini_php_http [--addr 127.0.0.1:8080] --script ./index.php [--docroot ./public]");
                return Ok(());
            }
            _ => return Err(format!("Unknown arg: {a}").into()),
        }
    }

    if !script.exists() {
        return Err(format!("script not found: {}", script.display()).into());
    }

    let php = RiphtSapi::instance();
    let server = Server::http(&addr)?;

    eprintln!("Listening on http://{addr}");
    eprintln!("Script: {}", script.display());

    loop {
        let mut req = match server.recv() {
            Ok(r) => r,
            Err(e) => {
                eprintln!("recv error: {e}");
                continue;
            }
        };

        let method = match req.method().as_str() {
            "GET" => Method::Get,
            "POST" => Method::Post,
            "PUT" => Method::Put,
            "DELETE" => Method::Delete,
            "PATCH" => Method::Patch,
            "HEAD" => Method::Head,
            "OPTIONS" => Method::Options,
            other => {
                let res = Response::from_string(format!("Method not allowed: {other}\n"))
                    .with_status_code(StatusCode(405));
                let _ = req.respond(res);
                continue;
            }
        };

        let uri = req.url().to_string();

        // body(req を消費しない)
        let mut body = Vec::new();
        if req.body_length().unwrap_or(0) > 0 {
            if let Err(e) = req.as_reader().read_to_end(&mut body) {
                eprintln!("body read error: {e}");
            }
        }

        let mut builder = WebRequest::new(method).with_uri(uri);

        if let Some(ct) = req
            .headers()
            .iter()
            .find(|h| h.field.equiv("Content-Type"))
            .map(|h| h.value.as_str().to_string())
        {
            builder = builder.with_content_type(ct);
        }

        for h in req.headers() {
            builder = builder.with_header(h.field.as_str().to_string(), h.value.as_str().to_string());
        }

        if !body.is_empty() {
            builder = builder.with_body(body);
        }

        if let Some(dr) = &docroot {
            builder = builder.with_document_root(dr.clone());
        } else if let Some(parent) = script.parent() {
            builder = builder.with_document_root(parent.to_path_buf());
        }

        let ctx = match builder.build(&script) {
            Ok(c) => c,
            Err(e) => {
                let res = Response::from_string(format!("WebRequest build error: {e}\n"))
                    .with_status_code(StatusCode(500));
                let _ = req.respond(res);
                continue;
            }
        };

        let mut result = match php.execute(ctx) {
            Ok(r) => r,
            Err(e) => {
                let res = Response::from_string(format!("PHP execute error: {e}\n"))
                    .with_status_code(StatusCode(500));
                let _ = req.respond(res);
                continue;
            }
        };

        let status = StatusCode(result.status_code());
        let body = result.take_body();

        let mut resp = Response::from_data(body).with_status_code(status);

        for h in result.all_headers() {
            let name = h.name();
            let value = h.value();
            if let Ok(hh) = Header::from_bytes(name.as_bytes(), value.as_bytes()) {
                resp.add_header(hh);
            }
        }

        let _ = req.respond(resp);
    }
}

index.php

<?php
header("Content-Type: text/plain; charset=utf-8");
echo "Hello from PHP\n";
echo "URI: " . ($_SERVER["REQUEST_URI"] ?? "") . "\n";
echo "Method: " . ($_SERVER["REQUEST_METHOD"] ?? "") . "\n";
echo "Query: " . ($_SERVER["QUERY_STRING"] ?? "") . "\n";

ビルド

cargo run -- --addr 127.0.0.1:8080 --script ./index.php
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment