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
87 crate::template_env::render(
88 "sync_method_body.jinja",
89 context! {
90 method_name => name,
91 args_expr => args_expr,
92 is_result_type => is_result_type,
93 is_unit_return => is_unit_return,
94 core_import => &self.core_import,
95 },
96 )
97 }
98
99 fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String {
100 let name = &method.name;
101
102 let string_params: Vec<String> = method
103 .params
104 .iter()
105 .filter(|p| matches!(&p.ty, TypeRef::String))
106 .map(|p| p.name.clone())
107 .collect();
108
109 let has_args = !method.params.is_empty();
110 let args_expr = if has_args {
111 let mut args_parts = Vec::new();
112 for p in &method.params {
113 let arg_expr = match &p.ty {
114 TypeRef::String => format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name),
115 TypeRef::Path => format!(
116 "ext_php_rs::types::Zval::try_from({}.to_string_lossy().to_string()).unwrap_or_default()",
117 p.name
118 ),
119 TypeRef::Bytes => format!(
120 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
121 p.name
122 ),
123 TypeRef::Named(_) => {
124 format!(
125 "ext_php_rs::types::Zval::try_from(serde_json::to_string(&{}).unwrap_or_default()).unwrap_or_default()",
126 p.name
127 )
128 }
129 TypeRef::Primitive(_) => {
130 format!("ext_php_rs::types::Zval::try_from({}).unwrap_or_default()", p.name)
131 }
132 _ => format!(
133 "ext_php_rs::types::Zval::try_from(format!(\"{{:?}}\", {})).unwrap_or_default()",
134 p.name
135 ),
136 };
137 args_parts.push(arg_expr);
138 }
139 let args_array = format!("[{}]", args_parts.join(", "));
140 format!(
141 "{}.iter().map(|z| z as &dyn ext_php_rs::convert::IntoZvalDyn).collect()",
142 args_array
143 )
144 } else {
145 "vec![]".to_string()
146 };
147
148 let is_result_type = method.error_type.is_some();
149
150 crate::template_env::render(
151 "async_method_body.jinja",
152 context! {
153 method_name => name,
154 args_expr => args_expr,
155 string_params => string_params,
156 is_result_type => is_result_type,
157 core_import => &spec.core_import,
158 },
159 )
160 }
161
162 fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
163 let wrapper = spec.wrapper_name();
164
165 crate::template_env::render(
166 "bridge_constructor.jinja",
167 context! {
168 wrapper => &wrapper,
169 },
170 )
171 }
172
173 fn gen_unregistration_fn(&self, spec: &TraitBridgeSpec) -> String {
174 let Some(unregister_fn) = spec.bridge_config.unregister_fn.as_deref() else {
175 return String::new();
176 };
177 let host_path = alef_codegen::generators::trait_bridge::host_function_path(spec, unregister_fn);
178
179 crate::template_env::render(
180 "bridge_unregister_fn.jinja",
181 context! {
182 unregister_fn => unregister_fn,
183 host_path => &host_path,
184 },
185 )
186 }
187
188 fn gen_clear_fn(&self, spec: &TraitBridgeSpec) -> String {
189 let Some(clear_fn) = spec.bridge_config.clear_fn.as_deref() else {
190 return String::new();
191 };
192 let host_path = alef_codegen::generators::trait_bridge::host_function_path(spec, clear_fn);
193
194 crate::template_env::render(
195 "bridge_clear_fn.jinja",
196 context! {
197 clear_fn => clear_fn,
198 host_path => &host_path,
199 },
200 )
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 req_methods: Vec<&MethodDef> = spec.required_methods();
214 let required_methods: Vec<minijinja::Value> = req_methods
215 .iter()
216 .map(|m| {
217 minijinja::context! {
218 name => m.name.as_str(),
219 }
220 })
221 .collect();
222
223 let extra_args = spec
224 .bridge_config
225 .register_extra_args
226 .as_deref()
227 .map(|a| format!(", {a}"))
228 .unwrap_or_default();
229
230 crate::template_env::render(
231 "bridge_registration_fn.jinja",
232 context! {
233 register_fn => register_fn,
234 required_methods => required_methods,
235 wrapper => &wrapper,
236 trait_path => &trait_path,
237 registry_getter => registry_getter,
238 extra_args => &extra_args,
239 },
240 )
241 }
242}
243
244pub fn gen_trait_bridge(
246 trait_type: &TypeDef,
247 bridge_cfg: &TraitBridgeConfig,
248 core_import: &str,
249 error_type: &str,
250 error_constructor: &str,
251 api: &ApiSurface,
252) -> BridgeOutput {
253 let type_paths: HashMap<String, String> = api
255 .types
256 .iter()
257 .map(|t| (t.name.clone(), t.rust_path.replace('-', "_")))
258 .chain(
259 api.enums
260 .iter()
261 .map(|e| (e.name.clone(), e.rust_path.replace('-', "_"))),
262 )
263 .collect();
264
265 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
267 && bridge_cfg.register_fn.is_none()
268 && bridge_cfg.super_trait.is_none()
269 && trait_type.methods.iter().all(|m| m.has_default_impl);
270
271 if is_visitor_bridge {
272 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
273 let trait_path = trait_type.rust_path.replace('-', "_");
274 let code = gen_visitor_bridge(trait_type, bridge_cfg, &struct_name, &trait_path, &type_paths);
275 BridgeOutput { imports: vec![], code }
276 } else {
277 let generator = PhpBridgeGenerator {
279 core_import: core_import.to_string(),
280 type_paths: type_paths.clone(),
281 error_type: error_type.to_string(),
282 };
283 let spec = TraitBridgeSpec {
284 trait_def: trait_type,
285 bridge_config: bridge_cfg,
286 core_import,
287 wrapper_prefix: "Php",
288 type_paths,
289 error_type: error_type.to_string(),
290 error_constructor: error_constructor.to_string(),
291 };
292 gen_bridge_all(&spec, &generator)
293 }
294}
295
296fn gen_visitor_bridge(
301 trait_type: &TypeDef,
302 _bridge_cfg: &TraitBridgeConfig,
303 struct_name: &str,
304 trait_path: &str,
305 type_paths: &HashMap<String, String>,
306) -> String {
307 let mut out = String::with_capacity(4096);
308 let core_crate = trait_path
309 .split("::")
310 .next()
311 .filter(|s| !s.is_empty())
312 .unwrap_or_else(|| panic!("trait_path '{trait_path}' must be a qualified path of the form 'crate_name::...'; configure extension_name in alef.toml"))
313 .to_string();
314
315 out.push_str(&crate::template_env::render(
317 "visitor_nodecontext_helper.jinja",
318 context! {
319 core_crate => &core_crate,
320 },
321 ));
322 out.push('\n');
323
324 out.push_str(&crate::template_env::render(
326 "visitor_zval_to_visitresult.jinja",
327 context! {
328 core_crate => &core_crate,
329 },
330 ));
331 out.push('\n');
332
333 out.push_str(&crate::template_env::render(
335 "php_visit_result_with_template.jinja",
336 context! {
337 core_crate => &core_crate,
338 },
339 ));
340 out.push_str("\n\n");
341
342 out.push_str(&crate::template_env::render(
344 "visitor_bridge_struct.jinja",
345 context! {
346 struct_name => struct_name,
347 },
348 ));
349 out.push('\n');
350
351 out.push_str(&crate::template_env::render(
353 "php_trait_impl_start.jinja",
354 context! {
355 trait_path => &trait_path,
356 struct_name => struct_name,
357 },
358 ));
359 for method in &trait_type.methods {
360 if method.trait_source.is_some() {
361 continue;
362 }
363 gen_visitor_method_php(&mut out, method, type_paths);
364 }
365 out.push_str("}\n");
366 out.push('\n');
367
368 out
369}
370
371fn gen_visitor_method_php(out: &mut String, method: &MethodDef, type_paths: &HashMap<String, String>) {
373 let name = &method.name;
374
375 let mut sig_parts = vec!["&mut self".to_string()];
376 for p in &method.params {
377 let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
378 sig_parts.push(format!("{}: {}", p.name, ty_str));
379 }
380 let sig = sig_parts.join(", ");
381
382 let ret_ty = match &method.return_type {
383 TypeRef::Named(n) => type_paths.get(n).cloned().unwrap_or_else(|| n.clone()),
384 other => param_type(other, "", false, type_paths),
385 };
386
387 out.push_str(&crate::template_env::render(
388 "php_visitor_method_signature.jinja",
389 context! {
390 name => name,
391 sig => &sig,
392 ret_ty => &ret_ty,
393 },
394 ));
395
396 out.push_str(" // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n");
398 out.push_str(" let php_obj_ref = unsafe { &mut *self.php_obj };\n");
399
400 let has_args = !method.params.is_empty();
402 if has_args {
403 out.push_str(" let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n");
404 for p in &method.params {
405 if let TypeRef::Named(n) = &p.ty {
406 if n == "NodeContext" {
407 out.push_str(&crate::template_env::render(
408 "php_visitor_arg_nodecontext.jinja",
409 context! {
410 name => &p.name,
411 ref => if p.is_ref { "" } else { "&" },
412 },
413 ));
414 out.push('\n');
415 continue;
416 }
417 }
418 if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
421 out.push_str(&crate::template_env::render(
422 "php_visitor_arg_optional_string_ref.jinja",
423 context! {
424 name => &p.name,
425 },
426 ));
427 out.push('\n');
428 continue;
429 }
430 if matches!(&p.ty, TypeRef::String) {
431 if p.is_ref {
432 out.push_str(&crate::template_env::render(
433 "php_visitor_arg_string_ref.jinja",
434 context! {
435 name => &p.name,
436 },
437 ));
438 } else {
439 out.push_str(&crate::template_env::render(
440 "php_visitor_arg_string_owned.jinja",
441 context! {
442 name => &p.name,
443 },
444 ));
445 }
446 out.push('\n');
447 continue;
448 }
449 if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
450 out.push_str(&crate::template_env::render(
451 "php_visitor_arg_bool.jinja",
452 context! {
453 name => &p.name,
454 },
455 ));
456 out.push('\n');
457 continue;
458 }
459 out.push_str(&crate::template_env::render(
461 "php_visitor_arg_default.jinja",
462 context! {
463 name => &p.name,
464 },
465 ));
466 out.push('\n');
467 }
468 }
469
470 if has_args {
474 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");
475 }
476 let args_expr = if has_args { "dyn_args" } else { "vec![]" };
477 out.push_str(&crate::template_env::render(
478 "php_visitor_method_php_call.jinja",
479 context! {
480 name => name,
481 args_expr => args_expr,
482 },
483 ));
484
485 let mut tmpl_var_names: Vec<String> = Vec::new();
488 for p in &method.params {
489 if let TypeRef::Named(n) = &p.ty {
490 if n == "NodeContext" {
491 continue;
492 }
493 }
494 if matches!(&p.ty, TypeRef::Vec(_)) {
496 continue;
497 }
498 let key = p.name.strip_prefix('_').unwrap_or(&p.name);
500 let owned_var = format!("_{key}_s");
501 let expr: String = if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
502 format!("{}.map(|s| s.to_string()).unwrap_or_default()", p.name)
503 } else if matches!(&p.ty, TypeRef::String) && p.is_ref {
504 format!("{}.to_string()", p.name)
505 } else if matches!(&p.ty, TypeRef::String) {
506 format!("{}.clone()", p.name)
507 } else if matches!(&p.ty, TypeRef::Optional(_)) {
508 format!("{}.map(|v| v.to_string()).unwrap_or_default()", p.name)
509 } else {
510 format!("{}.to_string()", p.name)
511 };
512 out.push_str(&crate::template_env::render(
513 "php_visitor_template_var_let_binding.jinja",
514 context! {
515 owned_var => &owned_var,
516 expr => &expr,
517 },
518 ));
519 out.push('\n');
520 tmpl_var_names.push(format!("(\"{key}\", {owned_var}.as_str())"));
521 }
522 let tmpl_vars_expr = if tmpl_var_names.is_empty() {
523 "&[]".to_string()
524 } else {
525 format!("&[{}]", tmpl_var_names.join(", "))
526 };
527
528 out.push_str(&crate::template_env::render(
530 "php_visitor_method_result_match.jinja",
531 context! {
532 ret_ty => &ret_ty,
533 tmpl_vars_expr => &tmpl_vars_expr,
534 },
535 ));
536 out.push('\n');
537}
538
539#[allow(clippy::too_many_arguments)]
542pub fn gen_bridge_function(
543 func: &alef_core::ir::FunctionDef,
544 bridge_param_idx: usize,
545 bridge_cfg: &TraitBridgeConfig,
546 mapper: &dyn alef_codegen::type_mapper::TypeMapper,
547 opaque_types: &ahash::AHashSet<String>,
548 core_import: &str,
549) -> String {
550 use alef_core::ir::TypeRef;
551
552 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
553 let handle_path = format!("{core_import}::visitor::VisitorHandle");
554 let param_name = &func.params[bridge_param_idx].name;
555 let bridge_param = &func.params[bridge_param_idx];
556 let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
557
558 let mut sig_parts = Vec::new();
560 for (idx, p) in func.params.iter().enumerate() {
561 if idx == bridge_param_idx {
562 let php_obj_ty = "&mut ext_php_rs::types::ZendObject";
566 if is_optional {
567 sig_parts.push(format!("{}: Option<{php_obj_ty}>", p.name));
568 } else {
569 sig_parts.push(format!("{}: {php_obj_ty}", p.name));
570 }
571 } else {
572 let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
573 let base = mapper.map_type(&p.ty);
574 let ty = match &p.ty {
577 TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
578 if p.optional || promoted {
579 format!("Option<&mut {base}>")
580 } else {
581 format!("&mut {base}")
582 }
583 }
584 TypeRef::Optional(inner) => {
585 if let TypeRef::Named(n) = inner.as_ref() {
586 if !opaque_types.contains(n.as_str()) {
587 format!("Option<&mut {base}>")
588 } else if p.optional || promoted {
589 format!("Option<{base}>")
590 } else {
591 base
592 }
593 } else if p.optional || promoted {
594 format!("Option<{base}>")
595 } else {
596 base
597 }
598 }
599 _ => {
600 if p.optional || promoted {
601 format!("Option<{base}>")
602 } else {
603 base
604 }
605 }
606 };
607 sig_parts.push(format!("{}: {}", p.name, ty));
608 }
609 }
610
611 let params_str = sig_parts.join(", ");
612 let return_type = mapper.map_type(&func.return_type);
613 let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
614
615 let err_conv = ".map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))";
616
617 let bridge_wrap = if is_optional {
619 format!(
620 "let {param_name} = {param_name}.map(|v| {{\n \
621 let bridge = {struct_name}::new(v);\n \
622 std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n \
623 }});"
624 )
625 } else {
626 format!(
627 "let {param_name} = {{\n \
628 let bridge = {struct_name}::new({param_name});\n \
629 std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n \
630 }};"
631 )
632 };
633
634 let serde_bindings: String = func
636 .params
637 .iter()
638 .enumerate()
639 .filter(|(idx, p)| {
640 if *idx == bridge_param_idx {
641 return false;
642 }
643 let named = match &p.ty {
644 TypeRef::Named(n) => Some(n.as_str()),
645 TypeRef::Optional(inner) => {
646 if let TypeRef::Named(n) = inner.as_ref() {
647 Some(n.as_str())
648 } else {
649 None
650 }
651 }
652 _ => None,
653 };
654 named.is_some_and(|n| !opaque_types.contains(n))
655 })
656 .map(|(_, p)| {
657 let name = &p.name;
658 let core_path = format!(
659 "{core_import}::{}",
660 match &p.ty {
661 TypeRef::Named(n) => n.clone(),
662 TypeRef::Optional(inner) =>
663 if let TypeRef::Named(n) = inner.as_ref() {
664 n.clone()
665 } else {
666 String::new()
667 },
668 _ => String::new(),
669 }
670 );
671 if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
672 format!(
673 "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n \
674 let json = serde_json::to_string(&v){err_conv}?;\n \
675 serde_json::from_str(&json){err_conv}\n \
676 }}).transpose()?;\n "
677 )
678 } else {
679 format!(
680 "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n \
681 let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n "
682 )
683 }
684 })
685 .collect();
686
687 let call_args: Vec<String> = func
689 .params
690 .iter()
691 .enumerate()
692 .map(|(idx, p)| {
693 if idx == bridge_param_idx {
694 return p.name.clone();
695 }
696 match &p.ty {
697 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
698 if p.optional {
699 format!("{}.as_ref().map(|v| &v.inner)", p.name)
700 } else {
701 format!("&{}.inner", p.name)
702 }
703 }
704 TypeRef::Named(_) => format!("{}_core", p.name),
705 TypeRef::Optional(inner) => {
706 if let TypeRef::Named(n) = inner.as_ref() {
707 if opaque_types.contains(n.as_str()) {
708 format!("{}.as_ref().map(|v| &v.inner)", p.name)
709 } else {
710 format!("{}_core", p.name)
711 }
712 } else {
713 p.name.clone()
714 }
715 }
716 TypeRef::String | TypeRef::Char => {
717 if p.is_ref {
718 format!("&{}", p.name)
719 } else {
720 p.name.clone()
721 }
722 }
723 _ => p.name.clone(),
724 }
725 })
726 .collect();
727 let call_args_str = call_args.join(", ");
728
729 let core_fn_path = {
730 let path = func.rust_path.replace('-', "_");
731 if path.starts_with(core_import) {
732 path
733 } else {
734 format!("{core_import}::{}", func.name)
735 }
736 };
737 let core_call = format!("{core_fn_path}({call_args_str})");
738
739 let return_wrap = match &func.return_type {
740 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
741 format!("{name} {{ inner: std::sync::Arc::new(val) }}")
742 }
743 TypeRef::Named(_) => "val.into()".to_string(),
744 TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
745 _ => "val".to_string(),
746 };
747
748 let body = if func.error_type.is_some() {
749 if return_wrap == "val" {
750 format!("{bridge_wrap}\n {serde_bindings}{core_call}{err_conv}")
751 } else {
752 format!("{bridge_wrap}\n {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
753 }
754 } else {
755 format!("{bridge_wrap}\n {serde_bindings}{core_call}")
756 };
757
758 let func_name = &func.name;
759 let mut out = String::with_capacity(1024);
760 if func.error_type.is_some() {
761 out.push_str("#[allow(clippy::missing_errors_doc)]\n");
762 }
763 out.push_str(&crate::template_env::render(
764 "php_bridge_function_definition.jinja",
765 context! {
766 func_name => func_name,
767 params_str => ¶ms_str,
768 ret => &ret,
769 body => &body,
770 },
771 ));
772
773 out
774}