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::{gen_enum_constants, gen_opaque_struct_methods, gen_php_struct};
25
26pub struct PhpBackend;
27
28impl PhpBackend {
29 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
30 RustBindingConfig {
31 struct_attrs: &["php_class"],
32 field_attrs: &[],
33 struct_derives: &["Clone"],
34 method_block_attr: Some("php_impl"),
35 constructor_attr: "",
36 static_attr: None,
37 function_attr: "#[php_function]",
38 enum_attrs: &[],
39 enum_derives: &[],
40 needs_signature: false,
41 signature_prefix: "",
42 signature_suffix: "",
43 core_import,
44 async_pattern: AsyncPattern::TokioBlockOn,
45 has_serde,
46 type_name_prefix: "",
47 option_duration_on_defaults: true,
48 opaque_type_names: &[],
49 }
50 }
51}
52
53impl Backend for PhpBackend {
54 fn name(&self) -> &str {
55 "php"
56 }
57
58 fn language(&self) -> Language {
59 Language::Php
60 }
61
62 fn capabilities(&self) -> Capabilities {
63 Capabilities {
64 supports_async: true,
65 supports_classes: true,
66 supports_enums: true,
67 supports_option: true,
68 supports_result: true,
69 ..Capabilities::default()
70 }
71 }
72
73 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
74 let enum_names = api.enums.iter().map(|e| e.name.clone()).collect();
75 let mapper = PhpMapper { enum_names };
76 let core_import = config.core_import();
77
78 let php_config = config.php.as_ref();
80 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
81 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
82
83 let output_dir = resolve_output_dir(
84 config.output.php.as_ref(),
85 &config.crate_config.name,
86 "crates/{name}-php/src/",
87 );
88 let has_serde = detect_serde_available(&output_dir);
89 let cfg = Self::binding_config(&core_import, has_serde);
90
91 let mut builder = RustFileBuilder::new().with_generated_header();
93 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
94 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)");
95 builder.add_import("ext_php_rs::prelude::*");
96
97 if has_serde {
99 builder.add_import("serde_json");
100 }
101
102 for trait_path in generators::collect_trait_imports(api) {
104 builder.add_import(&trait_path);
105 }
106
107 let has_maps = api.types.iter().any(|t| {
109 t.fields
110 .iter()
111 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
112 }) || api
113 .functions
114 .iter()
115 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
116 if has_maps {
117 builder.add_import("std::collections::HashMap");
118 }
119
120 let custom_mods = config.custom_modules.for_language(Language::Php);
122 for module in custom_mods {
123 builder.add_item(&format!("pub mod {module};"));
124 }
125
126 let has_async =
128 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
129
130 if has_async {
131 builder.add_item(&gen_tokio_runtime());
132 }
133
134 let opaque_types: AHashSet<String> = api
136 .types
137 .iter()
138 .filter(|t| t.is_opaque)
139 .map(|t| t.name.clone())
140 .collect();
141 if !opaque_types.is_empty() {
142 builder.add_import("std::sync::Arc");
143 }
144
145 let enum_names: AHashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
148
149 let extension_name = config.php_extension_name();
152 let php_namespace = if extension_name.contains('_') {
153 let parts: Vec<&str> = extension_name.split('_').collect();
154 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
155 ns_parts.join("\\")
156 } else {
157 extension_name.to_pascal_case()
158 };
159
160 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
162
163 for adapter in &config.adapters {
165 match adapter.pattern {
166 alef_core::config::AdapterPattern::Streaming => {
167 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
168 if let Some(struct_code) = adapter_bodies.get(&key) {
169 builder.add_item(struct_code);
170 }
171 }
172 alef_core::config::AdapterPattern::CallbackBridge => {
173 let struct_key = format!("{}.__bridge_struct__", adapter.name);
174 let impl_key = format!("{}.__bridge_impl__", adapter.name);
175 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
176 builder.add_item(struct_code);
177 }
178 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
179 builder.add_item(impl_code);
180 }
181 }
182 _ => {}
183 }
184 }
185
186 for typ in api
187 .types
188 .iter()
189 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
190 {
191 if typ.is_opaque {
192 let ns_escaped = php_namespace.replace('\\', "\\\\");
196 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
197 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
198 let opaque_cfg = RustBindingConfig {
199 struct_attrs: &opaque_attr_arr,
200 ..cfg
201 };
202 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
203 builder.add_item(&gen_opaque_struct_methods(
204 typ,
205 &mapper,
206 &opaque_types,
207 &core_import,
208 &adapter_bodies,
209 ));
210 } else {
211 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
214 builder.add_item(&types::gen_struct_methods_with_exclude(
215 typ,
216 &mapper,
217 has_serde,
218 &core_import,
219 &opaque_types,
220 &enum_names,
221 &api.enums,
222 &exclude_functions,
223 ));
224 }
225 }
226
227 for enum_def in &api.enums {
228 builder.add_item(&gen_enum_constants(enum_def));
229 }
230
231 let included_functions: Vec<_> = api
236 .functions
237 .iter()
238 .filter(|f| !exclude_functions.contains(&f.name))
239 .collect();
240 if !included_functions.is_empty() {
241 let facade_class_name = extension_name.to_pascal_case();
242 let mut method_items: Vec<String> = Vec::new();
245 for func in included_functions {
246 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
247 if let Some((param_idx, bridge_cfg)) = bridge_param {
248 method_items.push(crate::trait_bridge::gen_bridge_function(
249 func,
250 param_idx,
251 bridge_cfg,
252 &mapper,
253 &opaque_types,
254 &core_import,
255 ));
256 } else if func.is_async {
257 method_items.push(gen_async_function_as_static_method(
258 func,
259 &mapper,
260 &opaque_types,
261 &core_import,
262 &config.trait_bridges,
263 ));
264 } else {
265 method_items.push(gen_function_as_static_method(
266 func,
267 &mapper,
268 &opaque_types,
269 &core_import,
270 &config.trait_bridges,
271 has_serde,
272 ));
273 }
274 }
275
276 let methods_joined = method_items
277 .iter()
278 .map(|m| {
279 m.lines()
281 .map(|l| {
282 if l.is_empty() {
283 String::new()
284 } else {
285 format!(" {l}")
286 }
287 })
288 .collect::<Vec<_>>()
289 .join("\n")
290 })
291 .collect::<Vec<_>>()
292 .join("\n\n");
293 let php_api_class_name = format!("{facade_class_name}Api");
296 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
298 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
299 let facade_struct = format!(
300 "#[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}}"
301 );
302 builder.add_item(&facade_struct);
303
304 for bridge_cfg in &config.trait_bridges {
306 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
307 let bridge = crate::trait_bridge::gen_trait_bridge(
308 trait_type,
309 bridge_cfg,
310 &core_import,
311 &config.error_type(),
312 &config.error_constructor(),
313 api,
314 );
315 for imp in &bridge.imports {
316 builder.add_import(imp);
317 }
318 builder.add_item(&bridge.code);
319 }
320 }
321 }
322
323 let convertible = alef_codegen::conversions::convertible_types(api);
324 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
325 let input_types = alef_codegen::conversions::input_type_names(api);
326 let enum_names_ref = &mapper.enum_names;
331 let php_conv_config = ConversionConfig {
332 cast_large_ints_to_i64: true,
333 enum_string_names: Some(enum_names_ref),
334 json_to_string: true,
335 include_cfg_metadata: false,
336 option_duration_on_defaults: true,
337 ..Default::default()
338 };
339 let mut enum_tainted: AHashSet<String> = AHashSet::new();
341 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
342 if has_enum_named_field(typ, enum_names_ref) {
343 enum_tainted.insert(typ.name.clone());
344 }
345 }
346 let mut changed = true;
348 while changed {
349 changed = false;
350 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
351 if !enum_tainted.contains(&typ.name)
352 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
353 {
354 enum_tainted.insert(typ.name.clone());
355 changed = true;
356 }
357 }
358 }
359 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
360 if input_types.contains(&typ.name)
362 && !enum_tainted.contains(&typ.name)
363 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
364 {
365 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
366 typ,
367 &core_import,
368 &php_conv_config,
369 ));
370 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) && has_serde {
371 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
374 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
375 builder.add_item(&gen_enum_tainted_from_binding_to_core(
379 typ,
380 &core_import,
381 enum_names_ref,
382 &enum_tainted,
383 &php_conv_config,
384 &api.enums,
385 ));
386 }
387 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
389 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
390 typ,
391 &core_import,
392 &opaque_types,
393 &php_conv_config,
394 ));
395 }
396 }
397
398 for error in &api.errors {
400 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
401 }
402
403 let php_config = config.php.as_ref();
405 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
406 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
407 builder.add_inner_attribute(&format!(
408 "cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
409 ));
410 }
411
412 let mut class_registrations = String::new();
415 for typ in api
416 .types
417 .iter()
418 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
419 {
420 class_registrations.push_str(&format!("\n .class::<{}>()", typ.name));
421 }
422 if !api.functions.is_empty() {
424 let facade_class_name = extension_name.to_pascal_case();
425 class_registrations.push_str(&format!("\n .class::<{facade_class_name}Api>()"));
426 }
427 builder.add_item(&format!(
430 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
431 ));
432
433 let content = builder.build();
434
435 Ok(vec![GeneratedFile {
436 path: PathBuf::from(&output_dir).join("lib.rs"),
437 content,
438 generated_header: false,
439 }])
440 }
441
442 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
443 let extension_name = config.php_extension_name();
444 let class_name = extension_name.to_pascal_case();
445
446 let mut content = String::from("<?php\n");
448 content.push_str(&hash::header(CommentStyle::DoubleSlash));
449 content.push_str("declare(strict_types=1);\n\n");
450
451 let namespace = if extension_name.contains('_') {
453 let parts: Vec<&str> = extension_name.split('_').collect();
454 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
455 ns_parts.join("\\")
456 } else {
457 class_name.clone()
458 };
459
460 content.push_str(&format!("namespace {};\n\n", namespace));
461 content.push_str(&format!("final class {}\n", class_name));
462 content.push_str("{\n");
463
464 let bridge_param_names_pub: ahash::AHashSet<&str> = config
466 .trait_bridges
467 .iter()
468 .filter_map(|b| b.param_name.as_deref())
469 .collect();
470
471 for func in &api.functions {
473 let method_name = func.name.to_lower_camel_case();
474 let return_php_type = php_type(&func.return_type);
475
476 let visible_params: Vec<_> = func
478 .params
479 .iter()
480 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
481 .collect();
482
483 content.push_str(" /**\n");
485 for line in func.doc.lines() {
486 if line.is_empty() {
487 content.push_str(" *\n");
488 } else {
489 content.push_str(&format!(" * {}\n", line));
490 }
491 }
492 if func.doc.is_empty() {
493 content.push_str(&format!(" * {}.\n", method_name));
494 }
495 content.push_str(" *\n");
496 for p in &visible_params {
497 let ptype = php_phpdoc_type(&p.ty);
498 let nullable_prefix = if p.optional { "?" } else { "" };
499 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
500 }
501 let return_phpdoc = php_phpdoc_type(&func.return_type);
502 content.push_str(&format!(" * @return {}\n", return_phpdoc));
503 if func.error_type.is_some() {
504 content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
505 }
506 content.push_str(" */\n");
507
508 let mut sorted_visible_params = visible_params.clone();
512 sorted_visible_params.sort_by_key(|p| p.optional);
513
514 content.push_str(&format!(" public static function {}(", method_name));
515
516 let params: Vec<String> = sorted_visible_params
517 .iter()
518 .map(|p| {
519 let ptype = php_type(&p.ty);
520 if p.optional {
521 format!("?{} ${} = null", ptype, p.name)
522 } else {
523 format!("{} ${}", ptype, p.name)
524 }
525 })
526 .collect();
527 content.push_str(¶ms.join(", "));
528 content.push_str(&format!("): {}\n", return_php_type));
529 content.push_str(" {\n");
530 let ext_method_name = if func.is_async {
535 format!("{}_async", func.name).to_lower_camel_case()
536 } else {
537 func.name.to_lower_camel_case()
538 };
539 let is_void = matches!(&func.return_type, TypeRef::Unit);
540 let call_expr = format!(
541 "\\{}\\{}Api::{}({})",
542 namespace,
543 class_name,
544 ext_method_name,
545 sorted_visible_params
546 .iter()
547 .map(|p| format!("${}", p.name))
548 .collect::<Vec<_>>()
549 .join(", ")
550 );
551 if is_void {
552 content.push_str(&format!(
553 " {}; // delegate to native extension class\n",
554 call_expr
555 ));
556 } else {
557 content.push_str(&format!(
558 " return {}; // delegate to native extension class\n",
559 call_expr
560 ));
561 }
562 content.push_str(" }\n\n");
563 }
564
565 content.push_str("}\n");
566
567 let output_dir = config
571 .php
572 .as_ref()
573 .and_then(|p| p.stubs.as_ref())
574 .map(|s| s.output.to_string_lossy().to_string())
575 .unwrap_or_else(|| "packages/php/src/".to_string());
576
577 Ok(vec![GeneratedFile {
578 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
579 content,
580 generated_header: false,
581 }])
582 }
583
584 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
585 let extension_name = config.php_extension_name();
586 let class_name = extension_name.to_pascal_case();
587
588 let namespace = if extension_name.contains('_') {
590 let parts: Vec<&str> = extension_name.split('_').collect();
591 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
592 ns_parts.join("\\")
593 } else {
594 class_name.clone()
595 };
596
597 let mut content = String::from("<?php\n\n");
602 content.push_str(&hash::header(CommentStyle::DoubleSlash));
603 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
604 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
605 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
606 content.push_str("declare(strict_types=1);\n\n");
607 content.push_str(&format!("namespace {} {{\n\n", namespace));
609
610 content.push_str(&format!(
612 "class {}Exception extends \\RuntimeException\n{{\n",
613 class_name
614 ));
615 content.push_str(" public function getErrorCode(): int { }\n");
616 content.push_str("}\n\n");
617
618 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
620 if typ.is_opaque {
621 if !typ.doc.is_empty() {
622 content.push_str("/**\n");
623 for line in typ.doc.lines() {
624 if line.is_empty() {
625 content.push_str(" *\n");
626 } else {
627 content.push_str(&format!(" * {}\n", line));
628 }
629 }
630 content.push_str(" */\n");
631 }
632 content.push_str(&format!("class {}\n{{\n", typ.name));
633 content.push_str("}\n\n");
635 }
636 }
637
638 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
640 if typ.is_opaque || typ.fields.is_empty() {
641 continue;
642 }
643 if !typ.doc.is_empty() {
644 content.push_str("/**\n");
645 for line in typ.doc.lines() {
646 if line.is_empty() {
647 content.push_str(" *\n");
648 } else {
649 content.push_str(&format!(" * {}\n", line));
650 }
651 }
652 content.push_str(" */\n");
653 }
654 content.push_str(&format!("class {}\n{{\n", typ.name));
655
656 for field in &typ.fields {
658 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
659 let prop_type = if field.optional {
660 let inner = php_type(&field.ty);
661 if inner.starts_with('?') {
662 inner
663 } else {
664 format!("?{inner}")
665 }
666 } else {
667 php_type(&field.ty)
668 };
669 if is_array {
670 let phpdoc = php_phpdoc_type(&field.ty);
671 let nullable_prefix = if field.optional { "?" } else { "" };
672 content.push_str(&format!(" /** @var {}{} */\n", nullable_prefix, phpdoc));
673 }
674 content.push_str(&format!(" public {} ${};\n", prop_type, field.name));
675 }
676 content.push('\n');
677
678 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
682 sorted_fields.sort_by_key(|f| f.optional);
683
684 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
687 .iter()
688 .copied()
689 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
690 .collect();
691 if !array_fields.is_empty() {
692 content.push_str(" /**\n");
693 for f in &array_fields {
694 let phpdoc = php_phpdoc_type(&f.ty);
695 let nullable_prefix = if f.optional { "?" } else { "" };
696 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
697 }
698 content.push_str(" */\n");
699 }
700
701 let params: Vec<String> = sorted_fields
702 .iter()
703 .map(|f| {
704 let ptype = php_type(&f.ty);
705 let nullable = if f.optional && !ptype.starts_with('?') {
706 format!("?{ptype}")
707 } else {
708 ptype
709 };
710 let default = if f.optional { " = null" } else { "" };
711 format!(" {} ${}{}", nullable, f.name, default)
712 })
713 .collect();
714 content.push_str(" public function __construct(\n");
715 content.push_str(¶ms.join(",\n"));
716 content.push_str("\n ) { }\n\n");
717
718 for field in &typ.fields {
720 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
721 let return_type = if field.optional {
722 let inner = php_type(&field.ty);
723 if inner.starts_with('?') {
724 inner
725 } else {
726 format!("?{inner}")
727 }
728 } else {
729 php_type(&field.ty)
730 };
731 let getter_name = field.name.to_lower_camel_case();
732 if is_array {
734 let phpdoc = php_phpdoc_type(&field.ty);
735 let nullable_prefix = if field.optional { "?" } else { "" };
736 content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
737 }
738 content.push_str(&format!(
739 " public function get{}(): {} {{ }}\n",
740 getter_name.to_pascal_case(),
741 return_type
742 ));
743 }
744
745 content.push_str("}\n\n");
746 }
747
748 for enum_def in &api.enums {
750 content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
751 for variant in &enum_def.variants {
752 content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
753 }
754 content.push_str("}\n\n");
755 }
756
757 if !api.functions.is_empty() {
762 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
764 .trait_bridges
765 .iter()
766 .filter_map(|b| b.param_name.as_deref())
767 .collect();
768
769 content.push_str(&format!("class {}Api\n{{\n", class_name));
770 for func in &api.functions {
771 let return_type = php_type_fq(&func.return_type, &namespace);
772 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
773 let visible_params: Vec<_> = func
775 .params
776 .iter()
777 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
778 .collect();
779 let mut sorted_visible_params = visible_params.clone();
781 sorted_visible_params.sort_by_key(|p| p.optional);
782 let has_array_params = visible_params
785 .iter()
786 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
787 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
788 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
789 if has_array_params || has_array_return {
790 content.push_str(" /**\n");
791 for p in &visible_params {
792 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
793 let nullable_prefix = if p.optional { "?" } else { "" };
794 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
795 }
796 content.push_str(&format!(" * @return {}\n", return_phpdoc));
797 content.push_str(" */\n");
798 }
799 let params: Vec<String> = sorted_visible_params
800 .iter()
801 .map(|p| {
802 let ptype = php_type_fq(&p.ty, &namespace);
803 if p.optional {
804 format!("?{} ${} = null", ptype, p.name)
805 } else {
806 format!("{} ${}", ptype, p.name)
807 }
808 })
809 .collect();
810 let stub_method_name = if func.is_async {
812 format!("{}_async", func.name).to_lower_camel_case()
813 } else {
814 func.name.to_lower_camel_case()
815 };
816 content.push_str(&format!(
817 " public static function {}({}): {} {{ }}\n",
818 stub_method_name,
819 params.join(", "),
820 return_type
821 ));
822 }
823 content.push_str("}\n\n");
824 }
825
826 content.push_str("} // end namespace\n");
828
829 let output_dir = config
831 .php
832 .as_ref()
833 .and_then(|p| p.stubs.as_ref())
834 .map(|s| s.output.to_string_lossy().to_string())
835 .unwrap_or_else(|| "packages/php/stubs/".to_string());
836
837 Ok(vec![GeneratedFile {
838 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
839 content,
840 generated_header: false,
841 }])
842 }
843
844 fn build_config(&self) -> Option<BuildConfig> {
845 Some(BuildConfig {
846 tool: "cargo",
847 crate_suffix: "-php",
848 build_dep: BuildDependency::None,
849 post_build: vec![],
850 })
851 }
852}
853
854fn php_phpdoc_type(ty: &TypeRef) -> String {
857 match ty {
858 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
859 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
860 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
861 _ => php_type(ty),
862 }
863}
864
865fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
867 match ty {
868 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
869 TypeRef::Map(k, v) => format!(
870 "array<{}, {}>",
871 php_phpdoc_type_fq(k, namespace),
872 php_phpdoc_type_fq(v, namespace)
873 ),
874 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
875 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
876 _ => php_type(ty),
877 }
878}
879
880fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
882 match ty {
883 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
884 TypeRef::Optional(inner) => {
885 let inner_type = php_type_fq(inner, namespace);
886 if inner_type.starts_with('?') {
887 inner_type
888 } else {
889 format!("?{inner_type}")
890 }
891 }
892 _ => php_type(ty),
893 }
894}
895
896fn php_type(ty: &TypeRef) -> String {
898 match ty {
899 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
900 TypeRef::Primitive(p) => match p {
901 PrimitiveType::Bool => "bool".to_string(),
902 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
903 PrimitiveType::U8
904 | PrimitiveType::U16
905 | PrimitiveType::U32
906 | PrimitiveType::U64
907 | PrimitiveType::I8
908 | PrimitiveType::I16
909 | PrimitiveType::I32
910 | PrimitiveType::I64
911 | PrimitiveType::Usize
912 | PrimitiveType::Isize => "int".to_string(),
913 },
914 TypeRef::Optional(inner) => {
915 let inner_type = php_type(inner);
918 if inner_type.starts_with('?') {
919 inner_type
920 } else {
921 format!("?{inner_type}")
922 }
923 }
924 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
925 TypeRef::Named(name) => name.clone(),
926 TypeRef::Unit => "void".to_string(),
927 TypeRef::Duration => "float".to_string(),
928 }
929}