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