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