Skip to main content

alef_backend_php/
trait_bridge.rs

1//! PHP (ext-php-rs) specific trait bridge code generation.
2//!
3//! Generates Rust wrapper structs that implement Rust traits by delegating
4//! to PHP objects via ext-php-rs Zval method calls.
5
6use alef_codegen::generators::trait_bridge::{
7    BridgeOutput, TraitBridgeGenerator, TraitBridgeSpec, bridge_param_type as param_type, gen_bridge_all,
8    to_camel_case, visitor_param_type,
9};
10use alef_core::config::TraitBridgeConfig;
11use alef_core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
12use std::collections::HashMap;
13use std::fmt::Write;
14
15/// Find the first parameter index and bridge config where the parameter's named type
16/// matches a trait bridge's `type_alias`.
17///
18/// Returns `None` when no bridge applies.
19pub use alef_codegen::generators::trait_bridge::find_bridge_param;
20
21/// Find the first function parameter whose IR type carries a bridge field
22/// (`bind_via = "options_field"`).  Returns the match metadata needed for
23/// generating the options-field bridge wrapper.
24pub use alef_codegen::generators::trait_bridge::find_bridge_field;
25
26/// Match info for a trait bridge whose handle lives as a struct field.
27pub use alef_codegen::generators::trait_bridge::BridgeFieldMatch;
28
29/// PHP-specific trait bridge generator.
30/// Implements code generation for bridging PHP objects to Rust traits.
31pub struct PhpBridgeGenerator {
32    /// Core crate import path (e.g., `"kreuzberg"`).
33    pub core_import: String,
34    /// Map of type name → fully-qualified Rust path for type references.
35    pub type_paths: HashMap<String, String>,
36    /// Error type name (e.g., `"KreuzbergError"`).
37    pub error_type: String,
38}
39
40impl TraitBridgeGenerator for PhpBridgeGenerator {
41    fn foreign_object_type(&self) -> &str {
42        "*mut ext_php_rs::types::ZendObject"
43    }
44
45    fn bridge_imports(&self) -> Vec<String> {
46        vec!["std::sync::Arc".to_string()]
47    }
48
49    fn gen_sync_method_body(&self, method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
50        let name = &method.name;
51        let mut out = String::with_capacity(512);
52
53        // PHP is single-threaded; just call the method directly.
54        writeln!(
55            out,
56            "// SAFETY: PHP objects are single-threaded; method calls are safe within a request."
57        )
58        .ok();
59
60        let has_args = !method.params.is_empty();
61        let args_expr = if has_args {
62            let mut args_parts = Vec::new();
63            for p in &method.params {
64                let arg_expr = match &p.ty {
65                    TypeRef::String => format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name),
66                    TypeRef::Path => format!(
67                        "ext_php_rs::types::Zval::try_from({}.to_string_lossy().to_string()).unwrap_or_default()",
68                        p.name
69                    ),
70                    TypeRef::Bytes => format!(
71                        "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
72                        p.name
73                    ),
74                    TypeRef::Named(_) => {
75                        format!(
76                            "ext_php_rs::types::Zval::try_from(serde_json::to_string(&{}).unwrap_or_default()).unwrap_or_default()",
77                            p.name
78                        )
79                    }
80                    TypeRef::Primitive(_) => {
81                        format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name)
82                    }
83                    _ => format!(
84                        "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
85                        p.name
86                    ),
87                };
88                args_parts.push(arg_expr);
89            }
90            let args_array = format!("[{}]", args_parts.join(", "));
91            format!(
92                "{}.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()",
93                args_array
94            )
95        } else {
96            "vec![]".to_string()
97        };
98
99        writeln!(
100            out,
101            "let result = unsafe {{ (*self.inner).try_call_method(\"{name}\", {args_expr}) }};"
102        )
103        .ok();
104
105        // Check if method returns a Result type
106        if method.error_type.is_some() {
107            writeln!(out, "match result {{").ok();
108            // Check if return type is unit ()
109            if matches!(method.return_type, TypeRef::Unit) {
110                writeln!(out, "    Ok(_) => Ok(()),").ok();
111            } else {
112                // For Result-returning methods, the PHP value should be deserialized from JSON
113                writeln!(out, "    Ok(val) => {{").ok();
114                writeln!(out, "        let json_str = val.string().unwrap_or_default();").ok();
115                writeln!(out, "        serde_json::from_str(&json_str).map_err(|e| {}::KreuzbergError::Other(format!(\"Deserialize error: {{}}\", e)))", self.core_import).ok();
116                writeln!(out, "    }}").ok();
117            }
118            // Error conversion: ext-php-rs::error::Error → KreuzbergError
119            // Use string conversion since there's no direct From impl
120            writeln!(
121                out,
122                "    Err(e) => Err({}::KreuzbergError::Other(e.to_string())),",
123                self.core_import
124            )
125            .ok();
126            writeln!(out, "}}").ok();
127        } else {
128            writeln!(out, "match result {{").ok();
129            // Check if return type is unit ()
130            if matches!(method.return_type, TypeRef::Unit) {
131                writeln!(out, "    Ok(_) => (),").ok();
132            } else {
133                // For non-Result methods, try to deserialize from JSON
134                // (safer than trying to parse arbitrary types)
135                writeln!(out, "    Ok(val) => {{").ok();
136                writeln!(out, "        let json_str = val.string().unwrap_or_default();").ok();
137                writeln!(out, "        serde_json::from_str(&json_str).unwrap_or_default()").ok();
138                writeln!(out, "    }}").ok();
139            }
140            writeln!(out, "    Err(_) => Default::default(),").ok();
141            writeln!(out, "}}").ok();
142        }
143
144        out
145    }
146
147    fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
148        let name = &method.name;
149        let mut out = String::with_capacity(1024);
150
151        writeln!(out, "let inner_obj = self.inner;").ok();
152        writeln!(out, "let cached_name = self.cached_name.clone();").ok();
153
154        // Clone params for the blocking closure (String types need explicit clone)
155        for p in &method.params {
156            if matches!(&p.ty, TypeRef::String) {
157                writeln!(out, "let {} = {}.clone();", p.name, p.name).ok();
158            }
159        }
160
161        writeln!(out).ok();
162        writeln!(out, "// SAFETY: PHP objects are single-threaded within a request.").ok();
163        writeln!(out, "// The block_on executes within the async runtime.").ok();
164        writeln!(out, "WORKER_RUNTIME.block_on(async {{").ok();
165
166        let has_args = !method.params.is_empty();
167        let args_expr = if has_args {
168            let mut args_parts = Vec::new();
169            for p in &method.params {
170                let arg_expr = match &p.ty {
171                    TypeRef::String => format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name),
172                    TypeRef::Path => format!(
173                        "ext_php_rs::types::Zval::try_from({}.to_string_lossy().to_string()).unwrap_or_default()",
174                        p.name
175                    ),
176                    TypeRef::Bytes => format!(
177                        "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
178                        p.name
179                    ),
180                    TypeRef::Named(_) => {
181                        format!(
182                            "ext_php_rs::types::Zval::try_from(serde_json::to_string(&{}).unwrap_or_default()).unwrap_or_default()",
183                            p.name
184                        )
185                    }
186                    TypeRef::Primitive(_) => {
187                        format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name)
188                    }
189                    _ => format!(
190                        "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
191                        p.name
192                    ),
193                };
194                args_parts.push(arg_expr);
195            }
196            let args_array = format!("[{}]", args_parts.join(", "));
197            format!(
198                "{}.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()",
199                args_array
200            )
201        } else {
202            "vec![]".to_string()
203        };
204
205        // inner_obj is *mut ZendObject, so we need to dereference and call the method.
206        // SAFETY: the raw pointer is valid within the async block.
207        writeln!(
208            out,
209            "    match unsafe {{ (*inner_obj).try_call_method(\"{name}\", {args_expr}) }} {{"
210        )
211        .ok();
212
213        // For Result-returning methods, deserialize JSON. For non-Result, parse directly.
214        if method.error_type.is_some() {
215            writeln!(out, "        Ok(val) => {{").ok();
216            writeln!(out, "            let json_str = val.string().unwrap_or_default();").ok();
217            writeln!(
218                out,
219                "            serde_json::from_str(&json_str).map_err(|e| {}::KreuzbergError::Other(format!(\"Deserialize error: {{}}\", e)))",
220                spec.core_import
221            )
222            .ok();
223            writeln!(out, "        }}").ok();
224        } else {
225            writeln!(
226                out,
227                "        Ok(val) => val.string().unwrap_or_default().parse().unwrap_or_default(),"
228            )
229            .ok();
230        }
231
232        writeln!(
233            out,
234            "        Err(e) => Err({}::KreuzbergError::Plugin {{",
235            spec.core_import
236        )
237        .ok();
238        writeln!(
239            out,
240            "            message: format!(\"Plugin '{{}}' method '{name}' failed: {{}}\", cached_name, e),"
241        )
242        .ok();
243        writeln!(out, "            plugin_name: cached_name.clone(),").ok();
244        writeln!(out, "        }}),").ok();
245        writeln!(out, "    }}").ok();
246        writeln!(out, "}})",).ok();
247
248        out
249    }
250
251    fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
252        let wrapper = spec.wrapper_name();
253        let mut out = String::with_capacity(512);
254
255        writeln!(out, "impl {wrapper} {{").ok();
256        writeln!(out, "    /// Create a new bridge wrapping a PHP object.").ok();
257        writeln!(out, "    ///").ok();
258        writeln!(
259            out,
260            "    /// Validates that the PHP object provides all required methods."
261        )
262        .ok();
263        writeln!(
264            out,
265            "    pub fn new(php_obj: &mut ext_php_rs::types::ZendObject) -> Self {{"
266        )
267        .ok();
268
269        // Validation of required methods is done in the registration function below.
270        // Skipping debug_assert in constructor to avoid type issues with get_property.
271
272        // Extract and cache name
273        writeln!(out, "        let cached_name = php_obj").ok();
274        writeln!(out, "            .try_call_method(\"name\", vec![])").ok();
275        writeln!(out, "            .ok()").ok();
276        writeln!(out, "            .and_then(|v| v.string())").ok();
277        writeln!(out, "            .unwrap_or(\"unknown\".into())").ok();
278        writeln!(out, "            .to_string();").ok();
279
280        writeln!(out).ok();
281        writeln!(out, "        Self {{").ok();
282        writeln!(out, "            inner: php_obj as *mut _,").ok();
283        writeln!(out, "            cached_name,").ok();
284        writeln!(out, "        }}").ok();
285        writeln!(out, "    }}").ok();
286        writeln!(out, "}}").ok();
287
288        // SAFETY: PHP objects are single-threaded within a request.
289        // The raw pointer is only valid for the duration of the PHP call stack,
290        // and is never accessed concurrently or from multiple threads.
291        writeln!(out).ok();
292        writeln!(out, "// SAFETY: PHP is single-threaded within a request context.").ok();
293        writeln!(
294            out,
295            "// The raw pointer to ZendObject is only used within a single PHP request"
296        )
297        .ok();
298        writeln!(out, "// and is never accessed concurrently from multiple threads.").ok();
299        writeln!(out, "unsafe impl Send for {wrapper} {{}}").ok();
300        writeln!(out, "unsafe impl Sync for {wrapper} {{}}").ok();
301
302        out
303    }
304
305    fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
306        let Some(register_fn) = spec.bridge_config.register_fn.as_deref() else {
307            return String::new();
308        };
309        let Some(registry_getter) = spec.bridge_config.registry_getter.as_deref() else {
310            return String::new();
311        };
312        let wrapper = spec.wrapper_name();
313        let trait_path = spec.trait_path();
314
315        let mut out = String::with_capacity(1024);
316
317        writeln!(out, "#[php_function]").ok();
318        writeln!(
319            out,
320            "pub fn {register_fn}(backend: &mut ext_php_rs::types::ZendObject) -> ext_php_rs::prelude::PhpResult<()> {{"
321        )
322        .ok();
323
324        // Validate required methods
325        let req_methods: Vec<&MethodDef> = spec.required_methods();
326        if !req_methods.is_empty() {
327            for method in &req_methods {
328                // Check if required method exists by attempting to call it with empty args.
329                writeln!(
330                    out,
331                    r#"    if backend.try_call_method("{}", vec![]).is_err() {{"#,
332                    method.name
333                )
334                .ok();
335                writeln!(out, "        return Err(ext_php_rs::exception::PhpException::default(").ok();
336                writeln!(
337                    out,
338                    "            format!(\"Backend missing required method: {{}}\", \"{}\")",
339                    method.name
340                )
341                .ok();
342                writeln!(out, "        ));").ok();
343                writeln!(out, "    }}").ok();
344            }
345        }
346
347        writeln!(out).ok();
348        writeln!(out, "    let wrapper = {wrapper}::new(backend);").ok();
349        writeln!(
350            out,
351            "    let arc: std::sync::Arc<dyn {trait_path}> = std::sync::Arc::new(wrapper);"
352        )
353        .ok();
354        writeln!(out).ok();
355
356        let extra = spec
357            .bridge_config
358            .register_extra_args
359            .as_deref()
360            .map(|a| format!(", {a}"))
361            .unwrap_or_default();
362        writeln!(out, "    let registry = {registry_getter}();").ok();
363        writeln!(out, "    let mut registry = registry.write();").ok();
364        writeln!(
365            out,
366            "    registry.register(arc{extra}).map_err(|e| ext_php_rs::exception::PhpException::default("
367        )
368        .ok();
369        writeln!(out, "        format!(\"Failed to register backend: {{}}\", e)").ok();
370        writeln!(out, "    ))?;").ok();
371        writeln!(out, "    Ok(())").ok();
372        writeln!(out, "}}").ok();
373
374        out
375    }
376}
377
378/// Generate all trait bridge code for a given trait type and bridge config.
379pub fn gen_trait_bridge(
380    trait_type: &TypeDef,
381    bridge_cfg: &TraitBridgeConfig,
382    core_import: &str,
383    error_type: &str,
384    error_constructor: &str,
385    api: &ApiSurface,
386) -> BridgeOutput {
387    // Build type name → rust_path lookup as owned HashMap
388    let type_paths: HashMap<String, String> = api
389        .types
390        .iter()
391        .map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
392        .chain(
393            api.enums
394                .iter()
395                .map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
396        )
397        .collect();
398
399    // Visitor-style bridge: all methods have defaults, no registry, no super-trait.
400    let is_visitor_bridge = bridge_cfg.type_alias.is_some()
401        && bridge_cfg.register_fn.is_none()
402        && bridge_cfg.super_trait.is_none()
403        && trait_type.methods.iter().all(|m| m.has_default_impl);
404
405    if is_visitor_bridge {
406        let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
407        let trait_path = trait_type.rust_path.replace('-', "_");
408        let code = gen_visitor_bridge(trait_type, bridge_cfg, &struct_name, &trait_path, &type_paths);
409        BridgeOutput { imports: vec![], code }
410    } else {
411        // Use the IR-driven TraitBridgeGenerator infrastructure
412        let generator = PhpBridgeGenerator {
413            core_import: core_import.to_string(),
414            type_paths: type_paths.clone(),
415            error_type: error_type.to_string(),
416        };
417        let spec = TraitBridgeSpec {
418            trait_def: trait_type,
419            bridge_config: bridge_cfg,
420            core_import,
421            wrapper_prefix: "Php",
422            type_paths,
423            error_type: error_type.to_string(),
424            error_constructor: error_constructor.to_string(),
425        };
426        gen_bridge_all(&spec, &generator)
427    }
428}
429
430/// Generate a visitor-style bridge wrapping a PHP `Zval` object reference.
431///
432/// Every trait method checks if the PHP object has a matching camelCase method,
433/// then calls it and maps the PHP return value to `VisitResult`.
434fn gen_visitor_bridge(
435    trait_type: &TypeDef,
436    _bridge_cfg: &TraitBridgeConfig,
437    struct_name: &str,
438    trait_path: &str,
439    type_paths: &HashMap<String, String>,
440) -> String {
441    let mut out = String::with_capacity(4096);
442    let core_crate = trait_path
443        .split("::")
444        .next()
445        .unwrap_or("html_to_markdown_rs")
446        .to_string();
447    // Helper: convert NodeContext to a PHP array (Zval)
448    writeln!(out, "fn nodecontext_to_php_array(").unwrap();
449    writeln!(out, "    ctx: &{core_crate}::visitor::NodeContext,").unwrap();
450    writeln!(out, ") -> ext_php_rs::boxed::ZBox<ext_php_rs::types::ZendHashTable> {{").unwrap();
451    writeln!(out, "    let mut arr = ext_php_rs::types::ZendHashTable::new();").unwrap();
452    writeln!(
453        out,
454        "    arr.insert(\"nodeType\", ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", ctx.node_type)).unwrap_or_default()).ok();"
455    )
456    .unwrap();
457    writeln!(
458        out,
459        "    arr.insert(\"tagName\", ext_php_rs::types::Zval::try_from(ctx.tag_name.clone()).unwrap_or_default()).ok();"
460    )
461    .unwrap();
462    writeln!(
463        out,
464        "    arr.insert(\"depth\", ext_php_rs::types::Zval::try_from(ctx.depth as i64).unwrap_or_default()).ok();"
465    )
466    .unwrap();
467    writeln!(
468        out,
469        "    arr.insert(\"indexInParent\", ext_php_rs::types::Zval::try_from(ctx.index_in_parent as i64).unwrap_or_default()).ok();"
470    )
471    .unwrap();
472    writeln!(
473        out,
474        "    arr.insert(\"isInline\", ext_php_rs::types::Zval::try_from(ctx.is_inline).unwrap_or_default()).ok();"
475    )
476    .unwrap();
477    writeln!(out, "    if let Some(ref pt) = ctx.parent_tag {{").unwrap();
478    writeln!(
479        out,
480        "        arr.insert(\"parentTag\", ext_php_rs::types::Zval::try_from(pt.clone()).unwrap_or_default()).ok();"
481    )
482    .unwrap();
483    writeln!(out, "    }}").unwrap();
484    writeln!(out, "    let mut attrs = ext_php_rs::types::ZendHashTable::new();").unwrap();
485    writeln!(out, "    for (k, v) in &ctx.attributes {{").unwrap();
486    writeln!(
487        out,
488        "        attrs.insert(k.as_str(), ext_php_rs::types::Zval::try_from(v.clone()).unwrap_or_default()).ok();"
489    )
490    .unwrap();
491    writeln!(out, "    }}").unwrap();
492    writeln!(out, "    let mut attrs_zval = ext_php_rs::types::Zval::new();").unwrap();
493    writeln!(out, "    attrs_zval.set_hashtable(attrs);").unwrap();
494    writeln!(out, "    arr.insert(\"attributes\", attrs_zval).ok();").unwrap();
495    writeln!(out, "    arr").unwrap();
496    writeln!(out, "}}").unwrap();
497    writeln!(out).unwrap();
498
499    // Bridge struct — stores a reference to the PHP object.
500    // The reference is valid for the duration of the PHP function call that
501    // created the bridge, which spans the entire Rust trait method dispatch.
502    writeln!(out, "pub struct {struct_name} {{").unwrap();
503    writeln!(out, "    php_obj: *mut ext_php_rs::types::ZendObject,").unwrap();
504    writeln!(out, "    cached_name: String,").unwrap();
505    writeln!(out, "}}").unwrap();
506    writeln!(out).unwrap();
507
508    // SAFETY: The raw pointer is only used while the PHP call stack frame is
509    // alive. The bridge is consumed before the PHP function returns.
510    writeln!(out, "// SAFETY: PHP objects are single-threaded; the bridge is used").unwrap();
511    writeln!(out, "// only within a single PHP request, never across threads.").unwrap();
512    writeln!(out, "unsafe impl Send for {struct_name} {{}}").unwrap();
513    writeln!(out, "unsafe impl Sync for {struct_name} {{}}").unwrap();
514    writeln!(out).unwrap();
515
516    writeln!(out, "impl Clone for {struct_name} {{").unwrap();
517    writeln!(out, "    fn clone(&self) -> Self {{").unwrap();
518    writeln!(out, "        Self {{").unwrap();
519    writeln!(out, "            php_obj: self.php_obj,").unwrap();
520    writeln!(out, "            cached_name: self.cached_name.clone(),").unwrap();
521    writeln!(out, "        }}").unwrap();
522    writeln!(out, "    }}").unwrap();
523    writeln!(out, "}}").unwrap();
524    writeln!(out).unwrap();
525
526    // Manual Debug impl
527    writeln!(out, "impl std::fmt::Debug for {struct_name} {{").unwrap();
528    writeln!(
529        out,
530        "    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{"
531    )
532    .unwrap();
533    writeln!(out, "        write!(f, \"{struct_name}\")").unwrap();
534    writeln!(out, "    }}").unwrap();
535    writeln!(out, "}}").unwrap();
536    writeln!(out).unwrap();
537
538    // Constructor takes &mut ZendObject, which is what ext-php-rs exposes via
539    // FromZvalMut. We store the raw pointer; the caller guarantees the object
540    // outlives this bridge.
541    writeln!(out, "impl {struct_name} {{").unwrap();
542    writeln!(
543        out,
544        "    pub fn new(php_obj: &mut ext_php_rs::types::ZendObject) -> Self {{"
545    )
546    .unwrap();
547    writeln!(out, "        let cached_name = php_obj").unwrap();
548    writeln!(out, "            .try_call_method(\"name\", vec![])").unwrap();
549    writeln!(out, "            .ok()").unwrap();
550    writeln!(out, "            .and_then(|v| v.string())").unwrap();
551    writeln!(out, "            .unwrap_or(\"unknown\".into())").unwrap();
552    writeln!(out, "            .to_string();").unwrap();
553    writeln!(out, "        Self {{ php_obj: php_obj as *mut _, cached_name }}").unwrap();
554    writeln!(out, "    }}").unwrap();
555    writeln!(out, "}}").unwrap();
556    writeln!(out).unwrap();
557
558    // Trait impl
559    writeln!(out, "impl {trait_path} for {struct_name} {{").unwrap();
560    for method in &trait_type.methods {
561        if method.trait_source.is_some() {
562            continue;
563        }
564        gen_visitor_method_php(&mut out, method, type_paths);
565    }
566    writeln!(out, "}}").unwrap();
567    writeln!(out).unwrap();
568
569    out
570}
571
572/// Generate a single visitor method that checks for a camelCase PHP method and calls it.
573fn gen_visitor_method_php(out: &mut String, method: &MethodDef, type_paths: &HashMap<String, String>) {
574    let name = &method.name;
575    let php_name = to_camel_case(name);
576
577    let mut sig_parts = vec!["&mut self".to_string()];
578    for p in &method.params {
579        let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
580        sig_parts.push(format!("{}: {}", p.name, ty_str));
581    }
582    let sig = sig_parts.join(", ");
583
584    let ret_ty = match &method.return_type {
585        TypeRef::Named(n) => type_paths.get(n).cloned().unwrap_or_else(|| n.clone()),
586        other => param_type(other, "", false, type_paths),
587    };
588
589    writeln!(out, "    fn {name}({sig}) -> {ret_ty} {{").unwrap();
590
591    // SAFETY: php_obj pointer is valid for the lifetime of the PHP call frame.
592    writeln!(
593        out,
594        "        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call."
595    )
596    .unwrap();
597    writeln!(out, "        let php_obj_ref = unsafe {{ &mut *self.php_obj }};").unwrap();
598
599    // Build args array
600    let has_args = !method.params.is_empty();
601    if has_args {
602        writeln!(out, "        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();").unwrap();
603        for p in &method.params {
604            if let TypeRef::Named(n) = &p.ty {
605                if n == "NodeContext" {
606                    writeln!(
607                        out,
608                        "        let ctx_arr = nodecontext_to_php_array({}{});",
609                        if p.is_ref { "" } else { "&" },
610                        p.name
611                    )
612                    .unwrap();
613                    writeln!(
614                        out,
615                        "        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());"
616                    )
617                    .unwrap();
618                    continue;
619                }
620            }
621            // Check optional string ref BEFORE non-optional string, since visitor_param_type
622            // returns Option<&str> for optional string ref params.
623            if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
624                writeln!(
625                    out,
626                    "        args.push(match {0} {{ Some(s) => ext_php_rs::types::Zval::try_from(s.to_string()).unwrap_or_default(), None => ext_php_rs::types::Zval::new() }});",
627                    p.name
628                )
629                .unwrap();
630                continue;
631            }
632            if matches!(&p.ty, TypeRef::String) {
633                if p.is_ref {
634                    writeln!(
635                        out,
636                        "        args.push(ext_php_rs::types::Zval::try_from({}.to_string()).unwrap_or_default());",
637                        p.name
638                    )
639                    .unwrap();
640                } else {
641                    writeln!(
642                        out,
643                        "        args.push(ext_php_rs::types::Zval::try_from({}.clone()).unwrap_or_default());",
644                        p.name
645                    )
646                    .unwrap();
647                }
648                continue;
649            }
650            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
651                writeln!(
652                    out,
653                    "        {{ let mut _zv = ext_php_rs::types::Zval::new(); _zv.set_bool({}); args.push(_zv); }}",
654                    p.name
655                )
656                .unwrap();
657                continue;
658            }
659            // Default: format as string
660            writeln!(
661                out,
662                "        args.push(ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default());",
663                p.name
664            )
665            .unwrap();
666        }
667    }
668
669    // Call the PHP method via try_call_method which takes Vec<&dyn IntoZvalDyn>.
670    // If the method does not exist, try_call_method returns Err(Error::Callable),
671    // which we treat as a "no-op, return Continue" (same as the default impl).
672    if has_args {
673        writeln!(
674            out,
675            "        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect();"
676        )
677        .unwrap();
678    }
679    let args_expr = if has_args { "dyn_args" } else { "vec![]" };
680    writeln!(
681        out,
682        "        let result = php_obj_ref.try_call_method(\"{php_name}\", {args_expr});"
683    )
684    .unwrap();
685
686    // Parse result — try_call_method returns Result<Zval> (not Result<Option<Zval>>)
687    writeln!(out, "        match result {{").unwrap();
688    writeln!(out, "            Err(_) => {ret_ty}::Continue,").unwrap();
689    writeln!(out, "            Ok(val) => {{").unwrap();
690    writeln!(
691        out,
692        "                let s = val.string().unwrap_or_default().to_lowercase();"
693    )
694    .unwrap();
695    writeln!(out, "                match s.as_str() {{").unwrap();
696    writeln!(out, "                    \"continue\" => {ret_ty}::Continue,").unwrap();
697    writeln!(out, "                    \"skip\" => {ret_ty}::Skip,").unwrap();
698    writeln!(
699        out,
700        "                    \"preserve_html\" | \"preservehtml\" => {ret_ty}::PreserveHtml,"
701    )
702    .unwrap();
703    writeln!(out, "                    other => {ret_ty}::Custom(other.to_string()),").unwrap();
704    writeln!(out, "                }}").unwrap();
705    writeln!(out, "            }}").unwrap();
706    writeln!(out, "        }}").unwrap();
707    writeln!(out, "    }}").unwrap();
708    writeln!(out).unwrap();
709}
710
711/// Generate a PHP static method for an options-field bridge.
712///
713/// When `bind_via = "options_field"`, the visitor lives on the options struct
714/// (`options.visitor`).  ext-php-rs cannot read a raw-pointer field from a
715/// PHP class struct, so the generated Rust function takes the visitor as an
716/// *extra* parameter (`{field_name}_obj: Option<&mut ZendObject>`).  The PHP
717/// facade is responsible for passing `$options->{field_name}` as that extra arg.
718#[allow(clippy::too_many_arguments)]
719pub fn gen_bridge_field_function(
720    func: &alef_core::ir::FunctionDef,
721    field_match: &BridgeFieldMatch<'_>,
722    mapper: &dyn alef_codegen::type_mapper::TypeMapper,
723    opaque_types: &ahash::AHashSet<String>,
724    core_import: &str,
725) -> String {
726    use alef_core::ir::TypeRef;
727
728    let struct_name = format!("Php{}Bridge", field_match.bridge.trait_name);
729    let handle_path = format!("{core_import}::visitor::VisitorHandle");
730    let field_name = &field_match.field_name;
731    let extra_param = format!("{field_name}_obj");
732
733    // Build parameter list (all original params, then the extra visitor obj).
734    let mut sig_parts = Vec::new();
735    let err_conv = ".map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))";
736
737    for p in func.params.iter() {
738        let base = mapper.map_type(&p.ty);
739        let ty = match &p.ty {
740            TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
741                if p.optional {
742                    format!("Option<&mut {base}>")
743                } else {
744                    format!("&mut {base}")
745                }
746            }
747            TypeRef::Optional(inner) => {
748                if let TypeRef::Named(n) = inner.as_ref() {
749                    if !opaque_types.contains(n.as_str()) {
750                        format!("Option<&mut {base}>")
751                    } else if p.optional {
752                        format!("Option<{base}>")
753                    } else {
754                        base
755                    }
756                } else if p.optional {
757                    format!("Option<{base}>")
758                } else {
759                    base
760                }
761            }
762            _ => {
763                if p.optional {
764                    format!("Option<{base}>")
765                } else {
766                    base
767                }
768            }
769        };
770        sig_parts.push(format!("{}: {}", p.name, ty));
771    }
772    // Extra hidden param: the visitor object from $options->visitor.
773    sig_parts.push(format!("{extra_param}: Option<&mut ext_php_rs::types::ZendObject>"));
774
775    let params_str = sig_parts.join(", ");
776    let return_type = mapper.map_type(&func.return_type);
777    let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
778
779    // Bridge wrapping from the extra param.
780    let bridge_wrap = format!(
781        "let {extra_param} = {extra_param}.map(|v| {{\n        \
782         let bridge = {struct_name}::new(v);\n        \
783         std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n    \
784         }});"
785    );
786
787    // Serde round-trip for Named (non-opaque) options param so we get the core type.
788    let opts_param_name = &func.params[field_match.param_index].name;
789    let opts_is_optional = field_match.param_is_optional;
790
791    let serde_bindings: String = func
792        .params
793        .iter()
794        .filter(|p| {
795            let named = match &p.ty {
796                TypeRef::Named(n) => Some(n.as_str()),
797                TypeRef::Optional(inner) => {
798                    if let TypeRef::Named(n) = inner.as_ref() {
799                        Some(n.as_str())
800                    } else {
801                        None
802                    }
803                }
804                _ => None,
805            };
806            named.is_some_and(|n| !opaque_types.contains(n))
807        })
808        .map(|p| {
809            let name = &p.name;
810            let core_type_name = match &p.ty {
811                TypeRef::Named(n) => n.clone(),
812                TypeRef::Optional(inner) => {
813                    if let TypeRef::Named(n) = inner.as_ref() {
814                        n.clone()
815                    } else {
816                        String::new()
817                    }
818                }
819                _ => String::new(),
820            };
821            let core_path = format!("{core_import}::{core_type_name}");
822            if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
823                // Use `let mut` so visitor_attach can call `.as_mut()` on the Option.
824                format!(
825                    "let mut {name}_core: Option<{core_path}> = {name}.map(|v| {{\n        \
826                     let json = serde_json::to_string(&v){err_conv}?;\n        \
827                     serde_json::from_str(&json){err_conv}\n    \
828                     }}).transpose()?;\n    "
829                )
830            } else {
831                format!(
832                    "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n    \
833                     let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n    "
834                )
835            }
836        })
837        .collect();
838
839    // Attach the visitor to the options core struct.
840    let visitor_attach = if opts_is_optional {
841        format!(
842            "if let (Some(ref mut opts), Some(v)) = ({opts_param_name}_core.as_mut(), {extra_param}) {{\n        \
843             opts.{field_name} = Some(v);\n    \
844             }}\n    "
845        )
846    } else {
847        format!(
848            "if let Some(v) = {extra_param} {{\n        \
849             {opts_param_name}_core.{field_name} = Some(v);\n    \
850             }}\n    "
851        )
852    };
853
854    // Build call args.
855    let call_args: Vec<String> = func
856        .params
857        .iter()
858        .map(|p| match &p.ty {
859            TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
860                if p.optional {
861                    format!("{}.as_ref().map(|v| &v.inner)", p.name)
862                } else {
863                    format!("&{}.inner", p.name)
864                }
865            }
866            TypeRef::Named(_) => format!("{}_core", p.name),
867            TypeRef::Optional(inner) => {
868                if let TypeRef::Named(n) = inner.as_ref() {
869                    if opaque_types.contains(n.as_str()) {
870                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
871                    } else {
872                        format!("{}_core", p.name)
873                    }
874                } else {
875                    p.name.clone()
876                }
877            }
878            TypeRef::String | TypeRef::Char => {
879                if p.is_ref {
880                    format!("&{}", p.name)
881                } else {
882                    p.name.clone()
883                }
884            }
885            _ => p.name.clone(),
886        })
887        .collect();
888    let call_args_str = call_args.join(", ");
889
890    let core_fn_path = {
891        let path = func.rust_path.replace('-', "_");
892        if path.starts_with(core_import) {
893            path
894        } else {
895            format!("{core_import}::{}", func.name)
896        }
897    };
898    let core_call = format!("{core_fn_path}({call_args_str})");
899
900    let return_wrap = match &func.return_type {
901        TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
902            format!("{name} {{ inner: std::sync::Arc::new(val) }}")
903        }
904        TypeRef::Named(_) => "val.into()".to_string(),
905        TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
906        _ => "val".to_string(),
907    };
908
909    let body = if func.error_type.is_some() {
910        if return_wrap == "val" {
911            format!("{bridge_wrap}\n    {serde_bindings}{visitor_attach}{core_call}{err_conv}")
912        } else {
913            format!("{bridge_wrap}\n    {serde_bindings}{visitor_attach}{core_call}.map(|val| {return_wrap}){err_conv}")
914        }
915    } else {
916        format!("{bridge_wrap}\n    {serde_bindings}{visitor_attach}{core_call}")
917    };
918
919    let func_name = &func.name;
920    let mut out = String::with_capacity(1024);
921    if func.error_type.is_some() {
922        out.push_str("#[allow(clippy::missing_errors_doc)]\n");
923    }
924    out.push_str(&format!("pub fn {func_name}({params_str}) -> {ret} {{\n"));
925    out.push_str(&format!("    {body}\n"));
926    out.push_str("}\n");
927
928    out
929}
930
931/// Generate a PHP static method that has one parameter replaced by
932/// `Option<ext_php_rs::boxed::ZBox<ext_php_rs::types::ZendObject>>` (a trait bridge).
933#[allow(clippy::too_many_arguments)]
934pub fn gen_bridge_function(
935    func: &alef_core::ir::FunctionDef,
936    bridge_param_idx: usize,
937    bridge_cfg: &TraitBridgeConfig,
938    mapper: &dyn alef_codegen::type_mapper::TypeMapper,
939    opaque_types: &ahash::AHashSet<String>,
940    core_import: &str,
941) -> String {
942    use alef_core::ir::TypeRef;
943
944    let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
945    let handle_path = format!("{core_import}::visitor::VisitorHandle");
946    let param_name = &func.params[bridge_param_idx].name;
947    let bridge_param = &func.params[bridge_param_idx];
948    let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
949
950    // Build parameter list, hiding bridge params from signature
951    let mut sig_parts = Vec::new();
952    for (idx, p) in func.params.iter().enumerate() {
953        if idx == bridge_param_idx {
954            // Bridge param: &mut ZendObject implements FromZvalMut in ext-php-rs 0.15,
955            // allowing PHP to pass any object. ZBox<ZendObject> does NOT implement
956            // FromZvalMut, so we must use the reference form here.
957            let php_obj_ty = "&mut ext_php_rs::types::ZendObject";
958            if is_optional {
959                sig_parts.push(format!("{}: Option<{php_obj_ty}>", p.name));
960            } else {
961                sig_parts.push(format!("{}: {php_obj_ty}", p.name));
962            }
963        } else {
964            let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
965            let base = mapper.map_type(&p.ty);
966            // #[php_class] types (non-opaque Named) only implement FromZvalMut for &mut T,
967            // not for owned T — so we must use &mut T in the function signature.
968            let ty = match &p.ty {
969                TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
970                    if p.optional || promoted {
971                        format!("Option<&mut {base}>")
972                    } else {
973                        format!("&mut {base}")
974                    }
975                }
976                TypeRef::Optional(inner) => {
977                    if let TypeRef::Named(n) = inner.as_ref() {
978                        if !opaque_types.contains(n.as_str()) {
979                            format!("Option<&mut {base}>")
980                        } else if p.optional || promoted {
981                            format!("Option<{base}>")
982                        } else {
983                            base
984                        }
985                    } else if p.optional || promoted {
986                        format!("Option<{base}>")
987                    } else {
988                        base
989                    }
990                }
991                _ => {
992                    if p.optional || promoted {
993                        format!("Option<{base}>")
994                    } else {
995                        base
996                    }
997                }
998            };
999            sig_parts.push(format!("{}: {}", p.name, ty));
1000        }
1001    }
1002
1003    let params_str = sig_parts.join(", ");
1004    let return_type = mapper.map_type(&func.return_type);
1005    let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
1006
1007    let err_conv = ".map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))";
1008
1009    // Bridge wrapping code
1010    let bridge_wrap = if is_optional {
1011        format!(
1012            "let {param_name} = {param_name}.map(|v| {{\n        \
1013             let bridge = {struct_name}::new(v);\n        \
1014             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n    \
1015             }});"
1016        )
1017    } else {
1018        format!(
1019            "let {param_name} = {{\n        \
1020             let bridge = {struct_name}::new({param_name});\n        \
1021             std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n    \
1022             }};"
1023        )
1024    };
1025
1026    // Serde let bindings for non-bridge Named params
1027    let serde_bindings: String = func
1028        .params
1029        .iter()
1030        .enumerate()
1031        .filter(|(idx, p)| {
1032            if *idx == bridge_param_idx {
1033                return false;
1034            }
1035            let named = match &p.ty {
1036                TypeRef::Named(n) => Some(n.as_str()),
1037                TypeRef::Optional(inner) => {
1038                    if let TypeRef::Named(n) = inner.as_ref() {
1039                        Some(n.as_str())
1040                    } else {
1041                        None
1042                    }
1043                }
1044                _ => None,
1045            };
1046            named.is_some_and(|n| !opaque_types.contains(n))
1047        })
1048        .map(|(_, p)| {
1049            let name = &p.name;
1050            let core_path = format!(
1051                "{core_import}::{}",
1052                match &p.ty {
1053                    TypeRef::Named(n) => n.clone(),
1054                    TypeRef::Optional(inner) =>
1055                        if let TypeRef::Named(n) = inner.as_ref() {
1056                            n.clone()
1057                        } else {
1058                            String::new()
1059                        },
1060                    _ => String::new(),
1061                }
1062            );
1063            if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
1064                format!(
1065                    "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n        \
1066                     let json = serde_json::to_string(&v){err_conv}?;\n        \
1067                     serde_json::from_str(&json){err_conv}\n    \
1068                     }}).transpose()?;\n    "
1069                )
1070            } else {
1071                format!(
1072                    "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n    \
1073                     let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n    "
1074                )
1075            }
1076        })
1077        .collect();
1078
1079    // Build call args
1080    let call_args: Vec<String> = func
1081        .params
1082        .iter()
1083        .enumerate()
1084        .map(|(idx, p)| {
1085            if idx == bridge_param_idx {
1086                return p.name.clone();
1087            }
1088            match &p.ty {
1089                TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
1090                    if p.optional {
1091                        format!("{}.as_ref().map(|v| &v.inner)", p.name)
1092                    } else {
1093                        format!("&{}.inner", p.name)
1094                    }
1095                }
1096                TypeRef::Named(_) => format!("{}_core", p.name),
1097                TypeRef::Optional(inner) => {
1098                    if let TypeRef::Named(n) = inner.as_ref() {
1099                        if opaque_types.contains(n.as_str()) {
1100                            format!("{}.as_ref().map(|v| &v.inner)", p.name)
1101                        } else {
1102                            format!("{}_core", p.name)
1103                        }
1104                    } else {
1105                        p.name.clone()
1106                    }
1107                }
1108                TypeRef::String | TypeRef::Char => {
1109                    if p.is_ref {
1110                        format!("&{}", p.name)
1111                    } else {
1112                        p.name.clone()
1113                    }
1114                }
1115                _ => p.name.clone(),
1116            }
1117        })
1118        .collect();
1119    let call_args_str = call_args.join(", ");
1120
1121    let core_fn_path = {
1122        let path = func.rust_path.replace('-', "_");
1123        if path.starts_with(core_import) {
1124            path
1125        } else {
1126            format!("{core_import}::{}", func.name)
1127        }
1128    };
1129    let core_call = format!("{core_fn_path}({call_args_str})");
1130
1131    let return_wrap = match &func.return_type {
1132        TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
1133            format!("{name} {{ inner: std::sync::Arc::new(val) }}")
1134        }
1135        TypeRef::Named(_) => "val.into()".to_string(),
1136        TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
1137        _ => "val".to_string(),
1138    };
1139
1140    let body = if func.error_type.is_some() {
1141        if return_wrap == "val" {
1142            format!("{bridge_wrap}\n    {serde_bindings}{core_call}{err_conv}")
1143        } else {
1144            format!("{bridge_wrap}\n    {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
1145        }
1146    } else {
1147        format!("{bridge_wrap}\n    {serde_bindings}{core_call}")
1148    };
1149
1150    let func_name = &func.name;
1151    let mut out = String::with_capacity(1024);
1152    if func.error_type.is_some() {
1153        writeln!(out, "#[allow(clippy::missing_errors_doc)]").ok();
1154    }
1155    writeln!(out, "pub fn {func_name}({params_str}) -> {ret} {{").ok();
1156    writeln!(out, "    {body}").ok();
1157    writeln!(out, "}}").ok();
1158
1159    out
1160}