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::{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}
20
21impl TraitBridgeGenerator for PhpBridgeGenerator {
22    fn foreign_object_type(&self) -> &str {
23        "&mut ext_php_rs::types::ZendObject"
24    }
25
26    fn bridge_imports(&self) -> Vec<String> {
27        vec!["use std::rc::Rc;".to_string(), "use std::cell::RefCell;".to_string()]
28    }
29
30    fn gen_sync_method_body(&self, method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
31        let name = &method.name;
32        let mut out = String::with_capacity(512);
33
34        // PHP is single-threaded; just call the method directly.
35        writeln!(
36            out,
37            "// SAFETY: PHP objects are single-threaded; method calls are safe within a request."
38        )
39        .ok();
40
41        let has_args = !method.params.is_empty();
42        if has_args {
43            writeln!(out, "let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();").ok();
44            for p in &method.params {
45                writeln!(
46                    out,
47                    "args.push(ext_php_rs::types::Zval::try_from({}).unwrap_or_default());",
48                    p.name
49                )
50                .ok();
51            }
52        }
53
54        let args_expr = if has_args {
55            "args.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()"
56        } else {
57            "vec![]"
58        };
59
60        writeln!(out, "let result = self.inner.try_call_method(\"{name}\", {args_expr});").ok();
61        writeln!(out, "match result {{").ok();
62        writeln!(
63            out,
64            "    Ok(val) => val.string().unwrap_or_default().parse().unwrap_or_default(),"
65        )
66        .ok();
67        writeln!(out, "    Err(_) => Default::default(),").ok();
68        writeln!(out, "}}").ok();
69
70        out
71    }
72
73    fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
74        let name = &method.name;
75        let mut out = String::with_capacity(1024);
76
77        writeln!(out, "let inner_obj = self.inner.clone();").ok();
78        writeln!(out, "let cached_name = self.cached_name.clone();").ok();
79
80        // Clone params for the blocking closure
81        for p in &method.params {
82            match &p.ty {
83                TypeRef::String => {
84                    writeln!(out, "let {} = {}.clone();", p.name, p.name).ok();
85                }
86                _ => {
87                    writeln!(out, "let {} = {};", p.name, p.name).ok();
88                }
89            }
90        }
91
92        writeln!(out).ok();
93        writeln!(out, "Box::pin(async move {{").ok();
94        writeln!(out, "    // SAFETY: PHP objects are single-threaded within a request.").ok();
95        writeln!(out, "    // The block_on executes within the async runtime.").ok();
96        writeln!(out, "    let result = WORKER_RUNTIME.block_on(async {{").ok();
97
98        let has_args = !method.params.is_empty();
99        if has_args {
100            writeln!(out, "        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();").ok();
101            for p in &method.params {
102                writeln!(
103                    out,
104                    "        args.push(ext_php_rs::types::Zval::try_from({}).unwrap_or_default());",
105                    p.name
106                )
107                .ok();
108            }
109        }
110
111        let args_expr = if has_args {
112            "args.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()"
113        } else {
114            "vec![]"
115        };
116
117        writeln!(
118            out,
119            "        match inner_obj.try_call_method(\"{name}\", {args_expr}) {{"
120        )
121        .ok();
122        writeln!(
123            out,
124            "            Ok(val) => val.string().unwrap_or_default().parse().unwrap_or_default(),"
125        )
126        .ok();
127        writeln!(
128            out,
129            "            Err(e) => Err({}::KreuzbergError::Plugin {{",
130            spec.core_import
131        )
132        .ok();
133        writeln!(
134            out,
135            "                message: format!(\"Plugin '{{}}' method '{name}' failed: {{}}\", cached_name, e),"
136        )
137        .ok();
138        writeln!(out, "                plugin_name: cached_name.clone(),").ok();
139        writeln!(out, "            }}),").ok();
140        writeln!(out, "        }}").ok();
141        writeln!(out, "    }});").ok();
142        writeln!(out, "    result").ok();
143        writeln!(out, "}})").ok();
144
145        out
146    }
147
148    fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
149        let wrapper = spec.wrapper_name();
150        let mut out = String::with_capacity(512);
151
152        writeln!(out, "impl {wrapper} {{").ok();
153        writeln!(out, "    /// Create a new bridge wrapping a PHP object.").ok();
154        writeln!(out, "    ///").ok();
155        writeln!(
156            out,
157            "    /// Validates that the PHP object provides all required methods."
158        )
159        .ok();
160        writeln!(
161            out,
162            "    pub fn new(php_obj: &mut ext_php_rs::types::ZendObject) -> Self {{"
163        )
164        .ok();
165
166        // Validate all required methods exist
167        for req_method in spec.required_methods() {
168            writeln!(
169                out,
170                "        debug_assert!(php_obj.get_property(\"{}\").is_some(),",
171                req_method.name
172            )
173            .ok();
174            writeln!(
175                out,
176                "            \"PHP object missing required method: {}\");",
177                req_method.name
178            )
179            .ok();
180        }
181
182        // Extract and cache name
183        writeln!(out, "        let cached_name = php_obj").ok();
184        writeln!(out, "            .try_call_method(\"name\", vec![])").ok();
185        writeln!(out, "            .ok()").ok();
186        writeln!(out, "            .and_then(|v| v.string())").ok();
187        writeln!(out, "            .unwrap_or(\"unknown\".into())").ok();
188        writeln!(out, "            .to_string();").ok();
189
190        writeln!(out).ok();
191        writeln!(out, "        Self {{").ok();
192        writeln!(out, "            inner: php_obj,").ok();
193        writeln!(out, "            cached_name,").ok();
194        writeln!(out, "        }}").ok();
195        writeln!(out, "    }}").ok();
196        writeln!(out, "}}").ok();
197
198        out
199    }
200
201    fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
202        let Some(register_fn) = spec.bridge_config.register_fn.as_deref() else {
203            return String::new();
204        };
205        let Some(registry_getter) = spec.bridge_config.registry_getter.as_deref() else {
206            return String::new();
207        };
208        let wrapper = spec.wrapper_name();
209        let trait_path = spec.trait_path();
210
211        let mut out = String::with_capacity(1024);
212
213        writeln!(out, "#[php_function]").ok();
214        writeln!(
215            out,
216            "pub fn {register_fn}(backend: &mut ext_php_rs::types::ZendObject) -> ext_php_rs::prelude::PhpResult<()> {{"
217        )
218        .ok();
219
220        // Validate required methods
221        let req_methods: Vec<&MethodDef> = spec.required_methods();
222        if !req_methods.is_empty() {
223            for method in &req_methods {
224                writeln!(out, "    if backend.get_property(\"{}\").is_none() {{", method.name).ok();
225                writeln!(out, "        return Err(ext_php_rs::exception::PhpException::default(").ok();
226                writeln!(
227                    out,
228                    "            format!(\"Backend missing required method: {{}}\", \"{}\")",
229                    method.name
230                )
231                .ok();
232                writeln!(out, "        ).into());").ok();
233                writeln!(out, "    }}").ok();
234            }
235        }
236
237        writeln!(out).ok();
238        writeln!(out, "    let wrapper = {wrapper}::new(backend);").ok();
239        writeln!(
240            out,
241            "    let arc: Rc<RefCell<dyn {trait_path}>> = Rc::new(RefCell::new(wrapper));"
242        )
243        .ok();
244        writeln!(out).ok();
245
246        writeln!(out, "    let registry = {registry_getter}();").ok();
247        writeln!(out, "    let mut registry = registry;").ok();
248        writeln!(
249            out,
250            "    registry.register(arc).map_err(|e| ext_php_rs::exception::PhpException::default("
251        )
252        .ok();
253        writeln!(out, "        format!(\"Failed to register backend: {{}}\", e)").ok();
254        writeln!(out, "    ))?;").ok();
255        writeln!(out, "    Ok(())").ok();
256        writeln!(out, "}}").ok();
257
258        out
259    }
260}
261
262/// Generate all trait bridge code for a given trait type and bridge config.
263pub fn gen_trait_bridge(
264    trait_type: &TypeDef,
265    bridge_cfg: &TraitBridgeConfig,
266    core_import: &str,
267    api: &ApiSurface,
268) -> String {
269    // Build type name → rust_path lookup as owned HashMap
270    let type_paths: HashMap<String, String> = api
271        .types
272        .iter()
273        .map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
274        .chain(
275            api.enums
276                .iter()
277                .map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
278        )
279        .collect();
280
281    // Visitor-style bridge: all methods have defaults, no registry, no super-trait.
282    let is_visitor_bridge = bridge_cfg.type_alias.is_some()
283        && bridge_cfg.register_fn.is_none()
284        && bridge_cfg.super_trait.is_none()
285        && trait_type.methods.iter().all(|m| m.has_default_impl);
286
287    if is_visitor_bridge {
288        let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
289        let trait_path = trait_type.rust_path.replace('-', "_");
290        gen_visitor_bridge(trait_type, bridge_cfg, &struct_name, &trait_path, &type_paths)
291    } else {
292        // Use the IR-driven TraitBridgeGenerator infrastructure
293        let generator = PhpBridgeGenerator {
294            core_import: core_import.to_string(),
295            type_paths: type_paths.clone(),
296        };
297        let spec = TraitBridgeSpec {
298            trait_def: trait_type,
299            bridge_config: bridge_cfg,
300            core_import,
301            wrapper_prefix: "Php",
302            type_paths,
303        };
304        gen_bridge_all(&spec, &generator)
305    }
306}
307
308/// Generate a visitor-style bridge wrapping a PHP `Zval` object reference.
309///
310/// Every trait method checks if the PHP object has a matching camelCase method,
311/// then calls it and maps the PHP return value to `VisitResult`.
312fn gen_visitor_bridge(
313    trait_type: &TypeDef,
314    _bridge_cfg: &TraitBridgeConfig,
315    struct_name: &str,
316    trait_path: &str,
317    type_paths: &HashMap<String, String>,
318) -> String {
319    let mut out = String::with_capacity(4096);
320    let core_crate = trait_path
321        .split("::")
322        .next()
323        .unwrap_or("html_to_markdown_rs")
324        .to_string();
325    // Helper: convert NodeContext to a PHP array (Zval)
326    writeln!(out, "fn nodecontext_to_php_array(").unwrap();
327    writeln!(out, "    ctx: &{core_crate}::visitor::NodeContext,").unwrap();
328    writeln!(out, ") -> ext_php_rs::boxed::ZBox<ext_php_rs::types::ZendHashTable> {{").unwrap();
329    writeln!(out, "    let mut arr = ext_php_rs::types::ZendHashTable::new();").unwrap();
330    writeln!(
331        out,
332        "    arr.insert(\"nodeType\", ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", ctx.node_type)).unwrap_or_default()).ok();"
333    )
334    .unwrap();
335    writeln!(
336        out,
337        "    arr.insert(\"tagName\", ext_php_rs::types::Zval::try_from(ctx.tag_name.clone()).unwrap_or_default()).ok();"
338    )
339    .unwrap();
340    writeln!(
341        out,
342        "    arr.insert(\"depth\", ext_php_rs::types::Zval::try_from(ctx.depth as i64).unwrap_or_default()).ok();"
343    )
344    .unwrap();
345    writeln!(
346        out,
347        "    arr.insert(\"indexInParent\", ext_php_rs::types::Zval::try_from(ctx.index_in_parent as i64).unwrap_or_default()).ok();"
348    )
349    .unwrap();
350    writeln!(
351        out,
352        "    arr.insert(\"isInline\", ext_php_rs::types::Zval::try_from(ctx.is_inline).unwrap_or_default()).ok();"
353    )
354    .unwrap();
355    writeln!(out, "    if let Some(ref pt) = ctx.parent_tag {{").unwrap();
356    writeln!(
357        out,
358        "        arr.insert(\"parentTag\", ext_php_rs::types::Zval::try_from(pt.clone()).unwrap_or_default()).ok();"
359    )
360    .unwrap();
361    writeln!(out, "    }}").unwrap();
362    writeln!(out, "    let mut attrs = ext_php_rs::types::ZendHashTable::new();").unwrap();
363    writeln!(out, "    for (k, v) in &ctx.attributes {{").unwrap();
364    writeln!(
365        out,
366        "        attrs.insert(k.as_str(), ext_php_rs::types::Zval::try_from(v.clone()).unwrap_or_default()).ok();"
367    )
368    .unwrap();
369    writeln!(out, "    }}").unwrap();
370    writeln!(out, "    let mut attrs_zval = ext_php_rs::types::Zval::new();").unwrap();
371    writeln!(out, "    attrs_zval.set_hashtable(attrs);").unwrap();
372    writeln!(out, "    arr.insert(\"attributes\", attrs_zval).ok();").unwrap();
373    writeln!(out, "    arr").unwrap();
374    writeln!(out, "}}").unwrap();
375    writeln!(out).unwrap();
376
377    // Bridge struct — stores a reference to the PHP object.
378    // The reference is valid for the duration of the PHP function call that
379    // created the bridge, which spans the entire Rust trait method dispatch.
380    writeln!(out, "pub struct {struct_name} {{").unwrap();
381    writeln!(out, "    php_obj: *mut ext_php_rs::types::ZendObject,").unwrap();
382    writeln!(out, "    cached_name: String,").unwrap();
383    writeln!(out, "}}").unwrap();
384    writeln!(out).unwrap();
385
386    // SAFETY: The raw pointer is only used while the PHP call stack frame is
387    // alive. The bridge is consumed before the PHP function returns.
388    writeln!(out, "// SAFETY: PHP objects are single-threaded; the bridge is used").unwrap();
389    writeln!(out, "// only within a single PHP request, never across threads.").unwrap();
390    writeln!(out, "unsafe impl Send for {struct_name} {{}}").unwrap();
391    writeln!(out, "unsafe impl Sync for {struct_name} {{}}").unwrap();
392    writeln!(out).unwrap();
393
394    writeln!(out, "impl Clone for {struct_name} {{").unwrap();
395    writeln!(out, "    fn clone(&self) -> Self {{").unwrap();
396    writeln!(out, "        Self {{").unwrap();
397    writeln!(out, "            php_obj: self.php_obj,").unwrap();
398    writeln!(out, "            cached_name: self.cached_name.clone(),").unwrap();
399    writeln!(out, "        }}").unwrap();
400    writeln!(out, "    }}").unwrap();
401    writeln!(out, "}}").unwrap();
402    writeln!(out).unwrap();
403
404    // Manual Debug impl
405    writeln!(out, "impl std::fmt::Debug for {struct_name} {{").unwrap();
406    writeln!(
407        out,
408        "    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{"
409    )
410    .unwrap();
411    writeln!(out, "        write!(f, \"{struct_name}\")").unwrap();
412    writeln!(out, "    }}").unwrap();
413    writeln!(out, "}}").unwrap();
414    writeln!(out).unwrap();
415
416    // Constructor takes &mut ZendObject, which is what ext-php-rs exposes via
417    // FromZvalMut. We store the raw pointer; the caller guarantees the object
418    // outlives this bridge.
419    writeln!(out, "impl {struct_name} {{").unwrap();
420    writeln!(
421        out,
422        "    pub fn new(php_obj: &mut ext_php_rs::types::ZendObject) -> Self {{"
423    )
424    .unwrap();
425    writeln!(out, "        let cached_name = php_obj").unwrap();
426    writeln!(out, "            .try_call_method(\"name\", vec![])").unwrap();
427    writeln!(out, "            .ok()").unwrap();
428    writeln!(out, "            .and_then(|v| v.string())").unwrap();
429    writeln!(out, "            .unwrap_or(\"unknown\".into())").unwrap();
430    writeln!(out, "            .to_string();").unwrap();
431    writeln!(out, "        Self {{ php_obj: php_obj as *mut _, cached_name }}").unwrap();
432    writeln!(out, "    }}").unwrap();
433    writeln!(out, "}}").unwrap();
434    writeln!(out).unwrap();
435
436    // Trait impl
437    writeln!(out, "impl {trait_path} for {struct_name} {{").unwrap();
438    for method in &trait_type.methods {
439        if method.trait_source.is_some() {
440            continue;
441        }
442        gen_visitor_method_php(&mut out, method, type_paths);
443    }
444    writeln!(out, "}}").unwrap();
445    writeln!(out).unwrap();
446
447    out
448}
449
450/// Map a visitor method parameter type to the correct Rust type string.
451fn visitor_param_type(ty: &TypeRef, is_ref: bool, optional: bool, tp: &HashMap<String, String>) -> String {
452    if optional && matches!(ty, TypeRef::String) && is_ref {
453        return "Option<&str>".to_string();
454    }
455    if is_ref {
456        if let TypeRef::Vec(inner) = ty {
457            let inner_str = param_type(inner, "", false, tp);
458            return format!("&[{inner_str}]");
459        }
460    }
461    param_type(ty, "", is_ref, tp)
462}
463
464/// Generate a single visitor method that checks for a camelCase PHP method and calls it.
465fn gen_visitor_method_php(out: &mut String, method: &MethodDef, type_paths: &HashMap<String, String>) {
466    let name = &method.name;
467    let php_name = to_camel_case(name);
468
469    let mut sig_parts = vec!["&mut self".to_string()];
470    for p in &method.params {
471        let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
472        sig_parts.push(format!("{}: {}", p.name, ty_str));
473    }
474    let sig = sig_parts.join(", ");
475
476    let ret_ty = match &method.return_type {
477        TypeRef::Named(n) => type_paths.get(n).cloned().unwrap_or_else(|| n.clone()),
478        other => param_type(other, "", false, type_paths),
479    };
480
481    writeln!(out, "    fn {name}({sig}) -> {ret_ty} {{").unwrap();
482
483    // SAFETY: php_obj pointer is valid for the lifetime of the PHP call frame.
484    writeln!(
485        out,
486        "        // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call."
487    )
488    .unwrap();
489    writeln!(out, "        let php_obj_ref = unsafe {{ &mut *self.php_obj }};").unwrap();
490
491    // Build args array
492    let has_args = !method.params.is_empty();
493    if has_args {
494        writeln!(out, "        let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();").unwrap();
495        for p in &method.params {
496            if let TypeRef::Named(n) = &p.ty {
497                if n == "NodeContext" {
498                    writeln!(
499                        out,
500                        "        let ctx_arr = nodecontext_to_php_array({}{});",
501                        if p.is_ref { "" } else { "&" },
502                        p.name
503                    )
504                    .unwrap();
505                    writeln!(
506                        out,
507                        "        args.push(ext_php_rs::convert::IntoZval::into_zval(ctx_arr, false).unwrap_or_default());"
508                    )
509                    .unwrap();
510                    continue;
511                }
512            }
513            // Check optional string ref BEFORE non-optional string, since visitor_param_type
514            // returns Option<&str> for optional string ref params.
515            if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
516                writeln!(
517                    out,
518                    "        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() }});",
519                    p.name
520                )
521                .unwrap();
522                continue;
523            }
524            if matches!(&p.ty, TypeRef::String) {
525                if p.is_ref {
526                    writeln!(
527                        out,
528                        "        args.push(ext_php_rs::types::Zval::try_from({}.to_string()).unwrap_or_default());",
529                        p.name
530                    )
531                    .unwrap();
532                } else {
533                    writeln!(
534                        out,
535                        "        args.push(ext_php_rs::types::Zval::try_from({}.clone()).unwrap_or_default());",
536                        p.name
537                    )
538                    .unwrap();
539                }
540                continue;
541            }
542            if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
543                writeln!(
544                    out,
545                    "        {{ let mut _zv = ext_php_rs::types::Zval::new(); _zv.set_bool({}); args.push(_zv); }}",
546                    p.name
547                )
548                .unwrap();
549                continue;
550            }
551            // Default: format as string
552            writeln!(
553                out,
554                "        args.push(ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default());",
555                p.name
556            )
557            .unwrap();
558        }
559    }
560
561    // Call the PHP method via try_call_method which takes Vec<&dyn IntoZvalDyn>.
562    // If the method does not exist, try_call_method returns Err(Error::Callable),
563    // which we treat as a "no-op, return Continue" (same as the default impl).
564    if has_args {
565        writeln!(
566            out,
567            "        let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect();"
568        )
569        .unwrap();
570    }
571    let args_expr = if has_args { "dyn_args" } else { "vec![]" };
572    writeln!(
573        out,
574        "        let result = php_obj_ref.try_call_method(\"{php_name}\", {args_expr});"
575    )
576    .unwrap();
577
578    // Parse result — try_call_method returns Result<Zval> (not Result<Option<Zval>>)
579    writeln!(out, "        match result {{").unwrap();
580    writeln!(out, "            Err(_) => {ret_ty}::Continue,").unwrap();
581    writeln!(out, "            Ok(val) => {{").unwrap();
582    writeln!(
583        out,
584        "                let s = val.string().unwrap_or_default().to_lowercase();"
585    )
586    .unwrap();
587    writeln!(out, "                match s.as_str() {{").unwrap();
588    writeln!(out, "                    \"continue\" => {ret_ty}::Continue,").unwrap();
589    writeln!(out, "                    \"skip\" => {ret_ty}::Skip,").unwrap();
590    writeln!(
591        out,
592        "                    \"preserve_html\" | \"preservehtml\" => {ret_ty}::PreserveHtml,"
593    )
594    .unwrap();
595    writeln!(out, "                    other => {ret_ty}::Custom(other.to_string()),").unwrap();
596    writeln!(out, "                }}").unwrap();
597    writeln!(out, "            }}").unwrap();
598    writeln!(out, "        }}").unwrap();
599    writeln!(out, "    }}").unwrap();
600    writeln!(out).unwrap();
601}
602
603/// Convert snake_case to camelCase.
604fn to_camel_case(name: &str) -> String {
605    let mut result = String::with_capacity(name.len());
606    let mut capitalize_next = false;
607    for (i, c) in name.chars().enumerate() {
608        if c == '_' {
609            capitalize_next = true;
610        } else if capitalize_next {
611            result.extend(c.to_uppercase());
612            capitalize_next = false;
613        } else if i == 0 {
614            result.extend(c.to_lowercase());
615        } else {
616            result.push(c);
617        }
618    }
619    result
620}
621
622/// Map TypeRef to a Rust type string.
623fn param_type(ty: &TypeRef, ci: &str, is_ref: bool, tp: &HashMap<String, String>) -> String {
624    match ty {
625        TypeRef::Bytes if is_ref => "&[u8]".into(),
626        TypeRef::Bytes => "Vec<u8>".into(),
627        TypeRef::String if is_ref => "&str".into(),
628        TypeRef::String => "String".into(),
629        TypeRef::Path if is_ref => "&std::path::Path".into(),
630        TypeRef::Path => "std::path::PathBuf".into(),
631        TypeRef::Named(n) => {
632            let qualified = tp.get(n).cloned().unwrap_or_else(|| format!("{ci}::{n}"));
633            if is_ref { format!("&{qualified}") } else { qualified }
634        }
635        TypeRef::Vec(inner) => format!("Vec<{}>", param_type(inner, ci, false, tp)),
636        TypeRef::Optional(inner) => format!("Option<{}>", param_type(inner, ci, false, tp)),
637        TypeRef::Primitive(p) => prim(p).into(),
638        TypeRef::Unit => "()".into(),
639        TypeRef::Char => "char".into(),
640        TypeRef::Map(k, v) => format!(
641            "std::collections::HashMap<{}, {}>",
642            param_type(k, ci, false, tp),
643            param_type(v, ci, false, tp)
644        ),
645        TypeRef::Json => "serde_json::Value".into(),
646        TypeRef::Duration => "std::time::Duration".into(),
647    }
648}
649
650fn prim(p: &alef_core::ir::PrimitiveType) -> &'static str {
651    use alef_core::ir::PrimitiveType::*;
652    match p {
653        Bool => "bool",
654        U8 => "u8",
655        U16 => "u16",
656        U32 => "u32",
657        U64 => "u64",
658        I8 => "i8",
659        I16 => "i16",
660        I32 => "i32",
661        I64 => "i64",
662        F32 => "f32",
663        F64 => "f64",
664        Usize => "usize",
665        Isize => "isize",
666    }
667}
668
669/// Find the first parameter index and bridge config where the parameter's named type
670/// matches a trait bridge's `type_alias`.
671///
672/// Returns `None` when no bridge applies.
673pub fn find_bridge_param<'a>(
674    func: &alef_core::ir::FunctionDef,
675    bridges: &'a [TraitBridgeConfig],
676) -> Option<(usize, &'a TraitBridgeConfig)> {
677    for (idx, param) in func.params.iter().enumerate() {
678        let named = match &param.ty {
679            TypeRef::Named(n) => Some(n.as_str()),
680            TypeRef::Optional(inner) => {
681                if let TypeRef::Named(n) = inner.as_ref() {
682                    Some(n.as_str())
683                } else {
684                    None
685                }
686            }
687            _ => None,
688        };
689        for bridge in bridges {
690            if let Some(type_name) = named {
691                if bridge.type_alias.as_deref() == Some(type_name) {
692                    return Some((idx, bridge));
693                }
694            }
695            if bridge.param_name.as_deref() == Some(param.name.as_str()) {
696                return Some((idx, bridge));
697            }
698        }
699    }
700    None
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}