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
163 let extension_name = config.php_extension_name();
166 let php_namespace = if extension_name.contains('_') {
167 let parts: Vec<&str> = extension_name.split('_').collect();
168 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
169 ns_parts.join("\\")
170 } else {
171 extension_name.to_pascal_case()
172 };
173
174 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
176
177 for adapter in &config.adapters {
179 match adapter.pattern {
180 alef_core::config::AdapterPattern::Streaming => {
181 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
182 if let Some(struct_code) = adapter_bodies.get(&key) {
183 builder.add_item(struct_code);
184 }
185 }
186 alef_core::config::AdapterPattern::CallbackBridge => {
187 let struct_key = format!("{}.__bridge_struct__", adapter.name);
188 let impl_key = format!("{}.__bridge_impl__", adapter.name);
189 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
190 builder.add_item(struct_code);
191 }
192 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
193 builder.add_item(impl_code);
194 }
195 }
196 _ => {}
197 }
198 }
199
200 for typ in api
201 .types
202 .iter()
203 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
204 {
205 if typ.is_opaque {
206 let ns_escaped = php_namespace.replace('\\', "\\\\");
210 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
211 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
212 let opaque_cfg = RustBindingConfig {
213 struct_attrs: &opaque_attr_arr,
214 ..cfg
215 };
216 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
217 builder.add_item(&gen_opaque_struct_methods(
218 typ,
219 &mapper,
220 &opaque_types,
221 &core_import,
222 &adapter_bodies,
223 ));
224 } else {
225 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
228 builder.add_item(&types::gen_struct_methods_with_exclude(
229 typ,
230 &mapper,
231 has_serde,
232 &core_import,
233 &opaque_types,
234 &enum_names,
235 &api.enums,
236 &exclude_functions,
237 ));
238 }
239 }
240
241 for enum_def in &api.enums {
242 if is_tagged_data_enum(enum_def) {
243 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
245 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
246 } else {
247 builder.add_item(&gen_enum_constants(enum_def));
248 }
249 }
250
251 let included_functions: Vec<_> = api
256 .functions
257 .iter()
258 .filter(|f| !exclude_functions.contains(&f.name))
259 .collect();
260 if !included_functions.is_empty() {
261 let facade_class_name = extension_name.to_pascal_case();
262 let mut method_items: Vec<String> = Vec::new();
265 for func in included_functions {
266 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
267 if let Some((param_idx, bridge_cfg)) = bridge_param {
268 method_items.push(crate::trait_bridge::gen_bridge_function(
269 func,
270 param_idx,
271 bridge_cfg,
272 &mapper,
273 &opaque_types,
274 &core_import,
275 ));
276 } else if func.is_async {
277 method_items.push(gen_async_function_as_static_method(
278 func,
279 &mapper,
280 &opaque_types,
281 &core_import,
282 &config.trait_bridges,
283 ));
284 } else {
285 method_items.push(gen_function_as_static_method(
286 func,
287 &mapper,
288 &opaque_types,
289 &core_import,
290 &config.trait_bridges,
291 has_serde,
292 ));
293 }
294 }
295
296 let methods_joined = method_items
297 .iter()
298 .map(|m| {
299 m.lines()
301 .map(|l| {
302 if l.is_empty() {
303 String::new()
304 } else {
305 format!(" {l}")
306 }
307 })
308 .collect::<Vec<_>>()
309 .join("\n")
310 })
311 .collect::<Vec<_>>()
312 .join("\n\n");
313 let php_api_class_name = format!("{facade_class_name}Api");
316 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
318 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
319 let facade_struct = format!(
320 "#[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}}"
321 );
322 builder.add_item(&facade_struct);
323
324 for bridge_cfg in &config.trait_bridges {
326 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
327 let bridge = crate::trait_bridge::gen_trait_bridge(
328 trait_type,
329 bridge_cfg,
330 &core_import,
331 &config.error_type(),
332 &config.error_constructor(),
333 api,
334 );
335 for imp in &bridge.imports {
336 builder.add_import(imp);
337 }
338 builder.add_item(&bridge.code);
339 }
340 }
341 }
342
343 let convertible = alef_codegen::conversions::convertible_types(api);
344 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
345 let input_types = alef_codegen::conversions::input_type_names(api);
346 let enum_names_ref = &mapper.enum_names;
351 let php_conv_config = ConversionConfig {
352 cast_large_ints_to_i64: true,
353 enum_string_names: Some(enum_names_ref),
354 json_to_string: true,
355 include_cfg_metadata: false,
356 option_duration_on_defaults: true,
357 ..Default::default()
358 };
359 let mut enum_tainted: AHashSet<String> = AHashSet::new();
361 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
362 if has_enum_named_field(typ, enum_names_ref) {
363 enum_tainted.insert(typ.name.clone());
364 }
365 }
366 let mut changed = true;
368 while changed {
369 changed = false;
370 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
371 if !enum_tainted.contains(&typ.name)
372 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
373 {
374 enum_tainted.insert(typ.name.clone());
375 changed = true;
376 }
377 }
378 }
379 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
380 if input_types.contains(&typ.name)
382 && !enum_tainted.contains(&typ.name)
383 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
384 {
385 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
386 typ,
387 &core_import,
388 &php_conv_config,
389 ));
390 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) && has_serde {
391 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
394 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
395 builder.add_item(&gen_enum_tainted_from_binding_to_core(
399 typ,
400 &core_import,
401 enum_names_ref,
402 &enum_tainted,
403 &php_conv_config,
404 &api.enums,
405 ));
406 }
407 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
409 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
410 typ,
411 &core_import,
412 &opaque_types,
413 &php_conv_config,
414 ));
415 }
416 }
417
418 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
420 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
421 }
422
423 for error in &api.errors {
425 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
426 }
427
428 let php_config = config.php.as_ref();
434 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
435
436 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
440 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
441 }
442
443 let mut class_registrations = String::new();
446 for typ in api
447 .types
448 .iter()
449 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
450 {
451 class_registrations.push_str(&format!("\n .class::<{}>()", typ.name));
452 }
453 if !api.functions.is_empty() {
455 let facade_class_name = extension_name.to_pascal_case();
456 class_registrations.push_str(&format!("\n .class::<{facade_class_name}Api>()"));
457 }
458 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
461 class_registrations.push_str(&format!("\n .class::<{}>()", enum_def.name));
462 }
463 builder.add_item(&format!(
464 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
465 ));
466
467 let content = builder.build();
468
469 Ok(vec![GeneratedFile {
470 path: PathBuf::from(&output_dir).join("lib.rs"),
471 content,
472 generated_header: false,
473 }])
474 }
475
476 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
477 let extension_name = config.php_extension_name();
478 let class_name = extension_name.to_pascal_case();
479
480 let mut content = String::from("<?php\n");
482 content.push_str(&hash::header(CommentStyle::DoubleSlash));
483 content.push_str("declare(strict_types=1);\n\n");
484
485 let namespace = if extension_name.contains('_') {
487 let parts: Vec<&str> = extension_name.split('_').collect();
488 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
489 ns_parts.join("\\")
490 } else {
491 class_name.clone()
492 };
493
494 content.push_str(&format!("namespace {};\n\n", namespace));
495 content.push_str(&format!("final class {}\n", class_name));
496 content.push_str("{\n");
497
498 let bridge_param_names_pub: ahash::AHashSet<&str> = config
500 .trait_bridges
501 .iter()
502 .filter_map(|b| b.param_name.as_deref())
503 .collect();
504
505 for func in &api.functions {
507 let method_name = func.name.to_lower_camel_case();
508 let return_php_type = php_type(&func.return_type);
509
510 let visible_params: Vec<_> = func
512 .params
513 .iter()
514 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
515 .collect();
516
517 content.push_str(" /**\n");
519 for line in func.doc.lines() {
520 if line.is_empty() {
521 content.push_str(" *\n");
522 } else {
523 content.push_str(&format!(" * {}\n", line));
524 }
525 }
526 if func.doc.is_empty() {
527 content.push_str(&format!(" * {}.\n", method_name));
528 }
529 content.push_str(" *\n");
530 for p in &visible_params {
531 let ptype = php_phpdoc_type(&p.ty);
532 let nullable_prefix = if p.optional { "?" } else { "" };
533 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
534 }
535 let return_phpdoc = php_phpdoc_type(&func.return_type);
536 content.push_str(&format!(" * @return {}\n", return_phpdoc));
537 if func.error_type.is_some() {
538 content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
539 }
540 content.push_str(" */\n");
541
542 let mut sorted_visible_params = visible_params.clone();
546 sorted_visible_params.sort_by_key(|p| p.optional);
547
548 content.push_str(&format!(" public static function {}(", method_name));
549
550 let params: Vec<String> = sorted_visible_params
551 .iter()
552 .map(|p| {
553 let ptype = php_type(&p.ty);
554 if p.optional {
555 format!("?{} ${} = null", ptype, p.name)
556 } else {
557 format!("{} ${}", ptype, p.name)
558 }
559 })
560 .collect();
561 content.push_str(¶ms.join(", "));
562 content.push_str(&format!("): {}\n", return_php_type));
563 content.push_str(" {\n");
564 let ext_method_name = if func.is_async {
569 format!("{}_async", func.name).to_lower_camel_case()
570 } else {
571 func.name.to_lower_camel_case()
572 };
573 let is_void = matches!(&func.return_type, TypeRef::Unit);
574 let call_expr = format!(
575 "\\{}\\{}Api::{}({})",
576 namespace,
577 class_name,
578 ext_method_name,
579 sorted_visible_params
580 .iter()
581 .map(|p| format!("${}", p.name))
582 .collect::<Vec<_>>()
583 .join(", ")
584 );
585 if is_void {
586 content.push_str(&format!(
587 " {}; // delegate to native extension class\n",
588 call_expr
589 ));
590 } else {
591 content.push_str(&format!(
592 " return {}; // delegate to native extension class\n",
593 call_expr
594 ));
595 }
596 content.push_str(" }\n\n");
597 }
598
599 content.push_str("}\n");
600
601 let output_dir = config
605 .php
606 .as_ref()
607 .and_then(|p| p.stubs.as_ref())
608 .map(|s| s.output.to_string_lossy().to_string())
609 .unwrap_or_else(|| "packages/php/src/".to_string());
610
611 Ok(vec![GeneratedFile {
612 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
613 content,
614 generated_header: false,
615 }])
616 }
617
618 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
619 let extension_name = config.php_extension_name();
620 let class_name = extension_name.to_pascal_case();
621
622 let namespace = if extension_name.contains('_') {
624 let parts: Vec<&str> = extension_name.split('_').collect();
625 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
626 ns_parts.join("\\")
627 } else {
628 class_name.clone()
629 };
630
631 let mut content = String::from("<?php\n\n");
636 content.push_str(&hash::header(CommentStyle::DoubleSlash));
637 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
638 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
639 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
640 content.push_str("declare(strict_types=1);\n\n");
641 content.push_str(&format!("namespace {} {{\n\n", namespace));
643
644 content.push_str(&format!(
646 "class {}Exception extends \\RuntimeException\n{{\n",
647 class_name
648 ));
649 content.push_str(
650 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
651 );
652 content.push_str("}\n\n");
653
654 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
656 if typ.is_opaque {
657 if !typ.doc.is_empty() {
658 content.push_str("/**\n");
659 for line in typ.doc.lines() {
660 if line.is_empty() {
661 content.push_str(" *\n");
662 } else {
663 content.push_str(&format!(" * {}\n", line));
664 }
665 }
666 content.push_str(" */\n");
667 }
668 content.push_str(&format!("class {}\n{{\n", typ.name));
669 content.push_str("}\n\n");
671 }
672 }
673
674 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
676 if typ.is_opaque || typ.fields.is_empty() {
677 continue;
678 }
679 if !typ.doc.is_empty() {
680 content.push_str("/**\n");
681 for line in typ.doc.lines() {
682 if line.is_empty() {
683 content.push_str(" *\n");
684 } else {
685 content.push_str(&format!(" * {}\n", line));
686 }
687 }
688 content.push_str(" */\n");
689 }
690 content.push_str(&format!("class {}\n{{\n", typ.name));
691
692 for field in &typ.fields {
694 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
695 let prop_type = if field.optional {
696 let inner = php_type(&field.ty);
697 if inner.starts_with('?') {
698 inner
699 } else {
700 format!("?{inner}")
701 }
702 } else {
703 php_type(&field.ty)
704 };
705 if is_array {
706 let phpdoc = php_phpdoc_type(&field.ty);
707 let nullable_prefix = if field.optional { "?" } else { "" };
708 content.push_str(&format!(" /** @var {}{} */\n", nullable_prefix, phpdoc));
709 }
710 content.push_str(&format!(" public {} ${};\n", prop_type, field.name));
711 }
712 content.push('\n');
713
714 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
718 sorted_fields.sort_by_key(|f| f.optional);
719
720 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
723 .iter()
724 .copied()
725 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
726 .collect();
727 if !array_fields.is_empty() {
728 content.push_str(" /**\n");
729 for f in &array_fields {
730 let phpdoc = php_phpdoc_type(&f.ty);
731 let nullable_prefix = if f.optional { "?" } else { "" };
732 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
733 }
734 content.push_str(" */\n");
735 }
736
737 let params: Vec<String> = sorted_fields
738 .iter()
739 .map(|f| {
740 let ptype = php_type(&f.ty);
741 let nullable = if f.optional && !ptype.starts_with('?') {
742 format!("?{ptype}")
743 } else {
744 ptype
745 };
746 let default = if f.optional { " = null" } else { "" };
747 format!(" {} ${}{}", nullable, f.name, default)
748 })
749 .collect();
750 content.push_str(" public function __construct(\n");
751 content.push_str(¶ms.join(",\n"));
752 content.push_str("\n ) { }\n\n");
753
754 for field in &typ.fields {
756 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
757 let return_type = if field.optional {
758 let inner = php_type(&field.ty);
759 if inner.starts_with('?') {
760 inner
761 } else {
762 format!("?{inner}")
763 }
764 } else {
765 php_type(&field.ty)
766 };
767 let getter_name = field.name.to_lower_camel_case();
768 if is_array {
770 let phpdoc = php_phpdoc_type(&field.ty);
771 let nullable_prefix = if field.optional { "?" } else { "" };
772 content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
773 }
774 let is_void_getter = return_type == "void";
775 let getter_body = if is_void_getter {
776 "{ }".to_string()
777 } else {
778 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
779 };
780 content.push_str(&format!(
781 " public function get{}(): {} {getter_body}\n",
782 getter_name.to_pascal_case(),
783 return_type
784 ));
785 }
786
787 content.push_str("}\n\n");
788 }
789
790 for enum_def in &api.enums {
792 content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
793 for variant in &enum_def.variants {
794 content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
795 }
796 content.push_str("}\n\n");
797 }
798
799 if !api.functions.is_empty() {
804 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
806 .trait_bridges
807 .iter()
808 .filter_map(|b| b.param_name.as_deref())
809 .collect();
810
811 content.push_str(&format!("class {}Api\n{{\n", class_name));
812 for func in &api.functions {
813 let return_type = php_type_fq(&func.return_type, &namespace);
814 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
815 let visible_params: Vec<_> = func
817 .params
818 .iter()
819 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
820 .collect();
821 let mut sorted_visible_params = visible_params.clone();
823 sorted_visible_params.sort_by_key(|p| p.optional);
824 let has_array_params = visible_params
827 .iter()
828 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
829 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
830 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
831 if has_array_params || has_array_return {
832 content.push_str(" /**\n");
833 for p in &visible_params {
834 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
835 let nullable_prefix = if p.optional { "?" } else { "" };
836 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
837 }
838 content.push_str(&format!(" * @return {}\n", return_phpdoc));
839 content.push_str(" */\n");
840 }
841 let params: Vec<String> = sorted_visible_params
842 .iter()
843 .map(|p| {
844 let ptype = php_type_fq(&p.ty, &namespace);
845 if p.optional {
846 format!("?{} ${} = null", ptype, p.name)
847 } else {
848 format!("{} ${}", ptype, p.name)
849 }
850 })
851 .collect();
852 let stub_method_name = if func.is_async {
854 format!("{}_async", func.name).to_lower_camel_case()
855 } else {
856 func.name.to_lower_camel_case()
857 };
858 let is_void_stub = return_type == "void";
859 let stub_body = if is_void_stub {
860 "{ }".to_string()
861 } else {
862 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
863 };
864 content.push_str(&format!(
865 " public static function {}({}): {} {stub_body}\n",
866 stub_method_name,
867 params.join(", "),
868 return_type
869 ));
870 }
871 content.push_str("}\n\n");
872 }
873
874 content.push_str("} // end namespace\n");
876
877 let output_dir = config
879 .php
880 .as_ref()
881 .and_then(|p| p.stubs.as_ref())
882 .map(|s| s.output.to_string_lossy().to_string())
883 .unwrap_or_else(|| "packages/php/stubs/".to_string());
884
885 Ok(vec![GeneratedFile {
886 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
887 content,
888 generated_header: false,
889 }])
890 }
891
892 fn build_config(&self) -> Option<BuildConfig> {
893 Some(BuildConfig {
894 tool: "cargo",
895 crate_suffix: "-php",
896 build_dep: BuildDependency::None,
897 post_build: vec![],
898 })
899 }
900}
901
902fn php_phpdoc_type(ty: &TypeRef) -> String {
905 match ty {
906 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
907 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
908 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
909 _ => php_type(ty),
910 }
911}
912
913fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
915 match ty {
916 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
917 TypeRef::Map(k, v) => format!(
918 "array<{}, {}>",
919 php_phpdoc_type_fq(k, namespace),
920 php_phpdoc_type_fq(v, namespace)
921 ),
922 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
923 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
924 _ => php_type(ty),
925 }
926}
927
928fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
930 match ty {
931 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
932 TypeRef::Optional(inner) => {
933 let inner_type = php_type_fq(inner, namespace);
934 if inner_type.starts_with('?') {
935 inner_type
936 } else {
937 format!("?{inner_type}")
938 }
939 }
940 _ => php_type(ty),
941 }
942}
943
944fn php_type(ty: &TypeRef) -> String {
946 match ty {
947 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
948 TypeRef::Primitive(p) => match p {
949 PrimitiveType::Bool => "bool".to_string(),
950 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
951 PrimitiveType::U8
952 | PrimitiveType::U16
953 | PrimitiveType::U32
954 | PrimitiveType::U64
955 | PrimitiveType::I8
956 | PrimitiveType::I16
957 | PrimitiveType::I32
958 | PrimitiveType::I64
959 | PrimitiveType::Usize
960 | PrimitiveType::Isize => "int".to_string(),
961 },
962 TypeRef::Optional(inner) => {
963 let inner_type = php_type(inner);
966 if inner_type.starts_with('?') {
967 inner_type
968 } else {
969 format!("?{inner_type}")
970 }
971 }
972 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
973 TypeRef::Named(name) => name.clone(),
974 TypeRef::Unit => "void".to_string(),
975 TypeRef::Duration => "float".to_string(),
976 }
977}