bext-php 0.2.0

Embedded PHP runtime for bext — custom SAPI linking libphp via Rust FFI
Documentation
//! Shared memory bridge — bidirectional PHP ↔ JSC communication.
//!
//! ## PHP → JSC (bext_render)
//!
//!   PHP: $html = bext_render("Dashboard", '{"orders":42}')
//!     → C FFI → Rust → JSC pool → HTML → PHP
//!
//! ## JS → PHP (php_call)
//!
//!   JS: const data = bext.php("GET", "/api/products", '{"category":"Books"}')
//!     → Rust → PHP worker pool → JSON → JS
//!
//! Both directions use the same process memory — no HTTP, no sockets.

use std::sync::{Arc, OnceLock};

/// Global reference to the JSC render pool.
/// Set once at server startup, read by all PHP worker threads.
static JSC_POOL: OnceLock<Arc<dyn JscBridge + Send + Sync>> = OnceLock::new();

/// Trait abstracting the JSC render pool so bext-php doesn't depend on bext-core.
pub trait JscBridge: Send + Sync {
    /// Render a full page given props JSON.
    /// Returns HTML on success, error string on failure.
    fn render_page(&self, props_json: String) -> Result<String, String>;

    /// Render a single component.
    fn render_component(&self, component_id: String, props_json: String) -> Result<String, String>;
}

/// Register the JSC pool for PHP threads to use.
/// Called once at server startup after the JSC pool is initialized.
pub fn set_jsc_bridge(bridge: Arc<dyn JscBridge + Send + Sync>) {
    let _ = JSC_POOL.set(bridge);
}

/// Check if a JSC bridge is available.
pub fn has_jsc_bridge() -> bool {
    JSC_POOL.get().is_some()
}

/// Render via the JSC bridge. Called from the C SAPI's `bext_render()` function.
///
/// Returns the rendered HTML, or an error message wrapped in a div.
pub fn jsc_render(props_json: &str) -> String {
    match JSC_POOL.get() {
        Some(pool) => match pool.render_page(props_json.to_string()) {
            Ok(html) => html,
            Err(e) => format!("<div style=\"color:red\">JSC render error: {}</div>", e),
        },
        None => "<div style=\"color:red\">JSC pool not available. \
             Configure [render] bundle_path in bext.config.toml</div>"
            .to_string(),
    }
}

/// Render a named component via the JSC bridge.
pub fn jsc_render_component(component_id: &str, props_json: &str) -> String {
    match JSC_POOL.get() {
        Some(pool) => match pool.render_component(component_id.to_string(), props_json.to_string())
        {
            Ok(html) => html,
            Err(e) => format!("<div style=\"color:red\">JSC component error: {}</div>", e),
        },
        None => "<div style=\"color:red\">JSC pool not available</div>".into(),
    }
}

// ─── JS → PHP bridge ────────────────────────────────────────────────────

/// Global reference to the PHP pool for JS→PHP calls.
static PHP_POOL: OnceLock<Arc<dyn PhpBridge + Send + Sync>> = OnceLock::new();

/// Trait abstracting the PHP pool so JSC/Bun can call PHP.
pub trait PhpBridge: Send + Sync {
    /// Execute a PHP request and return the response body.
    ///
    /// This dispatches to the PHP worker pool, executes the request
    /// through PHP's front controller (or worker handler), and returns
    /// the response body as a string.
    fn call(&self, method: &str, uri: &str, body: Option<&str>) -> Result<PhpCallResult, String>;
}

/// Result of a JS→PHP call.
#[derive(Debug, Clone)]
pub struct PhpCallResult {
    pub status: u16,
    pub body: String,
    pub content_type: String,
}

/// Register the PHP pool for JS threads to use.
pub fn set_php_bridge(bridge: Arc<dyn PhpBridge + Send + Sync>) {
    let _ = PHP_POOL.set(bridge);
}

/// Check if a PHP bridge is available.
pub fn has_php_bridge() -> bool {
    PHP_POOL.get().is_some()
}

/// Call PHP from JS. Returns (status, body, content_type).
///
/// Used by JSC's `globalThis.__bext_php()` function and the NAPI export.
pub fn php_call(method: &str, uri: &str, body: Option<&str>) -> Result<PhpCallResult, String> {
    match PHP_POOL.get() {
        Some(pool) => pool.call(method, uri, body),
        None => Err("PHP pool not available".into()),
    }
}