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