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 }
48 }
49}
50
51impl Backend for PhpBackend {
52 fn name(&self) -> &str {
53 "php"
54 }
55
56 fn language(&self) -> Language {
57 Language::Php
58 }
59
60 fn capabilities(&self) -> Capabilities {
61 Capabilities {
62 supports_async: true,
63 supports_classes: true,
64 supports_enums: true,
65 supports_option: true,
66 supports_result: true,
67 ..Capabilities::default()
68 }
69 }
70
71 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
72 let enum_names = api.enums.iter().map(|e| e.name.clone()).collect();
73 let mapper = PhpMapper { enum_names };
74 let core_import = config.core_import();
75
76 let php_config = config.php.as_ref();
78 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
79 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
80
81 let output_dir = resolve_output_dir(
82 config.output.php.as_ref(),
83 &config.crate_config.name,
84 "crates/{name}-php/src/",
85 );
86 let has_serde = detect_serde_available(&output_dir);
87 let cfg = Self::binding_config(&core_import, has_serde);
88
89 let mut builder = RustFileBuilder::new();
91 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
92 builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits)");
93 builder.add_import("ext_php_rs::prelude::*");
94
95 if has_serde {
97 builder.add_import("serde_json");
98 }
99
100 for trait_path in generators::collect_trait_imports(api) {
102 builder.add_import(&trait_path);
103 }
104
105 let has_maps = api.types.iter().any(|t| {
107 t.fields
108 .iter()
109 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
110 }) || api
111 .functions
112 .iter()
113 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
114 if has_maps {
115 builder.add_import("std::collections::HashMap");
116 }
117
118 let custom_mods = config.custom_modules.for_language(Language::Php);
120 for module in custom_mods {
121 builder.add_item(&format!("pub mod {module};"));
122 }
123
124 let has_async =
126 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
127
128 if has_async {
129 builder.add_item(&gen_tokio_runtime());
130 }
131
132 let opaque_types: AHashSet<String> = api
134 .types
135 .iter()
136 .filter(|t| t.is_opaque)
137 .map(|t| t.name.clone())
138 .collect();
139 if !opaque_types.is_empty() {
140 builder.add_import("std::sync::Arc");
141 }
142
143 let enum_names: AHashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
146
147 let extension_name = config.php_extension_name();
150 let php_namespace = if extension_name.contains('_') {
151 let parts: Vec<&str> = extension_name.split('_').collect();
152 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
153 ns_parts.join("\\")
154 } else {
155 extension_name.to_pascal_case()
156 };
157
158 for typ in api
159 .types
160 .iter()
161 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
162 {
163 if typ.is_opaque {
164 let ns_escaped = php_namespace.replace('\\', "\\\\");
168 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
169 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
170 let opaque_cfg = RustBindingConfig {
171 struct_attrs: &opaque_attr_arr,
172 ..cfg
173 };
174 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
175 builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &opaque_types, &core_import));
176 } else {
177 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
180 builder.add_item(&types::gen_struct_methods_with_exclude(
181 typ,
182 &mapper,
183 has_serde,
184 &core_import,
185 &opaque_types,
186 &enum_names,
187 &api.enums,
188 &exclude_functions,
189 ));
190 }
191 }
192
193 for enum_def in &api.enums {
194 builder.add_item(&gen_enum_constants(enum_def));
195 }
196
197 let included_functions: Vec<_> = api
202 .functions
203 .iter()
204 .filter(|f| !exclude_functions.contains(&f.name))
205 .collect();
206 if !included_functions.is_empty() {
207 let facade_class_name = extension_name.to_pascal_case();
208 let mut method_items: Vec<String> = Vec::new();
211 for func in included_functions {
212 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
213 if let Some((param_idx, bridge_cfg)) = bridge_param {
214 method_items.push(crate::trait_bridge::gen_bridge_function(
215 func,
216 param_idx,
217 bridge_cfg,
218 &mapper,
219 &opaque_types,
220 &core_import,
221 ));
222 } else if func.is_async {
223 method_items.push(gen_async_function_as_static_method(
224 func,
225 &mapper,
226 &opaque_types,
227 &core_import,
228 &config.trait_bridges,
229 ));
230 } else {
231 method_items.push(gen_function_as_static_method(
232 func,
233 &mapper,
234 &opaque_types,
235 &core_import,
236 &config.trait_bridges,
237 ));
238 }
239 }
240
241 let methods_joined = method_items
242 .iter()
243 .map(|m| {
244 m.lines()
246 .map(|l| {
247 if l.is_empty() {
248 String::new()
249 } else {
250 format!(" {l}")
251 }
252 })
253 .collect::<Vec<_>>()
254 .join("\n")
255 })
256 .collect::<Vec<_>>()
257 .join("\n\n");
258 let php_api_class_name = format!("{facade_class_name}Api");
261 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
263 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
264 let facade_struct = format!(
265 "#[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}}"
266 );
267 builder.add_item(&facade_struct);
268
269 for bridge_cfg in &config.trait_bridges {
271 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
272 let bridge_code = crate::trait_bridge::gen_trait_bridge(trait_type, bridge_cfg, &core_import, api);
273 builder.add_item(&bridge_code);
274 }
275 }
276 }
277
278 let convertible = alef_codegen::conversions::convertible_types(api);
279 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
280 let input_types = alef_codegen::conversions::input_type_names(api);
281 let enum_names_ref = &mapper.enum_names;
286 let php_conv_config = ConversionConfig {
287 cast_large_ints_to_i64: true,
288 enum_string_names: Some(enum_names_ref),
289 json_to_string: true,
290 include_cfg_metadata: false,
291 option_duration_on_defaults: true,
292 ..Default::default()
293 };
294 let mut enum_tainted: AHashSet<String> = AHashSet::new();
296 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
297 if has_enum_named_field(typ, enum_names_ref) {
298 enum_tainted.insert(typ.name.clone());
299 }
300 }
301 let mut changed = true;
303 while changed {
304 changed = false;
305 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
306 if !enum_tainted.contains(&typ.name)
307 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
308 {
309 enum_tainted.insert(typ.name.clone());
310 changed = true;
311 }
312 }
313 }
314 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
315 if input_types.contains(&typ.name)
317 && !enum_tainted.contains(&typ.name)
318 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
319 {
320 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
321 typ,
322 &core_import,
323 &php_conv_config,
324 ));
325 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) && has_serde {
326 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
329 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
330 builder.add_item(&gen_enum_tainted_from_binding_to_core(
334 typ,
335 &core_import,
336 enum_names_ref,
337 &enum_tainted,
338 &php_conv_config,
339 &api.enums,
340 ));
341 }
342 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
344 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
345 typ,
346 &core_import,
347 &opaque_types,
348 &php_conv_config,
349 ));
350 }
351 }
352
353 for error in &api.errors {
355 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
356 }
357
358 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
359
360 let php_config = config.php.as_ref();
362 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
363 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
364 builder.add_inner_attribute(&format!(
365 "cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
366 ));
367 }
368
369 let mut class_registrations = String::new();
372 for typ in api
373 .types
374 .iter()
375 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
376 {
377 class_registrations.push_str(&format!("\n .class::<{}>()", typ.name));
378 }
379 if !api.functions.is_empty() {
381 let facade_class_name = extension_name.to_pascal_case();
382 class_registrations.push_str(&format!("\n .class::<{facade_class_name}Api>()"));
383 }
384 builder.add_item(&format!(
387 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
388 ));
389
390 let content = builder.build();
391
392 Ok(vec![GeneratedFile {
393 path: PathBuf::from(&output_dir).join("lib.rs"),
394 content,
395 generated_header: false,
396 }])
397 }
398
399 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
400 let extension_name = config.php_extension_name();
401 let class_name = extension_name.to_pascal_case();
402
403 let mut content = String::from("<?php\n");
405 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
406 content.push_str("declare(strict_types=1);\n\n");
407
408 let namespace = if extension_name.contains('_') {
410 let parts: Vec<&str> = extension_name.split('_').collect();
411 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
412 ns_parts.join("\\")
413 } else {
414 class_name.clone()
415 };
416
417 content.push_str(&format!("namespace {};\n\n", namespace));
418 content.push_str(&format!("final class {}\n", class_name));
419 content.push_str("{\n");
420
421 let bridge_param_names_pub: ahash::AHashSet<&str> = config
423 .trait_bridges
424 .iter()
425 .filter_map(|b| b.param_name.as_deref())
426 .collect();
427
428 for func in &api.functions {
430 let method_name = func.name.to_lower_camel_case();
431 let return_php_type = php_type(&func.return_type);
432
433 let visible_params: Vec<_> = func
435 .params
436 .iter()
437 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
438 .collect();
439
440 content.push_str(" /**\n");
442 for line in func.doc.lines() {
443 if line.is_empty() {
444 content.push_str(" *\n");
445 } else {
446 content.push_str(&format!(" * {}\n", line));
447 }
448 }
449 if func.doc.is_empty() {
450 content.push_str(&format!(" * {}.\n", method_name));
451 }
452 content.push_str(" *\n");
453 for p in &visible_params {
454 let ptype = php_phpdoc_type(&p.ty);
455 let nullable_prefix = if p.optional { "?" } else { "" };
456 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
457 }
458 let return_phpdoc = php_phpdoc_type(&func.return_type);
459 content.push_str(&format!(" * @return {}\n", return_phpdoc));
460 if func.error_type.is_some() {
461 content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
462 }
463 content.push_str(" */\n");
464
465 content.push_str(&format!(" public static function {}(", method_name));
467
468 let params: Vec<String> = visible_params
469 .iter()
470 .map(|p| {
471 let ptype = php_type(&p.ty);
472 if p.optional {
473 format!("?{} ${} = null", ptype, p.name)
474 } else {
475 format!("{} ${}", ptype, p.name)
476 }
477 })
478 .collect();
479 content.push_str(¶ms.join(", "));
480 content.push_str(&format!("): {}\n", return_php_type));
481 content.push_str(" {\n");
482 let ext_method_name = if func.is_async {
487 format!("{}_async", func.name).to_lower_camel_case()
488 } else {
489 func.name.to_lower_camel_case()
490 };
491 let is_void = matches!(&func.return_type, TypeRef::Unit);
492 let call_expr = format!(
493 "\\{}\\{}Api::{}({})",
494 namespace,
495 class_name,
496 ext_method_name,
497 visible_params
498 .iter()
499 .map(|p| format!("${}", p.name))
500 .collect::<Vec<_>>()
501 .join(", ")
502 );
503 if is_void {
504 content.push_str(&format!(
505 " {}; // delegate to native extension class\n",
506 call_expr
507 ));
508 } else {
509 content.push_str(&format!(
510 " return {}; // delegate to native extension class\n",
511 call_expr
512 ));
513 }
514 content.push_str(" }\n\n");
515 }
516
517 content.push_str("}\n");
518
519 let output_dir = config
523 .php
524 .as_ref()
525 .and_then(|p| p.stubs.as_ref())
526 .map(|s| s.output.to_string_lossy().to_string())
527 .unwrap_or_else(|| "packages/php/src/".to_string());
528
529 Ok(vec![GeneratedFile {
530 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
531 content,
532 generated_header: false,
533 }])
534 }
535
536 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
537 let extension_name = config.php_extension_name();
538 let class_name = extension_name.to_pascal_case();
539
540 let namespace = if extension_name.contains('_') {
542 let parts: Vec<&str> = extension_name.split('_').collect();
543 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
544 ns_parts.join("\\")
545 } else {
546 class_name.clone()
547 };
548
549 let mut content = String::from("<?php\n");
550 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
551 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
552 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
553 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
554 content.push_str("declare(strict_types=1);\n\n");
555 content.push_str(&format!("namespace {} {{\n\n", namespace));
557
558 content.push_str(&format!(
560 "class {}Exception extends \\RuntimeException\n{{\n",
561 class_name
562 ));
563 content.push_str(" public function getErrorCode(): int { }\n");
564 content.push_str("}\n\n");
565
566 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
568 if typ.is_opaque {
569 if !typ.doc.is_empty() {
570 content.push_str("/**\n");
571 for line in typ.doc.lines() {
572 if line.is_empty() {
573 content.push_str(" *\n");
574 } else {
575 content.push_str(&format!(" * {}\n", line));
576 }
577 }
578 content.push_str(" */\n");
579 }
580 content.push_str(&format!("class {}\n{{\n", typ.name));
581 content.push_str("}\n\n");
583 }
584 }
585
586 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
588 if typ.is_opaque || typ.fields.is_empty() {
589 continue;
590 }
591 if !typ.doc.is_empty() {
592 content.push_str("/**\n");
593 for line in typ.doc.lines() {
594 if line.is_empty() {
595 content.push_str(" *\n");
596 } else {
597 content.push_str(&format!(" * {}\n", line));
598 }
599 }
600 content.push_str(" */\n");
601 }
602 content.push_str(&format!("class {}\n{{\n", typ.name));
603
604 for field in &typ.fields {
606 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
607 let prop_type = if field.optional {
608 let inner = php_type(&field.ty);
609 if inner.starts_with('?') {
610 inner
611 } else {
612 format!("?{inner}")
613 }
614 } else {
615 php_type(&field.ty)
616 };
617 if is_array {
618 let phpdoc = php_phpdoc_type(&field.ty);
619 let nullable_prefix = if field.optional { "?" } else { "" };
620 content.push_str(&format!(" /** @var {}{} */\n", nullable_prefix, phpdoc));
621 }
622 content.push_str(&format!(" public {} ${};\n", prop_type, field.name));
623 }
624 content.push('\n');
625
626 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
630 sorted_fields.sort_by_key(|f| f.optional);
631
632 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
635 .iter()
636 .copied()
637 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
638 .collect();
639 if !array_fields.is_empty() {
640 content.push_str(" /**\n");
641 for f in &array_fields {
642 let phpdoc = php_phpdoc_type(&f.ty);
643 let nullable_prefix = if f.optional { "?" } else { "" };
644 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
645 }
646 content.push_str(" */\n");
647 }
648
649 let params: Vec<String> = sorted_fields
650 .iter()
651 .map(|f| {
652 let ptype = php_type(&f.ty);
653 let nullable = if f.optional && !ptype.starts_with('?') {
654 format!("?{ptype}")
655 } else {
656 ptype
657 };
658 let default = if f.optional { " = null" } else { "" };
659 format!(" {} ${}{}", nullable, f.name, default)
660 })
661 .collect();
662 content.push_str(" public function __construct(\n");
663 content.push_str(¶ms.join(",\n"));
664 content.push_str("\n ) { }\n\n");
665
666 for field in &typ.fields {
668 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
669 let return_type = if field.optional {
670 let inner = php_type(&field.ty);
671 if inner.starts_with('?') {
672 inner
673 } else {
674 format!("?{inner}")
675 }
676 } else {
677 php_type(&field.ty)
678 };
679 let getter_name = field.name.to_lower_camel_case();
680 if is_array {
682 let phpdoc = php_phpdoc_type(&field.ty);
683 let nullable_prefix = if field.optional { "?" } else { "" };
684 content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
685 }
686 content.push_str(&format!(
687 " public function get{}(): {} {{ }}\n",
688 getter_name.to_pascal_case(),
689 return_type
690 ));
691 }
692
693 content.push_str("}\n\n");
694 }
695
696 for enum_def in &api.enums {
698 content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
699 for variant in &enum_def.variants {
700 content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
701 }
702 content.push_str("}\n\n");
703 }
704
705 if !api.functions.is_empty() {
710 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
712 .trait_bridges
713 .iter()
714 .filter_map(|b| b.param_name.as_deref())
715 .collect();
716
717 content.push_str(&format!("class {}Api\n{{\n", class_name));
718 for func in &api.functions {
719 let return_type = php_type_fq(&func.return_type, &namespace);
720 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
721 let visible_params: Vec<_> = func
723 .params
724 .iter()
725 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
726 .collect();
727 let has_array_params = visible_params
729 .iter()
730 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
731 if has_array_params {
732 content.push_str(" /**\n");
733 for p in &visible_params {
734 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
735 let nullable_prefix = if p.optional { "?" } else { "" };
736 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
737 }
738 content.push_str(&format!(" * @return {}\n", return_phpdoc));
739 content.push_str(" */\n");
740 }
741 let params: Vec<String> = visible_params
742 .iter()
743 .map(|p| {
744 let ptype = php_type_fq(&p.ty, &namespace);
745 if p.optional {
746 format!("?{} ${} = null", ptype, p.name)
747 } else {
748 format!("{} ${}", ptype, p.name)
749 }
750 })
751 .collect();
752 let stub_method_name = if func.is_async {
754 format!("{}_async", func.name).to_lower_camel_case()
755 } else {
756 func.name.to_lower_camel_case()
757 };
758 content.push_str(&format!(
759 " public static function {}({}): {} {{ }}\n",
760 stub_method_name,
761 params.join(", "),
762 return_type
763 ));
764 }
765 content.push_str("}\n\n");
766 }
767
768 content.push_str("} // end namespace\n");
770
771 let output_dir = config
773 .php
774 .as_ref()
775 .and_then(|p| p.stubs.as_ref())
776 .map(|s| s.output.to_string_lossy().to_string())
777 .unwrap_or_else(|| "packages/php/stubs/".to_string());
778
779 Ok(vec![GeneratedFile {
780 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
781 content,
782 generated_header: false,
783 }])
784 }
785
786 fn build_config(&self) -> Option<BuildConfig> {
787 Some(BuildConfig {
788 tool: "cargo",
789 crate_suffix: "-php",
790 depends_on_ffi: false,
791 post_build: vec![],
792 })
793 }
794}
795
796fn php_phpdoc_type(ty: &TypeRef) -> String {
799 match ty {
800 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
801 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
802 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
803 _ => php_type(ty),
804 }
805}
806
807fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
809 match ty {
810 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
811 TypeRef::Map(k, v) => format!(
812 "array<{}, {}>",
813 php_phpdoc_type_fq(k, namespace),
814 php_phpdoc_type_fq(v, namespace)
815 ),
816 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
817 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
818 _ => php_type(ty),
819 }
820}
821
822fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
824 match ty {
825 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
826 TypeRef::Optional(inner) => {
827 let inner_type = php_type_fq(inner, namespace);
828 if inner_type.starts_with('?') {
829 inner_type
830 } else {
831 format!("?{inner_type}")
832 }
833 }
834 _ => php_type(ty),
835 }
836}
837
838fn php_type(ty: &TypeRef) -> String {
840 match ty {
841 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
842 TypeRef::Primitive(p) => match p {
843 PrimitiveType::Bool => "bool".to_string(),
844 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
845 PrimitiveType::U8
846 | PrimitiveType::U16
847 | PrimitiveType::U32
848 | PrimitiveType::U64
849 | PrimitiveType::I8
850 | PrimitiveType::I16
851 | PrimitiveType::I32
852 | PrimitiveType::I64
853 | PrimitiveType::Usize
854 | PrimitiveType::Isize => "int".to_string(),
855 },
856 TypeRef::Optional(inner) => {
857 let inner_type = php_type(inner);
860 if inner_type.starts_with('?') {
861 inner_type
862 } else {
863 format!("?{inner_type}")
864 }
865 }
866 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
867 TypeRef::Named(name) => name.clone(),
868 TypeRef::Unit => "void".to_string(),
869 TypeRef::Duration => "float".to_string(),
870 }
871}