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