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