1mod functions;
2mod helpers;
3mod types;
4
5use crate::type_map::PhpMapper;
6use ahash::AHashSet;
7use alef_codegen::builder::RustFileBuilder;
8use alef_codegen::conversions::ConversionConfig;
9use alef_codegen::generators::RustBindingConfig;
10use alef_codegen::generators::{self, AsyncPattern};
11use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
12use alef_core::config::{AlefConfig, Language, detect_serde_available, resolve_output_dir};
13use alef_core::hash::{self, CommentStyle};
14use alef_core::ir::ApiSurface;
15use alef_core::ir::{PrimitiveType, TypeRef};
16use heck::{ToLowerCamelCase, ToPascalCase};
17use std::path::PathBuf;
18
19use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
20use helpers::{
21 gen_enum_tainted_from_binding_to_core, gen_serde_bridge_from, gen_tokio_runtime, has_enum_named_field,
22 references_named_type,
23};
24use types::{
25 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods,
26 gen_opaque_struct_methods, gen_php_struct, is_tagged_data_enum,
27};
28
29pub struct PhpBackend;
30
31impl PhpBackend {
32 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
33 RustBindingConfig {
34 struct_attrs: &["php_class"],
35 field_attrs: &[],
36 struct_derives: &["Clone"],
37 method_block_attr: Some("php_impl"),
38 constructor_attr: "",
39 static_attr: None,
40 function_attr: "#[php_function]",
41 enum_attrs: &[],
42 enum_derives: &[],
43 needs_signature: false,
44 signature_prefix: "",
45 signature_suffix: "",
46 core_import,
47 async_pattern: AsyncPattern::TokioBlockOn,
48 has_serde,
49 type_name_prefix: "",
50 option_duration_on_defaults: true,
51 opaque_type_names: &[],
52 }
53 }
54}
55
56impl Backend for PhpBackend {
57 fn name(&self) -> &str {
58 "php"
59 }
60
61 fn language(&self) -> Language {
62 Language::Php
63 }
64
65 fn capabilities(&self) -> Capabilities {
66 Capabilities {
67 supports_async: true,
68 supports_classes: true,
69 supports_enums: true,
70 supports_option: true,
71 supports_result: true,
72 ..Capabilities::default()
73 }
74 }
75
76 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
77 let data_enum_names: AHashSet<String> = api
79 .enums
80 .iter()
81 .filter(|e| is_tagged_data_enum(e))
82 .map(|e| e.name.clone())
83 .collect();
84 let enum_names: AHashSet<String> = api
85 .enums
86 .iter()
87 .filter(|e| !is_tagged_data_enum(e))
88 .map(|e| e.name.clone())
89 .collect();
90 let mapper = PhpMapper {
91 enum_names: enum_names.clone(),
92 data_enum_names: data_enum_names.clone(),
93 };
94 let core_import = config.core_import();
95
96 let php_config = config.php.as_ref();
98 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
99 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
100
101 let output_dir = resolve_output_dir(
102 config.output.php.as_ref(),
103 &config.crate_config.name,
104 "crates/{name}-php/src/",
105 );
106 let has_serde = detect_serde_available(&output_dir);
107 let cfg = Self::binding_config(&core_import, has_serde);
108
109 let mut builder = RustFileBuilder::new().with_generated_header();
111 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
112 builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions)");
113 builder.add_import("ext_php_rs::prelude::*");
114
115 if has_serde {
117 builder.add_import("serde_json");
118 }
119
120 for trait_path in generators::collect_trait_imports(api) {
122 builder.add_import(&trait_path);
123 }
124
125 let has_maps = api.types.iter().any(|t| {
127 t.fields
128 .iter()
129 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
130 }) || api
131 .functions
132 .iter()
133 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
134 if has_maps {
135 builder.add_import("std::collections::HashMap");
136 }
137
138 let custom_mods = config.custom_modules.for_language(Language::Php);
140 for module in custom_mods {
141 builder.add_item(&format!("pub mod {module};"));
142 }
143
144 let has_async =
146 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
147
148 if has_async {
149 builder.add_item(&gen_tokio_runtime());
150 }
151
152 let opaque_types: AHashSet<String> = api
154 .types
155 .iter()
156 .filter(|t| t.is_opaque)
157 .map(|t| t.name.clone())
158 .collect();
159 if !opaque_types.is_empty() {
160 builder.add_import("std::sync::Arc");
161 }
162 let opaque_type_names_vec: Vec<String> = {
166 let mut v: Vec<String> = opaque_types.iter().cloned().collect();
167 v.sort();
168 v
169 };
170
171 let extension_name = config.php_extension_name();
174 let php_namespace = config.php_autoload_namespace();
175
176 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
178
179 for adapter in &config.adapters {
181 match adapter.pattern {
182 alef_core::config::AdapterPattern::Streaming => {
183 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
184 if let Some(struct_code) = adapter_bodies.get(&key) {
185 builder.add_item(struct_code);
186 }
187 }
188 alef_core::config::AdapterPattern::CallbackBridge => {
189 let struct_key = format!("{}.__bridge_struct__", adapter.name);
190 let impl_key = format!("{}.__bridge_impl__", adapter.name);
191 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
192 builder.add_item(struct_code);
193 }
194 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
195 builder.add_item(impl_code);
196 }
197 }
198 _ => {}
199 }
200 }
201
202 for typ in api
203 .types
204 .iter()
205 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
206 {
207 if typ.is_opaque {
208 let ns_escaped = php_namespace.replace('\\', "\\\\");
212 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
213 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
214 let opaque_cfg = RustBindingConfig {
215 struct_attrs: &opaque_attr_arr,
216 ..cfg
217 };
218 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
219 builder.add_item(&gen_opaque_struct_methods(
220 typ,
221 &mapper,
222 &opaque_types,
223 &core_import,
224 &adapter_bodies,
225 ));
226 } else {
227 let struct_cfg = RustBindingConfig {
232 opaque_type_names: &opaque_type_names_vec,
233 ..cfg
234 };
235 builder.add_item(&gen_php_struct(
236 typ,
237 &mapper,
238 &struct_cfg,
239 Some(&php_namespace),
240 &enum_names,
241 ));
242 builder.add_item(&types::gen_struct_methods_with_exclude(
243 typ,
244 &mapper,
245 has_serde,
246 &core_import,
247 &opaque_types,
248 &enum_names,
249 &api.enums,
250 &exclude_functions,
251 ));
252 }
253 }
254
255 for enum_def in &api.enums {
256 if is_tagged_data_enum(enum_def) {
257 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
259 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
260 } else {
261 builder.add_item(&gen_enum_constants(enum_def));
262 }
263 }
264
265 let included_functions: Vec<_> = api
270 .functions
271 .iter()
272 .filter(|f| !exclude_functions.contains(&f.name))
273 .collect();
274 if !included_functions.is_empty() {
275 let facade_class_name = extension_name.to_pascal_case();
276 let mut method_items: Vec<String> = Vec::new();
279 for func in included_functions {
280 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
281 let bridge_field = crate::trait_bridge::find_bridge_field(func, &api.types, &config.trait_bridges);
282 if let Some((param_idx, bridge_cfg)) = bridge_param {
283 method_items.push(crate::trait_bridge::gen_bridge_function(
284 func,
285 param_idx,
286 bridge_cfg,
287 &mapper,
288 &opaque_types,
289 &core_import,
290 ));
291 } else if let Some(ref field_match) = bridge_field {
292 method_items.push(crate::trait_bridge::gen_bridge_field_function(
293 func,
294 field_match,
295 &mapper,
296 &opaque_types,
297 &core_import,
298 ));
299 } else if func.is_async {
300 method_items.push(gen_async_function_as_static_method(
301 func,
302 &mapper,
303 &opaque_types,
304 &core_import,
305 &config.trait_bridges,
306 ));
307 } else {
308 method_items.push(gen_function_as_static_method(
309 func,
310 &mapper,
311 &opaque_types,
312 &core_import,
313 &config.trait_bridges,
314 has_serde,
315 ));
316 }
317 }
318
319 let methods_joined = method_items
320 .iter()
321 .map(|m| {
322 m.lines()
324 .map(|l| {
325 if l.is_empty() {
326 String::new()
327 } else {
328 format!(" {l}")
329 }
330 })
331 .collect::<Vec<_>>()
332 .join("\n")
333 })
334 .collect::<Vec<_>>()
335 .join("\n\n");
336 let php_api_class_name = format!("{facade_class_name}Api");
339 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
341 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
342 let facade_struct = format!(
343 "#[php_class]\n#[{php_name_attr}]\npub struct {facade_class_name}Api;\n\n#[php_impl]\nimpl {facade_class_name}Api {{\n{methods_joined}\n}}"
344 );
345 builder.add_item(&facade_struct);
346
347 for bridge_cfg in &config.trait_bridges {
349 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
350 let bridge = crate::trait_bridge::gen_trait_bridge(
351 trait_type,
352 bridge_cfg,
353 &core_import,
354 &config.error_type(),
355 &config.error_constructor(),
356 api,
357 );
358 for imp in &bridge.imports {
359 builder.add_import(imp);
360 }
361 builder.add_item(&bridge.code);
362 }
363 }
364 }
365
366 let convertible = alef_codegen::conversions::convertible_types(api);
367 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
368 let input_types = alef_codegen::conversions::input_type_names(api);
369 let enum_names_ref = &mapper.enum_names;
374 let php_conv_config = ConversionConfig {
375 cast_large_ints_to_i64: true,
376 enum_string_names: Some(enum_names_ref),
377 json_to_string: true,
378 include_cfg_metadata: false,
379 option_duration_on_defaults: true,
380 ..Default::default()
381 };
382 let mut enum_tainted: AHashSet<String> = AHashSet::new();
384 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
385 if has_enum_named_field(typ, enum_names_ref) {
386 enum_tainted.insert(typ.name.clone());
387 }
388 }
389 let mut changed = true;
391 while changed {
392 changed = false;
393 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
394 if !enum_tainted.contains(&typ.name)
395 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
396 {
397 enum_tainted.insert(typ.name.clone());
398 changed = true;
399 }
400 }
401 }
402 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
403 if input_types.contains(&typ.name)
405 && !enum_tainted.contains(&typ.name)
406 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
407 {
408 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
409 typ,
410 &core_import,
411 &php_conv_config,
412 ));
413 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) && has_serde {
414 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
417 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
418 builder.add_item(&gen_enum_tainted_from_binding_to_core(
422 typ,
423 &core_import,
424 enum_names_ref,
425 &enum_tainted,
426 &php_conv_config,
427 &api.enums,
428 ));
429 }
430 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
432 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
433 typ,
434 &core_import,
435 &opaque_types,
436 &php_conv_config,
437 ));
438 }
439 }
440
441 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
443 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
444 }
445
446 for error in &api.errors {
448 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
449 }
450
451 let php_config = config.php.as_ref();
457 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
458
459 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
463 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
464 }
465
466 let mut class_registrations = String::new();
469 for typ in api
470 .types
471 .iter()
472 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
473 {
474 class_registrations.push_str(&format!("\n .class::<{}>()", typ.name));
475 }
476 if !api.functions.is_empty() {
478 let facade_class_name = extension_name.to_pascal_case();
479 class_registrations.push_str(&format!("\n .class::<{facade_class_name}Api>()"));
480 }
481 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
484 class_registrations.push_str(&format!("\n .class::<{}>()", enum_def.name));
485 }
486 builder.add_item(&format!(
487 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
488 ));
489
490 let content = builder.build();
491
492 Ok(vec![GeneratedFile {
493 path: PathBuf::from(&output_dir).join("lib.rs"),
494 content,
495 generated_header: false,
496 }])
497 }
498
499 fn generate_scaffold(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
500 if api.functions.is_empty() {
509 return Ok(vec![]);
510 }
511 let extension_name = config.php_extension_name();
512 let class_name = extension_name.to_pascal_case();
513 let namespace = config.php_autoload_namespace();
514
515 let output_dir = config
517 .php
518 .as_ref()
519 .and_then(|p| p.stubs.as_ref())
520 .map(|s| s.output.to_string_lossy().to_string())
521 .unwrap_or_else(|| "packages/php/src/".to_string());
522
523 let mut content = String::from("<?php\n\n");
524 content.push_str(&hash::header(CommentStyle::DoubleSlash));
525 content.push_str("declare(strict_types=1);\n\n");
526 content.push_str("namespace {\n\n");
529 content.push_str(&format!(" use {}\\{};\n\n", namespace, class_name));
530
531 for func in &api.functions {
532 if func.is_async {
535 continue;
536 }
537 let global_fn_name = format!("{}_{}", extension_name, func.name);
538 let return_php_type = php_type(&func.return_type);
539 let is_void = return_php_type == "void";
540
541 let bridge_param_names: ahash::AHashSet<&str> = config
543 .trait_bridges
544 .iter()
545 .filter_map(|b| b.param_name.as_deref())
546 .collect();
547 let visible_params: Vec<_> = func
548 .params
549 .iter()
550 .filter(|p| !bridge_param_names.contains(p.name.as_str()))
551 .collect();
552
553 content.push_str(&format!(" if (!\\function_exists('{}')) {{\n", global_fn_name));
554 content.push_str(" /**\n");
555 for line in func.doc.lines() {
556 if line.is_empty() {
557 content.push_str(" *\n");
558 } else {
559 content.push_str(&format!(" * {}\n", line));
560 }
561 }
562 if func.doc.is_empty() {
563 content.push_str(&format!(" * {}.\n", global_fn_name));
564 }
565 content.push_str(" *\n");
566 for p in &visible_params {
567 let ptype = php_phpdoc_type(&p.ty);
568 let nullable_prefix = if p.optional { "?" } else { "" };
569 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
570 }
571 let return_phpdoc = php_phpdoc_type(&func.return_type);
572 content.push_str(&format!(" * @return {}\n", return_phpdoc));
573 if func.error_type.is_some() {
574 content.push_str(&format!(
575 " * @throws \\{}\\{}Exception\n",
576 namespace, class_name
577 ));
578 }
579 content.push_str(" */\n");
580
581 let mut sorted_params = visible_params.clone();
582 sorted_params.sort_by_key(|p| p.optional);
583 let params_str: Vec<String> = sorted_params
584 .iter()
585 .map(|p| {
586 let ptype = php_type(&p.ty);
587 if p.optional {
588 format!("?{} ${} = null", ptype, p.name)
589 } else {
590 format!("{} ${}", ptype, p.name)
591 }
592 })
593 .collect();
594 let method_name = func.name.to_lower_camel_case();
595 let call_args: Vec<String> = sorted_params.iter().map(|p| format!("${}", p.name)).collect();
596 let call_expr = format!("{}::{}({})", class_name, method_name, call_args.join(", "));
597 content.push_str(&format!(
598 " function {}({}): {} {{\n",
599 global_fn_name,
600 params_str.join(", "),
601 return_php_type
602 ));
603 if is_void {
604 content.push_str(&format!(" {};\n", call_expr));
605 } else {
606 content.push_str(&format!(" return {};\n", call_expr));
607 }
608 content.push_str(" }\n");
609 content.push_str(" }\n\n");
610 }
611
612 content.push_str("} // end namespace\n");
613
614 Ok(vec![GeneratedFile {
615 path: PathBuf::from(&output_dir).join("functions.php"),
616 content,
617 generated_header: false,
618 }])
619 }
620
621 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
622 let extension_name = config.php_extension_name();
623 let class_name = extension_name.to_pascal_case();
624
625 let mut content = String::from("<?php\n\n");
627 content.push_str(&hash::header(CommentStyle::DoubleSlash));
628 content.push_str("declare(strict_types=1);\n\n");
629
630 let namespace = config.php_autoload_namespace();
632
633 content.push_str(&format!("namespace {};\n\n", namespace));
634 content.push_str(&format!("final class {}\n", class_name));
635 content.push_str("{\n");
636
637 let bridge_param_names_pub: ahash::AHashSet<&str> = config
639 .trait_bridges
640 .iter()
641 .filter_map(|b| b.param_name.as_deref())
642 .collect();
643
644 let bridge_field_funcs_pub: std::collections::HashMap<&str, crate::trait_bridge::BridgeFieldMatch<'_>> = api
647 .functions
648 .iter()
649 .filter_map(|func| {
650 crate::trait_bridge::find_bridge_field(func, &api.types, &config.trait_bridges)
651 .map(|m| (func.name.as_str(), m))
652 })
653 .collect();
654
655 for func in &api.functions {
657 let method_name = func.name.to_lower_camel_case();
658 let return_php_type = php_type(&func.return_type);
659
660 let visible_params: Vec<_> = func
662 .params
663 .iter()
664 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
665 .collect();
666
667 content.push_str(" /**\n");
669 for line in func.doc.lines() {
670 if line.is_empty() {
671 content.push_str(" *\n");
672 } else {
673 content.push_str(&format!(" * {}\n", line));
674 }
675 }
676 if func.doc.is_empty() {
677 content.push_str(&format!(" * {}.\n", method_name));
678 }
679 content.push_str(" *\n");
680 for p in &visible_params {
681 let ptype = php_phpdoc_type(&p.ty);
682 let nullable_prefix = if p.optional { "?" } else { "" };
683 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
684 }
685 let return_phpdoc = php_phpdoc_type(&func.return_type);
686 content.push_str(&format!(" * @return {}\n", return_phpdoc));
687 if func.error_type.is_some() {
688 content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
689 }
690 content.push_str(" */\n");
691
692 let mut sorted_visible_params = visible_params.clone();
696 sorted_visible_params.sort_by_key(|p| p.optional);
697
698 content.push_str(&format!(" public static function {}(", method_name));
699
700 let params: Vec<String> = sorted_visible_params
701 .iter()
702 .map(|p| {
703 let ptype = php_type(&p.ty);
704 if p.optional {
705 format!("?{} ${} = null", ptype, p.name)
706 } else {
707 format!("{} ${}", ptype, p.name)
708 }
709 })
710 .collect();
711 content.push_str(¶ms.join(", "));
712 content.push_str(&format!("): {}\n", return_php_type));
713 content.push_str(" {\n");
714 let ext_method_name = if func.is_async {
719 format!("{}_async", func.name).to_lower_camel_case()
720 } else {
721 func.name.to_lower_camel_case()
722 };
723 let is_void = matches!(&func.return_type, TypeRef::Unit);
724 let mut call_arg_parts: Vec<String> =
728 sorted_visible_params.iter().map(|p| format!("${}", p.name)).collect();
729 if let Some(bfm) = bridge_field_funcs_pub.get(func.name.as_str()) {
730 let opts_param_name = &func.params[bfm.param_index].name;
731 let access_op = if bfm.param_is_optional { "?->" } else { "->" };
735 call_arg_parts.push(format!("${}{}{}", opts_param_name, access_op, bfm.field_name));
736 }
737 let call_expr = format!(
738 "\\{}\\{}Api::{}({})",
739 namespace,
740 class_name,
741 ext_method_name,
742 call_arg_parts.join(", ")
743 );
744 if is_void {
745 content.push_str(&format!(
746 " {}; // delegate to native extension class\n",
747 call_expr
748 ));
749 } else {
750 content.push_str(&format!(
751 " return {}; // delegate to native extension class\n",
752 call_expr
753 ));
754 }
755 content.push_str(" }\n\n");
756 }
757
758 content.push_str("}\n");
759
760 let output_dir = config
764 .php
765 .as_ref()
766 .and_then(|p| p.stubs.as_ref())
767 .map(|s| s.output.to_string_lossy().to_string())
768 .unwrap_or_else(|| "packages/php/src/".to_string());
769
770 Ok(vec![GeneratedFile {
771 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
772 content,
773 generated_header: false,
774 }])
775 }
776
777 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
778 let extension_name = config.php_extension_name();
779 let class_name = extension_name.to_pascal_case();
780
781 let namespace = config.php_autoload_namespace();
783
784 let mut bridge_field_overrides: std::collections::HashMap<(String, String), String> =
786 std::collections::HashMap::new();
787 for bridge_cfg in &config.trait_bridges {
788 if bridge_cfg.bind_via != alef_core::config::BridgeBinding::OptionsField {
789 continue;
790 }
791 if let Some(options_type) = bridge_cfg.options_type.as_deref() {
792 let field_name = bridge_cfg
793 .resolved_options_field()
794 .unwrap_or(bridge_cfg.trait_name.as_str())
795 .to_string();
796 let bridge_class = bridge_cfg
797 .type_alias
798 .as_deref()
799 .unwrap_or(bridge_cfg.trait_name.as_str())
800 .to_string();
801 bridge_field_overrides.insert((options_type.to_string(), field_name), bridge_class);
802 }
803 }
804
805 let bridge_field_funcs_stubs: std::collections::HashMap<&str, crate::trait_bridge::BridgeFieldMatch<'_>> = api
807 .functions
808 .iter()
809 .filter_map(|func| {
810 crate::trait_bridge::find_bridge_field(func, &api.types, &config.trait_bridges)
811 .map(|m| (func.name.as_str(), m))
812 })
813 .collect();
814
815 let mut content = String::from("<?php\n\n");
820 content.push_str(&hash::header(CommentStyle::DoubleSlash));
821 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
822 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
823 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
824 content.push_str("declare(strict_types=1);\n\n");
825 content.push_str(&format!("namespace {} {{\n\n", namespace));
827
828 content.push_str(&format!(
830 "class {}Exception extends \\RuntimeException\n{{\n",
831 class_name
832 ));
833 content.push_str(
834 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
835 );
836 content.push_str("}\n\n");
837
838 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
840 if typ.is_opaque {
841 if !typ.doc.is_empty() {
842 content.push_str("/**\n");
843 for line in typ.doc.lines() {
844 if line.is_empty() {
845 content.push_str(" *\n");
846 } else {
847 content.push_str(&format!(" * {}\n", line));
848 }
849 }
850 content.push_str(" */\n");
851 }
852 content.push_str(&format!("class {}\n{{\n", typ.name));
853 content.push_str("}\n\n");
855 }
856 }
857
858 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
860 if typ.is_opaque || typ.fields.is_empty() {
861 continue;
862 }
863 if !typ.doc.is_empty() {
864 content.push_str("/**\n");
865 for line in typ.doc.lines() {
866 if line.is_empty() {
867 content.push_str(" *\n");
868 } else {
869 content.push_str(&format!(" * {}\n", line));
870 }
871 }
872 content.push_str(" */\n");
873 }
874 content.push_str(&format!("class {}\n{{\n", typ.name));
875
876 for field in &typ.fields {
878 let override_key = (typ.name.clone(), field.name.clone());
880 if let Some(bridge_class) = bridge_field_overrides.get(&override_key) {
881 content.push_str(&format!(" public ?{} ${};\n", bridge_class, field.name));
882 continue;
883 }
884 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
885 let prop_type = if field.optional {
886 let inner = php_type(&field.ty);
887 if inner.starts_with('?') {
888 inner
889 } else {
890 format!("?{inner}")
891 }
892 } else {
893 php_type(&field.ty)
894 };
895 if is_array {
896 let phpdoc = php_phpdoc_type(&field.ty);
897 let nullable_prefix = if field.optional { "?" } else { "" };
898 content.push_str(&format!(" /** @var {}{} */\n", nullable_prefix, phpdoc));
899 }
900 content.push_str(&format!(" public {} ${};\n", prop_type, field.name));
901 }
902 content.push('\n');
903
904 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
908 sorted_fields.sort_by_key(|f| f.optional);
909
910 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
913 .iter()
914 .copied()
915 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
916 .collect();
917 if !array_fields.is_empty() {
918 content.push_str(" /**\n");
919 for f in &array_fields {
920 let phpdoc = php_phpdoc_type(&f.ty);
921 let nullable_prefix = if f.optional { "?" } else { "" };
922 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
923 }
924 content.push_str(" */\n");
925 }
926
927 let params: Vec<String> = sorted_fields
928 .iter()
929 .map(|f| {
930 let override_key = (typ.name.clone(), f.name.clone());
931 if let Some(bridge_class) = bridge_field_overrides.get(&override_key) {
932 return format!(" ?{} ${} = null", bridge_class, f.name);
933 }
934 let ptype = php_type(&f.ty);
935 let nullable = if f.optional && !ptype.starts_with('?') {
936 format!("?{ptype}")
937 } else {
938 ptype
939 };
940 let default = if f.optional { " = null" } else { "" };
941 format!(" {} ${}{}", nullable, f.name, default)
942 })
943 .collect();
944 content.push_str(" public function __construct(\n");
945 content.push_str(¶ms.join(",\n"));
946 content.push_str("\n ) { }\n\n");
947
948 for field in &typ.fields {
950 let getter_name = field.name.to_lower_camel_case();
951 let override_key = (typ.name.clone(), field.name.clone());
952 if let Some(bridge_class) = bridge_field_overrides.get(&override_key) {
953 content.push_str(&format!(
954 " public function get{}(): ?{} {{ throw new \\RuntimeException('Not implemented.'); }}\n",
955 getter_name.to_pascal_case(),
956 bridge_class
957 ));
958 continue;
959 }
960 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
961 let return_type = if field.optional {
962 let inner = php_type(&field.ty);
963 if inner.starts_with('?') {
964 inner
965 } else {
966 format!("?{inner}")
967 }
968 } else {
969 php_type(&field.ty)
970 };
971 if is_array {
973 let phpdoc = php_phpdoc_type(&field.ty);
974 let nullable_prefix = if field.optional { "?" } else { "" };
975 content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
976 }
977 let is_void_getter = return_type == "void";
978 let getter_body = if is_void_getter {
979 "{ }".to_string()
980 } else {
981 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
982 };
983 content.push_str(&format!(
984 " public function get{}(): {} {getter_body}\n",
985 getter_name.to_pascal_case(),
986 return_type
987 ));
988 }
989
990 content.push_str("}\n\n");
991 }
992
993 for enum_def in &api.enums {
995 content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
996 for variant in &enum_def.variants {
997 content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
998 }
999 content.push_str("}\n\n");
1000 }
1001
1002 if !api.functions.is_empty() {
1007 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
1009 .trait_bridges
1010 .iter()
1011 .filter_map(|b| b.param_name.as_deref())
1012 .collect();
1013
1014 content.push_str(&format!("class {}Api\n{{\n", class_name));
1015 for func in &api.functions {
1016 let return_type = php_type_fq(&func.return_type, &namespace);
1017 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
1018 let visible_params: Vec<_> = func
1020 .params
1021 .iter()
1022 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
1023 .collect();
1024 let mut sorted_visible_params = visible_params.clone();
1026 sorted_visible_params.sort_by_key(|p| p.optional);
1027 let has_array_params = visible_params
1030 .iter()
1031 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
1032 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
1033 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
1034 if has_array_params || has_array_return {
1035 content.push_str(" /**\n");
1036 for p in &visible_params {
1037 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
1038 let nullable_prefix = if p.optional { "?" } else { "" };
1039 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
1040 }
1041 content.push_str(&format!(" * @return {}\n", return_phpdoc));
1042 content.push_str(" */\n");
1043 }
1044 let mut params: Vec<String> = sorted_visible_params
1045 .iter()
1046 .map(|p| {
1047 let ptype = php_type_fq(&p.ty, &namespace);
1048 if p.optional {
1049 format!("?{} ${} = null", ptype, p.name)
1050 } else {
1051 format!("{} ${}", ptype, p.name)
1052 }
1053 })
1054 .collect();
1055 if let Some(bfm) = bridge_field_funcs_stubs.get(func.name.as_str()) {
1057 let bridge_class = bfm
1058 .bridge
1059 .type_alias
1060 .as_deref()
1061 .unwrap_or(bfm.bridge.trait_name.as_str());
1062 params.push(format!("?{} ${}_obj = null", bridge_class, bfm.field_name));
1063 }
1064 let stub_method_name = if func.is_async {
1066 format!("{}_async", func.name).to_lower_camel_case()
1067 } else {
1068 func.name.to_lower_camel_case()
1069 };
1070 let is_void_stub = return_type == "void";
1071 let stub_body = if is_void_stub {
1072 "{ }".to_string()
1073 } else {
1074 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
1075 };
1076 content.push_str(&format!(
1077 " public static function {}({}): {} {stub_body}\n",
1078 stub_method_name,
1079 params.join(", "),
1080 return_type
1081 ));
1082 }
1083 content.push_str("}\n\n");
1084 }
1085
1086 content.push_str("} // end namespace\n");
1088
1089 let output_dir = config
1091 .php
1092 .as_ref()
1093 .and_then(|p| p.stubs.as_ref())
1094 .map(|s| s.output.to_string_lossy().to_string())
1095 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1096
1097 Ok(vec![GeneratedFile {
1098 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1099 content,
1100 generated_header: false,
1101 }])
1102 }
1103
1104 fn build_config(&self) -> Option<BuildConfig> {
1105 Some(BuildConfig {
1106 tool: "cargo",
1107 crate_suffix: "-php",
1108 build_dep: BuildDependency::None,
1109 post_build: vec![],
1110 })
1111 }
1112}
1113
1114fn php_phpdoc_type(ty: &TypeRef) -> String {
1117 match ty {
1118 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1119 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1120 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1121 _ => php_type(ty),
1122 }
1123}
1124
1125fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1127 match ty {
1128 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1129 TypeRef::Map(k, v) => format!(
1130 "array<{}, {}>",
1131 php_phpdoc_type_fq(k, namespace),
1132 php_phpdoc_type_fq(v, namespace)
1133 ),
1134 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1135 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1136 _ => php_type(ty),
1137 }
1138}
1139
1140fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1142 match ty {
1143 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1144 TypeRef::Optional(inner) => {
1145 let inner_type = php_type_fq(inner, namespace);
1146 if inner_type.starts_with('?') {
1147 inner_type
1148 } else {
1149 format!("?{inner_type}")
1150 }
1151 }
1152 _ => php_type(ty),
1153 }
1154}
1155
1156fn php_type(ty: &TypeRef) -> String {
1158 match ty {
1159 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1160 TypeRef::Primitive(p) => match p {
1161 PrimitiveType::Bool => "bool".to_string(),
1162 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1163 PrimitiveType::U8
1164 | PrimitiveType::U16
1165 | PrimitiveType::U32
1166 | PrimitiveType::U64
1167 | PrimitiveType::I8
1168 | PrimitiveType::I16
1169 | PrimitiveType::I32
1170 | PrimitiveType::I64
1171 | PrimitiveType::Usize
1172 | PrimitiveType::Isize => "int".to_string(),
1173 },
1174 TypeRef::Optional(inner) => {
1175 let inner_type = php_type(inner);
1178 if inner_type.starts_with('?') {
1179 inner_type
1180 } else {
1181 format!("?{inner_type}")
1182 }
1183 }
1184 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1185 TypeRef::Named(name) => name.clone(),
1186 TypeRef::Unit => "void".to_string(),
1187 TypeRef::Duration => "float".to_string(),
1188 }
1189}