1use minijinja::context;
7
8use alef_codegen::doc_emission::{DocTarget, sanitize_rust_idioms};
9use alef_codegen::generators::trait_bridge::{
10 BridgeOutput, TraitBridgeGenerator, TraitBridgeSpec, bridge_param_type as param_type, gen_bridge_all,
11 visitor_param_type,
12};
13use alef_core::config::TraitBridgeConfig;
14use alef_core::ir::{ApiSurface, MethodDef, TypeDef, TypeRef};
15use std::collections::HashMap;
16
17pub use alef_codegen::generators::trait_bridge::find_bridge_param;
22
23pub struct PhpBridgeGenerator {
26 pub core_import: String,
28 pub type_paths: HashMap<String, String>,
30 pub error_type: String,
32}
33
34impl TraitBridgeGenerator for PhpBridgeGenerator {
35 fn foreign_object_type(&self) -> &str {
36 "*mut ext_php_rs::types::ZendObject"
37 }
38
39 fn bridge_imports(&self) -> Vec<String> {
40 vec!["std::sync::Arc".to_string()]
41 }
42
43 fn gen_sync_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
44 let name = &method.name;
45
46 let has_args = !method.params.is_empty();
47 let args_expr = if has_args {
48 let mut args_parts = Vec::new();
49 for p in &method.params {
50 let arg_expr = match &p.ty {
51 TypeRef::String => format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name),
52 TypeRef::Path => format!(
53 "ext_php_rs::types::Zval::try_from({}.to_string_lossy().to_string()).unwrap_or_default()",
54 p.name
55 ),
56 TypeRef::Bytes => format!(
57 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
58 p.name
59 ),
60 TypeRef::Named(_) => {
61 format!(
62 "ext_php_rs::types::Zval::try_from(serde_json::to_string(&{}).unwrap_or_default()).unwrap_or_default()",
63 p.name
64 )
65 }
66 TypeRef::Primitive(_) => {
67 format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name)
68 }
69 _ => format!(
70 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
71 p.name
72 ),
73 };
74 args_parts.push(arg_expr);
75 }
76 let args_array = format!("[{}]", args_parts.join(", "));
77 format!(
78 "{}.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()",
79 args_array
80 )
81 } else {
82 "vec![]".to_string()
83 };
84
85 let is_result_type = method.error_type.is_some();
86 let is_unit_return = matches!(method.return_type, TypeRef::Unit);
87 let deserialize_error_expr = spec.make_error("format!(\"Deserialize error: {}\", e)");
88 let call_error_expr = spec.make_error("e.to_string()");
89
90 crate::template_env::render(
91 "sync_method_body.jinja",
92 context! {
93 method_name => name,
94 args_expr => args_expr,
95 is_result_type => is_result_type,
96 is_unit_return => is_unit_return,
97 deserialize_error_expr => deserialize_error_expr,
98 call_error_expr => call_error_expr,
99 },
100 )
101 }
102
103 fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
104 let name = &method.name;
105
106 let string_params: Vec<String> = method
107 .params
108 .iter()
109 .filter(|p| matches!(&p.ty, TypeRef::String))
110 .map(|p| p.name.clone())
111 .collect();
112
113 let has_args = !method.params.is_empty();
114 let args_expr = if has_args {
115 let mut args_parts = Vec::new();
116 for p in &method.params {
117 let arg_expr = match &p.ty {
118 TypeRef::String => format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name),
119 TypeRef::Path => format!(
120 "ext_php_rs::types::Zval::try_from({}.to_string_lossy().to_string()).unwrap_or_default()",
121 p.name
122 ),
123 TypeRef::Bytes => format!(
124 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
125 p.name
126 ),
127 TypeRef::Named(_) => {
128 format!(
129 "ext_php_rs::types::Zval::try_from(serde_json::to_string(&{}).unwrap_or_default()).unwrap_or_default()",
130 p.name
131 )
132 }
133 TypeRef::Primitive(_) => {
134 format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name)
135 }
136 _ => format!(
137 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
138 p.name
139 ),
140 };
141 args_parts.push(arg_expr);
142 }
143 let args_array = format!("[{}]", args_parts.join(", "));
144 format!(
145 "{}.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()",
146 args_array
147 )
148 } else {
149 "vec![]".to_string()
150 };
151
152 let is_result_type = method.error_type.is_some();
153 let deserialize_error_expr = spec.make_error("format!(\"Deserialize error: {}\", e)");
154 let call_error_expr = spec.make_error(&format!(
155 "format!(\"Plugin '{{}}' method '{name}' failed: {{}}\", cached_name, e)"
156 ));
157
158 crate::template_env::render(
159 "async_method_body.jinja",
160 context! {
161 method_name => name,
162 args_expr => args_expr,
163 string_params => string_params,
164 is_result_type => is_result_type,
165 deserialize_error_expr => deserialize_error_expr,
166 call_error_expr => call_error_expr,
167 },
168 )
169 }
170
171 fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
172 let wrapper = spec.wrapper_name();
173
174 crate::template_env::render(
175 "bridge_constructor.jinja",
176 context! {
177 wrapper => &wrapper,
178 },
179 )
180 }
181
182 fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
183 let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
184 return String::new();
185 };
186 let host_path = alef_codegen::generators::trait_bridge::host_function_path(spec, unregister_fn);
187
188 crate::template_env::render(
189 "bridge_unregister_fn.jinja",
190 context! {
191 unregister_fn => unregister_fn,
192 host_path => &host_path,
193 },
194 )
195 }
196
197 fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
198 let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
199 return String::new();
200 };
201 let host_path = alef_codegen::generators::trait_bridge::host_function_path(spec, clear_fn);
202
203 crate::template_env::render(
204 "bridge_clear_fn.jinja",
205 context! {
206 clear_fn => clear_fn,
207 host_path => &host_path,
208 },
209 )
210 }
211
212 fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
213 let Some(register_fn) = spec.bridge_config.register_fn.as_deref() else {
214 return String::new();
215 };
216 let Some(registry_getter) = spec.bridge_config.registry_getter.as_deref() else {
217 return String::new();
218 };
219 let wrapper = spec.wrapper_name();
220 let trait_path = spec.trait_path();
221
222 let req_methods: Vec<&MethodDef> = spec.required_methods();
223 let required_methods: Vec<minijinja::Value> = req_methods
224 .iter()
225 .map(|m| {
226 minijinja::context! {
227 name => m.name.as_str(),
228 }
229 })
230 .collect();
231
232 let extra_args = spec
233 .bridge_config
234 .register_extra_args
235 .as_deref()
236 .map(|a| format!(", {a}"))
237 .unwrap_or_default();
238
239 crate::template_env::render(
240 "bridge_registration_fn.jinja",
241 context! {
242 register_fn => register_fn,
243 required_methods => required_methods,
244 wrapper => &wrapper,
245 trait_path => &trait_path,
246 registry_getter => registry_getter,
247 extra_args => &extra_args,
248 },
249 )
250 }
251}
252
253pub fn gen_trait_bridge(
255 trait_type: &TypeDef,
256 bridge_cfg: &TraitBridgeConfig,
257 core_import: &str,
258 error_type: &str,
259 error_constructor: &str,
260 api: &ApiSurface,
261) -> BridgeOutput {
262 let type_paths: HashMap<String, String> = api
264 .types
265 .iter()
266 .map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
267 .chain(
268 api.enums
269 .iter()
270 .map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
271 )
272 .chain(
275 api.excluded_type_paths
276 .iter()
277 .map(|(name, path)| (name.clone(), path.replace('-', "_"))),
278 )
279 .collect();
280
281 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 let code = gen_visitor_bridge(trait_type, bridge_cfg, &struct_name, &trait_path, &type_paths);
291
292 BridgeOutput { imports: vec![], code }
295 } else {
296 let generator = PhpBridgeGenerator {
298 core_import: core_import.to_string(),
299 type_paths: type_paths.clone(),
300 error_type: error_type.to_string(),
301 };
302 let spec = TraitBridgeSpec {
303 trait_def: trait_type,
304 bridge_config: bridge_cfg,
305 core_import,
306 wrapper_prefix: "Php",
307 type_paths,
308 error_type: error_type.to_string(),
309 error_constructor: error_constructor.to_string(),
310 };
311 gen_bridge_all(&spec, &generator)
312 }
313}
314
315fn gen_visitor_bridge(
320 trait_type: &TypeDef,
321 bridge_cfg: &TraitBridgeConfig,
322 struct_name: &str,
323 trait_path: &str,
324 type_paths: &HashMap<String, String>,
325) -> String {
326 let mut out = String::with_capacity(4096);
327 let core_crate = trait_path
328 .split("::")
329 .next()
330 .filter(|s| !s.is_empty())
331 .unwrap_or_else(|| panic!("trait_path '{trait_path}' must be a qualified path of the form 'crate_name::...'; configure extension_name in alef.toml"))
332 .to_string();
333
334 out.push_str(&crate::template_env::render(
336 "visitor_nodecontext_helper.jinja",
337 context! {
338 core_crate => &core_crate,
339 },
340 ));
341 out.push('\n');
342
343 out.push_str(&crate::template_env::render(
345 "visitor_zval_to_visitresult.jinja",
346 context! {
347 core_crate => &core_crate,
348 },
349 ));
350 out.push('\n');
351
352 out.push_str(&crate::template_env::render(
354 "php_visit_result_with_template.jinja",
355 context! {
356 core_crate => &core_crate,
357 },
358 ));
359 out.push_str("\n\n");
360
361 out.push_str(&crate::template_env::render(
363 "visitor_bridge_struct.jinja",
364 context! {
365 struct_name => struct_name,
366 },
367 ));
368 out.push('\n');
369
370 out.push_str(&crate::template_env::render(
372 "php_trait_impl_start.jinja",
373 context! {
374 trait_path => &trait_path,
375 struct_name => struct_name,
376 },
377 ));
378 for method in &trait_type.methods {
379 if method.trait_source.is_some() {
380 continue;
381 }
382 gen_visitor_method_php(&mut out, method, bridge_cfg, type_paths);
383 }
384 out.push_str("}\n");
385 out.push('\n');
386
387 out
388}
389
390fn gen_visitor_method_php(
392 out: &mut String,
393 method: &MethodDef,
394 bridge_cfg: &TraitBridgeConfig,
395 type_paths: &HashMap<String, String>,
396) {
397 let name = &method.name;
398
399 let mut sig_parts = vec!["&mut self".to_string()];
400 for p in &method.params {
401 let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
402 sig_parts.push(format!("{}: {}", p.name, ty_str));
403 }
404 let sig = sig_parts.join(", ");
405
406 let ret_ty = match &method.return_type {
407 TypeRef::Named(n) => type_paths.get(n).cloned().unwrap_or_else(|| n.clone()),
408 other => param_type(other, "", false, type_paths),
409 };
410
411 out.push_str(&crate::template_env::render(
412 "php_visitor_method_signature.jinja",
413 context! {
414 name => name,
415 sig => &sig,
416 ret_ty => &ret_ty,
417 },
418 ));
419
420 out.push_str(" // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n");
422 out.push_str(" let php_obj_ref = unsafe { &mut *self.php_obj };\n");
423
424 let has_args = !method.params.is_empty();
426 if has_args {
427 out.push_str(" let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n");
428 for p in &method.params {
429 if let TypeRef::Named(n) = &p.ty {
430 if Some(n.as_str()) == bridge_cfg.context_type.as_deref() {
431 out.push_str(&crate::template_env::render(
432 "php_visitor_arg_nodecontext.jinja",
433 context! {
434 name => &p.name,
435 ref => if p.is_ref { "" } else { "&" },
436 },
437 ));
438 out.push('\n');
439 continue;
440 }
441 }
442 if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
445 out.push_str(&crate::template_env::render(
446 "php_visitor_arg_optional_string_ref.jinja",
447 context! {
448 name => &p.name,
449 },
450 ));
451 out.push('\n');
452 continue;
453 }
454 if matches!(&p.ty, TypeRef::String) {
455 if p.is_ref {
456 out.push_str(&crate::template_env::render(
457 "php_visitor_arg_string_ref.jinja",
458 context! {
459 name => &p.name,
460 },
461 ));
462 } else {
463 out.push_str(&crate::template_env::render(
464 "php_visitor_arg_string_owned.jinja",
465 context! {
466 name => &p.name,
467 },
468 ));
469 }
470 out.push('\n');
471 continue;
472 }
473 if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
474 out.push_str(&crate::template_env::render(
475 "php_visitor_arg_bool.jinja",
476 context! {
477 name => &p.name,
478 },
479 ));
480 out.push('\n');
481 continue;
482 }
483 out.push_str(&crate::template_env::render(
485 "php_visitor_arg_default.jinja",
486 context! {
487 name => &p.name,
488 },
489 ));
490 out.push('\n');
491 }
492 }
493
494 if has_args {
498 out.push_str(" let dyn_args: Vec<&dyn ext_php_rs::convert::IntoZvalDyn> = args.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect();\n");
499 }
500 let args_expr = if has_args { "dyn_args" } else { "vec![]" };
501 out.push_str(&crate::template_env::render(
502 "php_visitor_method_php_call.jinja",
503 context! {
504 name => name,
505 args_expr => args_expr,
506 },
507 ));
508
509 let mut tmpl_var_names: Vec<String> = Vec::new();
512 for p in &method.params {
513 if let TypeRef::Named(n) = &p.ty {
514 if Some(n.as_str()) == bridge_cfg.context_type.as_deref() {
515 continue;
516 }
517 }
518 if matches!(&p.ty, TypeRef::Vec(_)) {
520 continue;
521 }
522 let key = p.name.strip_prefix('_').unwrap_or(&p.name);
524 let owned_var = format!("_{key}_s");
525 let expr: String = if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
526 format!("{}.map(|s| s.to_string()).unwrap_or_default()", p.name)
527 } else if matches!(&p.ty, TypeRef::String) && p.is_ref {
528 format!("{}.to_string()", p.name)
529 } else if matches!(&p.ty, TypeRef::String) {
530 format!("{}.clone()", p.name)
531 } else if matches!(&p.ty, TypeRef::Optional(_)) {
532 format!("{}.map(|v| v.to_string()).unwrap_or_default()", p.name)
533 } else {
534 format!("{}.to_string()", p.name)
535 };
536 out.push_str(&crate::template_env::render(
537 "php_visitor_template_var_let_binding.jinja",
538 context! {
539 owned_var => &owned_var,
540 expr => &expr,
541 },
542 ));
543 out.push('\n');
544 tmpl_var_names.push(format!("(\"{key}\", {owned_var}.as_str())"));
545 }
546 let tmpl_vars_expr = if tmpl_var_names.is_empty() {
547 "&[]".to_string()
548 } else {
549 format!("&[{}]", tmpl_var_names.join(", "))
550 };
551
552 out.push_str(&crate::template_env::render(
554 "php_visitor_method_result_match.jinja",
555 context! {
556 ret_ty => &ret_ty,
557 tmpl_vars_expr => &tmpl_vars_expr,
558 },
559 ));
560 out.push('\n');
561}
562
563#[allow(clippy::too_many_arguments)]
566pub fn gen_bridge_function(
567 func: &alef_core::ir::FunctionDef,
568 bridge_param_idx: usize,
569 bridge_cfg: &TraitBridgeConfig,
570 mapper: &dyn alef_codegen::type_mapper::TypeMapper,
571 opaque_types: &ahash::AHashSet<String>,
572 core_import: &str,
573 handle_path: &str,
574) -> String {
575 use alef_core::ir::TypeRef;
576
577 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
578 let param_name = &func.params[bridge_param_idx].name;
579 let bridge_param = &func.params[bridge_param_idx];
580 let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
581
582 let mut sig_parts = Vec::new();
584 for (idx, p) in func.params.iter().enumerate() {
585 if idx == bridge_param_idx {
586 let php_obj_ty = "&mut ext_php_rs::types::ZendObject";
590 if is_optional {
591 sig_parts.push(format!("{}: Option<{php_obj_ty}>", p.name));
592 } else {
593 sig_parts.push(format!("{}: {php_obj_ty}", p.name));
594 }
595 } else {
596 let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
597 let base = mapper.map_type(&p.ty);
598 let ty = match &p.ty {
601 TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
602 if p.optional || promoted {
603 format!("Option<&mut {base}>")
604 } else {
605 format!("&mut {base}")
606 }
607 }
608 TypeRef::Optional(inner) => {
609 if let TypeRef::Named(n) = inner.as_ref() {
610 if !opaque_types.contains(n.as_str()) {
611 format!("Option<&mut {base}>")
612 } else if p.optional || promoted {
613 format!("Option<{base}>")
614 } else {
615 base
616 }
617 } else if p.optional || promoted {
618 format!("Option<{base}>")
619 } else {
620 base
621 }
622 }
623 _ => {
624 if p.optional || promoted {
625 format!("Option<{base}>")
626 } else {
627 base
628 }
629 }
630 };
631 sig_parts.push(format!("{}: {}", p.name, ty));
632 }
633 }
634
635 let params_str = sig_parts.join(", ");
636 let return_type = mapper.map_type(&func.return_type);
637 let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
638
639 let err_conv = ".map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))";
640
641 let bridge_wrap = if is_optional {
643 format!(
644 "let {param_name} = {param_name}.map(|v| {{\n \
645 let bridge = {struct_name}::new(v);\n \
646 std::sync::Arc::new(std::sync::Mutex::new(bridge)) as {handle_path}\n \
647 }});"
648 )
649 } else {
650 format!(
651 "let {param_name} = {{\n \
652 let bridge = {struct_name}::new({param_name});\n \
653 std::sync::Arc::new(std::sync::Mutex::new(bridge)) as {handle_path}\n \
654 }};"
655 )
656 };
657
658 let serde_bindings: String = func
660 .params
661 .iter()
662 .enumerate()
663 .filter(|(idx, p)| {
664 if *idx == bridge_param_idx {
665 return false;
666 }
667 let named = match &p.ty {
668 TypeRef::Named(n) => Some(n.as_str()),
669 TypeRef::Optional(inner) => {
670 if let TypeRef::Named(n) = inner.as_ref() {
671 Some(n.as_str())
672 } else {
673 None
674 }
675 }
676 _ => None,
677 };
678 named.is_some_and(|n| !opaque_types.contains(n))
679 })
680 .map(|(_, p)| {
681 let name = &p.name;
682 let core_path = format!(
683 "{core_import}::{}",
684 match &p.ty {
685 TypeRef::Named(n) => n.clone(),
686 TypeRef::Optional(inner) =>
687 if let TypeRef::Named(n) = inner.as_ref() {
688 n.clone()
689 } else {
690 String::new()
691 },
692 _ => String::new(),
693 }
694 );
695 if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
696 format!(
697 "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n \
698 let json = serde_json::to_string(&v){err_conv}?;\n \
699 serde_json::from_str(&json){err_conv}\n \
700 }}).transpose()?;\n "
701 )
702 } else {
703 format!(
704 "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n \
705 let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n "
706 )
707 }
708 })
709 .collect();
710
711 let call_args: Vec<String> = func
713 .params
714 .iter()
715 .enumerate()
716 .map(|(idx, p)| {
717 if idx == bridge_param_idx {
718 return p.name.clone();
719 }
720 match &p.ty {
721 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
722 if p.optional {
723 format!("{}.as_ref().map(|v| &v.inner)", p.name)
724 } else {
725 format!("&{}.inner", p.name)
726 }
727 }
728 TypeRef::Named(_) => format!("{}_core", p.name),
729 TypeRef::Optional(inner) => {
730 if let TypeRef::Named(n) = inner.as_ref() {
731 if opaque_types.contains(n.as_str()) {
732 format!("{}.as_ref().map(|v| &v.inner)", p.name)
733 } else {
734 format!("{}_core", p.name)
735 }
736 } else {
737 p.name.clone()
738 }
739 }
740 TypeRef::String | TypeRef::Char => {
741 if p.is_ref {
742 format!("&{}", p.name)
743 } else {
744 p.name.clone()
745 }
746 }
747 _ => p.name.clone(),
748 }
749 })
750 .collect();
751 let call_args_str = call_args.join(", ");
752
753 let core_fn_path = {
754 let path = func.rust_path.replace('-', "_");
755 if path.starts_with(core_import) {
756 path
757 } else {
758 format!("{core_import}::{}", func.name)
759 }
760 };
761 let core_call = format!("{core_fn_path}({call_args_str})");
762
763 let return_wrap = match &func.return_type {
764 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
765 format!("{name} {{ inner: std::sync::Arc::new(val) }}")
766 }
767 TypeRef::Named(_) => "val.into()".to_string(),
768 TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
769 _ => "val".to_string(),
770 };
771
772 let body = if func.error_type.is_some() {
773 if return_wrap == "val" {
774 format!("{bridge_wrap}\n {serde_bindings}{core_call}{err_conv}")
775 } else {
776 format!("{bridge_wrap}\n {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
777 }
778 } else {
779 format!("{bridge_wrap}\n {serde_bindings}{core_call}")
780 };
781
782 let func_name = &func.name;
783 let mut out = String::with_capacity(1024);
784 if func.error_type.is_some() {
785 out.push_str("#[allow(clippy::missing_errors_doc)]\n");
786 }
787 out.push_str(&crate::template_env::render(
788 "php_bridge_function_definition.jinja",
789 context! {
790 func_name => func_name,
791 params_str => ¶ms_str,
792 ret => &ret,
793 body => &body,
794 },
795 ));
796
797 out
798}
799
800fn rust_type_to_php_type(ty: &TypeRef, _is_ref: bool, optional: bool, _type_paths: &HashMap<String, String>) -> String {
803 if matches!(ty, TypeRef::String) {
805 if optional {
806 return "?string".to_string();
807 }
808 return "string".to_string();
809 }
810
811 if matches!(ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
813 if optional {
814 return "?bool".to_string();
815 }
816 return "bool".to_string();
817 }
818
819 if let TypeRef::Primitive(prim) = ty {
821 match prim {
822 alef_core::ir::PrimitiveType::I32
823 | alef_core::ir::PrimitiveType::I64
824 | alef_core::ir::PrimitiveType::U32
825 | alef_core::ir::PrimitiveType::U64
826 | alef_core::ir::PrimitiveType::Usize => {
827 if optional {
828 return "?int".to_string();
829 }
830 return "int".to_string();
831 }
832 alef_core::ir::PrimitiveType::F32 | alef_core::ir::PrimitiveType::F64 => {
833 if optional {
834 return "?float".to_string();
835 }
836 return "float".to_string();
837 }
838 _ => {}
839 }
840 }
841
842 if optional {
844 "?mixed".to_string()
845 } else {
846 "mixed".to_string()
847 }
848}
849
850pub fn gen_visitor_interface(
853 trait_type: &TypeDef,
854 bridge_cfg: &TraitBridgeConfig,
855 namespace: &str,
856 type_paths: &HashMap<String, String>,
857) -> String {
858 let interface_name = format!("{}Interface", bridge_cfg.trait_name);
859 let mut out = String::with_capacity(2048);
860
861 out.push_str("<?php\n\n");
863 out.push_str("declare(strict_types=1);\n\n");
864 out.push_str(&format!("namespace {namespace};\n\n"));
865
866 out.push_str(&crate::template_env::render(
868 "php_visitor_interface_start.jinja",
869 context! {
870 interface_name => &interface_name,
871 },
872 ));
873 out.push('\n');
874
875 for method in &trait_type.methods {
877 if method.trait_source.is_some() {
878 continue;
879 }
880
881 let name = &method.name;
882
883 let mut method_params_parts = Vec::new();
885 let mut param_docs = Vec::new();
886
887 for p in &method.params {
888 let is_ctx_param = match &p.ty {
890 TypeRef::Named(n) => Some(n.as_str()) == bridge_cfg.context_type.as_deref(),
891 _ => false,
892 };
893 if is_ctx_param {
894 continue;
895 }
896
897 let php_type = rust_type_to_php_type(&p.ty, p.is_ref, p.optional, type_paths);
899 method_params_parts.push(format!("{} ${}", php_type, p.name));
900
901 let doc = format!(" * @param {} ${}", php_type, p.name);
902 param_docs.push(doc);
903 }
904
905 let method_params = if method_params_parts.is_empty() {
906 String::new()
907 } else {
908 format!(", {}", method_params_parts.join(", "))
909 };
910
911 let param_docs_str = if param_docs.is_empty() {
912 String::new()
913 } else {
914 format!("\n{}", param_docs.join("\n"))
915 };
916
917 let doc_lines = if !method.doc.is_empty() {
919 let sanitized = sanitize_rust_idioms(&method.doc, DocTarget::PhpDoc);
920 sanitized.lines().next().unwrap_or("").to_string()
921 } else {
922 format!("Handle for {} callback", name)
923 };
924
925 out.push_str(&crate::template_env::render(
926 "php_visitor_interface_method.jinja",
927 context! {
928 method_name => name,
929 method_params => &method_params,
930 doc_lines => &doc_lines,
931 param_docs => ¶m_docs_str,
932 },
933 ));
934 out.push('\n');
935 }
936
937 out.push_str("}\n");
938
939 out
940}