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