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 .chain(
266 api.excluded_type_paths
267 .iter()
268 .map(|(name, path)| (name.clone(), path.replace('-', "_"))),
269 )
270 .collect();
271
272 let is_visitor_bridge = bridge_cfg.type_alias.is_some()
274 && bridge_cfg.register_fn.is_none()
275 && bridge_cfg.super_trait.is_none()
276 && trait_type.methods.iter().all(|m| m.has_default_impl);
277
278 if is_visitor_bridge {
279 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
280 let trait_path = trait_type.rust_path.replace('-', "_");
281 let code = gen_visitor_bridge(trait_type, bridge_cfg, &struct_name, &trait_path, &type_paths);
282 BridgeOutput { imports: vec![], code }
283 } else {
284 let generator = PhpBridgeGenerator {
286 core_import: core_import.to_string(),
287 type_paths: type_paths.clone(),
288 error_type: error_type.to_string(),
289 };
290 let spec = TraitBridgeSpec {
291 trait_def: trait_type,
292 bridge_config: bridge_cfg,
293 core_import,
294 wrapper_prefix: "Php",
295 type_paths,
296 error_type: error_type.to_string(),
297 error_constructor: error_constructor.to_string(),
298 };
299 gen_bridge_all(&spec, &generator)
300 }
301}
302
303fn gen_visitor_bridge(
308 trait_type: &TypeDef,
309 _bridge_cfg: &TraitBridgeConfig,
310 struct_name: &str,
311 trait_path: &str,
312 type_paths: &HashMap<String, String>,
313) -> String {
314 let mut out = String::with_capacity(4096);
315 let core_crate = trait_path
316 .split("::")
317 .next()
318 .filter(|s| !s.is_empty())
319 .unwrap_or_else(|| panic!("trait_path '{trait_path}' must be a qualified path of the form 'crate_name::...'; configure extension_name in alef.toml"))
320 .to_string();
321
322 out.push_str(&crate::template_env::render(
324 "visitor_nodecontext_helper.jinja",
325 context! {
326 core_crate => &core_crate,
327 },
328 ));
329 out.push('\n');
330
331 out.push_str(&crate::template_env::render(
333 "visitor_zval_to_visitresult.jinja",
334 context! {
335 core_crate => &core_crate,
336 },
337 ));
338 out.push('\n');
339
340 out.push_str(&crate::template_env::render(
342 "php_visit_result_with_template.jinja",
343 context! {
344 core_crate => &core_crate,
345 },
346 ));
347 out.push_str("\n\n");
348
349 out.push_str(&crate::template_env::render(
351 "visitor_bridge_struct.jinja",
352 context! {
353 struct_name => struct_name,
354 },
355 ));
356 out.push('\n');
357
358 out.push_str(&crate::template_env::render(
360 "php_trait_impl_start.jinja",
361 context! {
362 trait_path => &trait_path,
363 struct_name => struct_name,
364 },
365 ));
366 for method in &trait_type.methods {
367 if method.trait_source.is_some() {
368 continue;
369 }
370 gen_visitor_method_php(&mut out, method, type_paths);
371 }
372 out.push_str("}\n");
373 out.push('\n');
374
375 out
376}
377
378fn gen_visitor_method_php(out: &mut String, method: &MethodDef, type_paths: &HashMap<String, String>) {
380 let name = &method.name;
381
382 let mut sig_parts = vec!["&mut self".to_string()];
383 for p in &method.params {
384 let ty_str = visitor_param_type(&p.ty, p.is_ref, p.optional, type_paths);
385 sig_parts.push(format!("{}: {}", p.name, ty_str));
386 }
387 let sig = sig_parts.join(", ");
388
389 let ret_ty = match &method.return_type {
390 TypeRef::Named(n) => type_paths.get(n).cloned().unwrap_or_else(|| n.clone()),
391 other => param_type(other, "", false, type_paths),
392 };
393
394 out.push_str(&crate::template_env::render(
395 "php_visitor_method_signature.jinja",
396 context! {
397 name => name,
398 sig => &sig,
399 ret_ty => &ret_ty,
400 },
401 ));
402
403 out.push_str(" // SAFETY: php_obj is a valid ZendObject pointer for the duration of this call.\n");
405 out.push_str(" let php_obj_ref = unsafe { &mut *self.php_obj };\n");
406
407 let has_args = !method.params.is_empty();
409 if has_args {
410 out.push_str(" let mut args: Vec<ext_php_rs::types::Zval> = Vec::new();\n");
411 for p in &method.params {
412 if let TypeRef::Named(n) = &p.ty {
413 if n == "NodeContext" {
414 out.push_str(&crate::template_env::render(
415 "php_visitor_arg_nodecontext.jinja",
416 context! {
417 name => &p.name,
418 ref => if p.is_ref { "" } else { "&" },
419 },
420 ));
421 out.push('\n');
422 continue;
423 }
424 }
425 if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
428 out.push_str(&crate::template_env::render(
429 "php_visitor_arg_optional_string_ref.jinja",
430 context! {
431 name => &p.name,
432 },
433 ));
434 out.push('\n');
435 continue;
436 }
437 if matches!(&p.ty, TypeRef::String) {
438 if p.is_ref {
439 out.push_str(&crate::template_env::render(
440 "php_visitor_arg_string_ref.jinja",
441 context! {
442 name => &p.name,
443 },
444 ));
445 } else {
446 out.push_str(&crate::template_env::render(
447 "php_visitor_arg_string_owned.jinja",
448 context! {
449 name => &p.name,
450 },
451 ));
452 }
453 out.push('\n');
454 continue;
455 }
456 if matches!(&p.ty, TypeRef::Primitive(alef_core::ir::PrimitiveType::Bool)) {
457 out.push_str(&crate::template_env::render(
458 "php_visitor_arg_bool.jinja",
459 context! {
460 name => &p.name,
461 },
462 ));
463 out.push('\n');
464 continue;
465 }
466 out.push_str(&crate::template_env::render(
468 "php_visitor_arg_default.jinja",
469 context! {
470 name => &p.name,
471 },
472 ));
473 out.push('\n');
474 }
475 }
476
477 if has_args {
481 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");
482 }
483 let args_expr = if has_args { "dyn_args" } else { "vec![]" };
484 out.push_str(&crate::template_env::render(
485 "php_visitor_method_php_call.jinja",
486 context! {
487 name => name,
488 args_expr => args_expr,
489 },
490 ));
491
492 let mut tmpl_var_names: Vec<String> = Vec::new();
495 for p in &method.params {
496 if let TypeRef::Named(n) = &p.ty {
497 if n == "NodeContext" {
498 continue;
499 }
500 }
501 if matches!(&p.ty, TypeRef::Vec(_)) {
503 continue;
504 }
505 let key = p.name.strip_prefix('_').unwrap_or(&p.name);
507 let owned_var = format!("_{key}_s");
508 let expr: String = if p.optional && matches!(&p.ty, TypeRef::String) && p.is_ref {
509 format!("{}.map(|s| s.to_string()).unwrap_or_default()", p.name)
510 } else if matches!(&p.ty, TypeRef::String) && p.is_ref {
511 format!("{}.to_string()", p.name)
512 } else if matches!(&p.ty, TypeRef::String) {
513 format!("{}.clone()", p.name)
514 } else if matches!(&p.ty, TypeRef::Optional(_)) {
515 format!("{}.map(|v| v.to_string()).unwrap_or_default()", p.name)
516 } else {
517 format!("{}.to_string()", p.name)
518 };
519 out.push_str(&crate::template_env::render(
520 "php_visitor_template_var_let_binding.jinja",
521 context! {
522 owned_var => &owned_var,
523 expr => &expr,
524 },
525 ));
526 out.push('\n');
527 tmpl_var_names.push(format!("(\"{key}\", {owned_var}.as_str())"));
528 }
529 let tmpl_vars_expr = if tmpl_var_names.is_empty() {
530 "&[]".to_string()
531 } else {
532 format!("&[{}]", tmpl_var_names.join(", "))
533 };
534
535 out.push_str(&crate::template_env::render(
537 "php_visitor_method_result_match.jinja",
538 context! {
539 ret_ty => &ret_ty,
540 tmpl_vars_expr => &tmpl_vars_expr,
541 },
542 ));
543 out.push('\n');
544}
545
546#[allow(clippy::too_many_arguments)]
549pub fn gen_bridge_function(
550 func: &alef_core::ir::FunctionDef,
551 bridge_param_idx: usize,
552 bridge_cfg: &TraitBridgeConfig,
553 mapper: &dyn alef_codegen::type_mapper::TypeMapper,
554 opaque_types: &ahash::AHashSet<String>,
555 core_import: &str,
556) -> String {
557 use alef_core::ir::TypeRef;
558
559 let struct_name = format!("Php{}Bridge", bridge_cfg.trait_name);
560 let handle_path = format!("{core_import}::visitor::VisitorHandle");
561 let param_name = &func.params[bridge_param_idx].name;
562 let bridge_param = &func.params[bridge_param_idx];
563 let is_optional = bridge_param.optional || matches!(&bridge_param.ty, TypeRef::Optional(_));
564
565 let mut sig_parts = Vec::new();
567 for (idx, p) in func.params.iter().enumerate() {
568 if idx == bridge_param_idx {
569 let php_obj_ty = "&mut ext_php_rs::types::ZendObject";
573 if is_optional {
574 sig_parts.push(format!("{}: Option<{php_obj_ty}>", p.name));
575 } else {
576 sig_parts.push(format!("{}: {php_obj_ty}", p.name));
577 }
578 } else {
579 let promoted = idx > bridge_param_idx || func.params[..idx].iter().any(|pp| pp.optional);
580 let base = mapper.map_type(&p.ty);
581 let ty = match &p.ty {
584 TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => {
585 if p.optional || promoted {
586 format!("Option<&mut {base}>")
587 } else {
588 format!("&mut {base}")
589 }
590 }
591 TypeRef::Optional(inner) => {
592 if let TypeRef::Named(n) = inner.as_ref() {
593 if !opaque_types.contains(n.as_str()) {
594 format!("Option<&mut {base}>")
595 } else if p.optional || promoted {
596 format!("Option<{base}>")
597 } else {
598 base
599 }
600 } else if p.optional || promoted {
601 format!("Option<{base}>")
602 } else {
603 base
604 }
605 }
606 _ => {
607 if p.optional || promoted {
608 format!("Option<{base}>")
609 } else {
610 base
611 }
612 }
613 };
614 sig_parts.push(format!("{}: {}", p.name, ty));
615 }
616 }
617
618 let params_str = sig_parts.join(", ");
619 let return_type = mapper.map_type(&func.return_type);
620 let ret = mapper.wrap_return(&return_type, func.error_type.is_some());
621
622 let err_conv = ".map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()))";
623
624 let bridge_wrap = if is_optional {
626 format!(
627 "let {param_name} = {param_name}.map(|v| {{\n \
628 let bridge = {struct_name}::new(v);\n \
629 std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n \
630 }});"
631 )
632 } else {
633 format!(
634 "let {param_name} = {{\n \
635 let bridge = {struct_name}::new({param_name});\n \
636 std::rc::Rc::new(std::cell::RefCell::new(bridge)) as {handle_path}\n \
637 }};"
638 )
639 };
640
641 let serde_bindings: String = func
643 .params
644 .iter()
645 .enumerate()
646 .filter(|(idx, p)| {
647 if *idx == bridge_param_idx {
648 return false;
649 }
650 let named = match &p.ty {
651 TypeRef::Named(n) => Some(n.as_str()),
652 TypeRef::Optional(inner) => {
653 if let TypeRef::Named(n) = inner.as_ref() {
654 Some(n.as_str())
655 } else {
656 None
657 }
658 }
659 _ => None,
660 };
661 named.is_some_and(|n| !opaque_types.contains(n))
662 })
663 .map(|(_, p)| {
664 let name = &p.name;
665 let core_path = format!(
666 "{core_import}::{}",
667 match &p.ty {
668 TypeRef::Named(n) => n.clone(),
669 TypeRef::Optional(inner) =>
670 if let TypeRef::Named(n) = inner.as_ref() {
671 n.clone()
672 } else {
673 String::new()
674 },
675 _ => String::new(),
676 }
677 );
678 if p.optional || matches!(&p.ty, TypeRef::Optional(_)) {
679 format!(
680 "let {name}_core: Option<{core_path}> = {name}.map(|v| {{\n \
681 let json = serde_json::to_string(&v){err_conv}?;\n \
682 serde_json::from_str(&json){err_conv}\n \
683 }}).transpose()?;\n "
684 )
685 } else {
686 format!(
687 "let {name}_json = serde_json::to_string(&{name}){err_conv}?;\n \
688 let {name}_core: {core_path} = serde_json::from_str(&{name}_json){err_conv}?;\n "
689 )
690 }
691 })
692 .collect();
693
694 let call_args: Vec<String> = func
696 .params
697 .iter()
698 .enumerate()
699 .map(|(idx, p)| {
700 if idx == bridge_param_idx {
701 return p.name.clone();
702 }
703 match &p.ty {
704 TypeRef::Named(n) if opaque_types.contains(n.as_str()) => {
705 if p.optional {
706 format!("{}.as_ref().map(|v| &v.inner)", p.name)
707 } else {
708 format!("&{}.inner", p.name)
709 }
710 }
711 TypeRef::Named(_) => format!("{}_core", p.name),
712 TypeRef::Optional(inner) => {
713 if let TypeRef::Named(n) = inner.as_ref() {
714 if opaque_types.contains(n.as_str()) {
715 format!("{}.as_ref().map(|v| &v.inner)", p.name)
716 } else {
717 format!("{}_core", p.name)
718 }
719 } else {
720 p.name.clone()
721 }
722 }
723 TypeRef::String | TypeRef::Char => {
724 if p.is_ref {
725 format!("&{}", p.name)
726 } else {
727 p.name.clone()
728 }
729 }
730 _ => p.name.clone(),
731 }
732 })
733 .collect();
734 let call_args_str = call_args.join(", ");
735
736 let core_fn_path = {
737 let path = func.rust_path.replace('-', "_");
738 if path.starts_with(core_import) {
739 path
740 } else {
741 format!("{core_import}::{}", func.name)
742 }
743 };
744 let core_call = format!("{core_fn_path}({call_args_str})");
745
746 let return_wrap = match &func.return_type {
747 TypeRef::Named(name) if opaque_types.contains(name.as_str()) => {
748 format!("{name} {{ inner: std::sync::Arc::new(val) }}")
749 }
750 TypeRef::Named(_) => "val.into()".to_string(),
751 TypeRef::String | TypeRef::Bytes => "val.into()".to_string(),
752 _ => "val".to_string(),
753 };
754
755 let body = if func.error_type.is_some() {
756 if return_wrap == "val" {
757 format!("{bridge_wrap}\n {serde_bindings}{core_call}{err_conv}")
758 } else {
759 format!("{bridge_wrap}\n {serde_bindings}{core_call}.map(|val| {return_wrap}){err_conv}")
760 }
761 } else {
762 format!("{bridge_wrap}\n {serde_bindings}{core_call}")
763 };
764
765 let func_name = &func.name;
766 let mut out = String::with_capacity(1024);
767 if func.error_type.is_some() {
768 out.push_str("#[allow(clippy::missing_errors_doc)]\n");
769 }
770 out.push_str(&crate::template_env::render(
771 "php_bridge_function_definition.jinja",
772 context! {
773 func_name => func_name,
774 params_str => ¶ms_str,
775 ret => &ret,
776 body => &body,
777 },
778 ));
779
780 out
781}