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)");
92 builder.add_import("ext_php_rs::prelude::*");
93
94 if has_serde {
96 builder.add_import("serde_json");
97 }
98
99 for trait_path in generators::collect_trait_imports(api) {
101 builder.add_import(&trait_path);
102 }
103
104 let has_maps = api.types.iter().any(|t| {
106 t.fields
107 .iter()
108 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
109 }) || api
110 .functions
111 .iter()
112 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
113 if has_maps {
114 builder.add_import("std::collections::HashMap");
115 }
116
117 let custom_mods = config.custom_modules.for_language(Language::Php);
119 for module in custom_mods {
120 builder.add_item(&format!("pub mod {module};"));
121 }
122
123 let has_async =
125 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
126
127 if has_async {
128 builder.add_item(&gen_tokio_runtime());
129 }
130
131 let opaque_types: AHashSet<String> = api
133 .types
134 .iter()
135 .filter(|t| t.is_opaque)
136 .map(|t| t.name.clone())
137 .collect();
138 if !opaque_types.is_empty() {
139 builder.add_import("std::sync::Arc");
140 }
141
142 let enum_names: AHashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
145
146 let extension_name = config.php_extension_name();
149 let php_namespace = if extension_name.contains('_') {
150 let parts: Vec<&str> = extension_name.split('_').collect();
151 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
152 ns_parts.join("\\")
153 } else {
154 extension_name.to_pascal_case()
155 };
156
157 for typ in api
158 .types
159 .iter()
160 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
161 {
162 if typ.is_opaque {
163 let ns_escaped = php_namespace.replace('\\', "\\\\");
167 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
168 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
169 let opaque_cfg = RustBindingConfig {
170 struct_attrs: &opaque_attr_arr,
171 ..cfg
172 };
173 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
174 builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &opaque_types, &core_import));
175 } else {
176 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
179 builder.add_item(&types::gen_struct_methods_with_exclude(
180 typ,
181 &mapper,
182 has_serde,
183 &core_import,
184 &opaque_types,
185 &enum_names,
186 &api.enums,
187 &exclude_functions,
188 ));
189 }
190 }
191
192 for enum_def in &api.enums {
193 builder.add_item(&gen_enum_constants(enum_def));
194 }
195
196 let included_functions: Vec<_> = api
201 .functions
202 .iter()
203 .filter(|f| !exclude_functions.contains(&f.name))
204 .collect();
205 if !included_functions.is_empty() {
206 let facade_class_name = extension_name.to_pascal_case();
207 let mut method_items: Vec<String> = Vec::new();
210 for func in included_functions {
211 if func.is_async {
212 method_items.push(gen_async_function_as_static_method(
213 func,
214 &mapper,
215 &opaque_types,
216 &core_import,
217 ));
218 } else {
219 method_items.push(gen_function_as_static_method(
220 func,
221 &mapper,
222 &opaque_types,
223 &core_import,
224 ));
225 }
226 }
227 let methods_joined = method_items
228 .iter()
229 .map(|m| {
230 m.lines()
232 .map(|l| {
233 if l.is_empty() {
234 String::new()
235 } else {
236 format!(" {l}")
237 }
238 })
239 .collect::<Vec<_>>()
240 .join("\n")
241 })
242 .collect::<Vec<_>>()
243 .join("\n\n");
244 let php_api_class_name = format!("{facade_class_name}Api");
247 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
249 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
250 let facade_struct = format!(
251 "#[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}}"
252 );
253 builder.add_item(&facade_struct);
254 }
255
256 let convertible = alef_codegen::conversions::convertible_types(api);
257 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
258 let input_types = alef_codegen::conversions::input_type_names(api);
259 let enum_names_ref = &mapper.enum_names;
264 let php_conv_config = ConversionConfig {
265 cast_large_ints_to_i64: true,
266 enum_string_names: Some(enum_names_ref),
267 json_to_string: true,
268 include_cfg_metadata: false,
269 option_duration_on_defaults: true,
270 ..Default::default()
271 };
272 let mut enum_tainted: AHashSet<String> = AHashSet::new();
274 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
275 if has_enum_named_field(typ, enum_names_ref) {
276 enum_tainted.insert(typ.name.clone());
277 }
278 }
279 let mut changed = true;
281 while changed {
282 changed = false;
283 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
284 if !enum_tainted.contains(&typ.name)
285 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
286 {
287 enum_tainted.insert(typ.name.clone());
288 changed = true;
289 }
290 }
291 }
292 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
293 if input_types.contains(&typ.name)
295 && !enum_tainted.contains(&typ.name)
296 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
297 {
298 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
299 typ,
300 &core_import,
301 &php_conv_config,
302 ));
303 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) && has_serde {
304 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
307 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
308 builder.add_item(&gen_enum_tainted_from_binding_to_core(
312 typ,
313 &core_import,
314 enum_names_ref,
315 &enum_tainted,
316 &php_conv_config,
317 &api.enums,
318 ));
319 }
320 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
322 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
323 typ,
324 &core_import,
325 &opaque_types,
326 &php_conv_config,
327 ));
328 }
329 }
330
331 for error in &api.errors {
333 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
334 }
335
336 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
337
338 let php_config = config.php.as_ref();
340 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
341 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
342 builder.add_inner_attribute(&format!(
343 "cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
344 ));
345 }
346
347 let mut class_registrations = String::new();
350 for typ in api
351 .types
352 .iter()
353 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
354 {
355 class_registrations.push_str(&format!("\n .class::<{}>()", typ.name));
356 }
357 if !api.functions.is_empty() {
359 let facade_class_name = extension_name.to_pascal_case();
360 class_registrations.push_str(&format!("\n .class::<{facade_class_name}Api>()"));
361 }
362 builder.add_item(&format!(
365 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
366 ));
367
368 let content = builder.build();
369
370 Ok(vec![GeneratedFile {
371 path: PathBuf::from(&output_dir).join("lib.rs"),
372 content,
373 generated_header: false,
374 }])
375 }
376
377 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
378 let extension_name = config.php_extension_name();
379 let class_name = extension_name.to_pascal_case();
380
381 let mut content = String::from("<?php\n");
383 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
384 content.push_str("declare(strict_types=1);\n\n");
385
386 let namespace = if extension_name.contains('_') {
388 let parts: Vec<&str> = extension_name.split('_').collect();
389 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
390 ns_parts.join("\\")
391 } else {
392 class_name.clone()
393 };
394
395 content.push_str(&format!("namespace {};\n\n", namespace));
396 content.push_str(&format!("final class {}\n", class_name));
397 content.push_str("{\n");
398
399 for func in &api.functions {
401 let method_name = func.name.to_lower_camel_case();
402 let return_php_type = php_type(&func.return_type);
403
404 content.push_str(" /**\n");
406 for line in func.doc.lines() {
407 if line.is_empty() {
408 content.push_str(" *\n");
409 } else {
410 content.push_str(&format!(" * {}\n", line));
411 }
412 }
413 if func.doc.is_empty() {
414 content.push_str(&format!(" * {}.\n", method_name));
415 }
416 content.push_str(" *\n");
417 for p in &func.params {
418 let ptype = php_phpdoc_type(&p.ty);
419 let nullable_prefix = if p.optional { "?" } else { "" };
420 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
421 }
422 let return_phpdoc = php_phpdoc_type(&func.return_type);
423 content.push_str(&format!(" * @return {}\n", return_phpdoc));
424 if func.error_type.is_some() {
425 content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
426 }
427 content.push_str(" */\n");
428
429 content.push_str(&format!(" public static function {}(", method_name));
431
432 let params: Vec<String> = func
433 .params
434 .iter()
435 .map(|p| {
436 let ptype = php_type(&p.ty);
437 if p.optional {
438 format!("?{} ${} = null", ptype, p.name)
439 } else {
440 format!("{} ${}", ptype, p.name)
441 }
442 })
443 .collect();
444 content.push_str(¶ms.join(", "));
445 content.push_str(&format!("): {}\n", return_php_type));
446 content.push_str(" {\n");
447 let ext_method_name = if func.is_async {
452 format!("{}_async", func.name).to_lower_camel_case()
453 } else {
454 func.name.to_lower_camel_case()
455 };
456 content.push_str(&format!(
457 " return \\{}\\{}Api::{}({}); // delegate to native extension class\n",
458 namespace,
459 class_name,
460 ext_method_name,
461 func.params
462 .iter()
463 .map(|p| format!("${}", p.name))
464 .collect::<Vec<_>>()
465 .join(", ")
466 ));
467 content.push_str(" }\n\n");
468 }
469
470 content.push_str("}\n");
471
472 let output_dir = config
476 .php
477 .as_ref()
478 .and_then(|p| p.stubs.as_ref())
479 .map(|s| s.output.to_string_lossy().to_string())
480 .unwrap_or_else(|| "packages/php/src/".to_string());
481
482 Ok(vec![GeneratedFile {
483 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
484 content,
485 generated_header: false,
486 }])
487 }
488
489 fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
490 let extension_name = config.php_extension_name();
491 let class_name = extension_name.to_pascal_case();
492
493 let namespace = if extension_name.contains('_') {
495 let parts: Vec<&str> = extension_name.split('_').collect();
496 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
497 ns_parts.join("\\")
498 } else {
499 class_name.clone()
500 };
501
502 let mut content = String::from("<?php\n");
503 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
504 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
505 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
506 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
507 content.push_str("declare(strict_types=1);\n\n");
508 content.push_str(&format!("namespace {} {{\n\n", namespace));
510
511 content.push_str(&format!(
513 "class {}Exception extends \\RuntimeException\n{{\n",
514 class_name
515 ));
516 content.push_str(" public function getErrorCode(): int { }\n");
517 content.push_str("}\n\n");
518
519 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
521 if typ.is_opaque {
522 if !typ.doc.is_empty() {
523 content.push_str("/**\n");
524 for line in typ.doc.lines() {
525 if line.is_empty() {
526 content.push_str(" *\n");
527 } else {
528 content.push_str(&format!(" * {}\n", line));
529 }
530 }
531 content.push_str(" */\n");
532 }
533 content.push_str(&format!("class {}\n{{\n", typ.name));
534 content.push_str("}\n\n");
536 }
537 }
538
539 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
541 if typ.is_opaque || typ.fields.is_empty() {
542 continue;
543 }
544 if !typ.doc.is_empty() {
545 content.push_str("/**\n");
546 for line in typ.doc.lines() {
547 if line.is_empty() {
548 content.push_str(" *\n");
549 } else {
550 content.push_str(&format!(" * {}\n", line));
551 }
552 }
553 content.push_str(" */\n");
554 }
555 content.push_str(&format!("class {}\n{{\n", typ.name));
556
557 for field in &typ.fields {
559 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
560 let prop_type = if field.optional {
561 let inner = php_type(&field.ty);
562 if inner.starts_with('?') {
563 inner
564 } else {
565 format!("?{inner}")
566 }
567 } else {
568 php_type(&field.ty)
569 };
570 if is_array {
571 let phpdoc = php_phpdoc_type(&field.ty);
572 let nullable_prefix = if field.optional { "?" } else { "" };
573 content.push_str(&format!(" /** @var {}{} */\n", nullable_prefix, phpdoc));
574 }
575 content.push_str(&format!(" public {} ${};\n", prop_type, field.name));
576 }
577 content.push('\n');
578
579 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
583 sorted_fields.sort_by_key(|f| f.optional);
584
585 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
588 .iter()
589 .copied()
590 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
591 .collect();
592 if !array_fields.is_empty() {
593 content.push_str(" /**\n");
594 for f in &array_fields {
595 let phpdoc = php_phpdoc_type(&f.ty);
596 let nullable_prefix = if f.optional { "?" } else { "" };
597 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
598 }
599 content.push_str(" */\n");
600 }
601
602 let params: Vec<String> = sorted_fields
603 .iter()
604 .map(|f| {
605 let ptype = php_type(&f.ty);
606 let nullable = if f.optional && !ptype.starts_with('?') {
607 format!("?{ptype}")
608 } else {
609 ptype
610 };
611 let default = if f.optional { " = null" } else { "" };
612 format!(" {} ${}{}", nullable, f.name, default)
613 })
614 .collect();
615 content.push_str(" public function __construct(\n");
616 content.push_str(¶ms.join(",\n"));
617 content.push_str("\n ) { }\n\n");
618
619 for field in &typ.fields {
621 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
622 let return_type = if field.optional {
623 let inner = php_type(&field.ty);
624 if inner.starts_with('?') {
625 inner
626 } else {
627 format!("?{inner}")
628 }
629 } else {
630 php_type(&field.ty)
631 };
632 let getter_name = field.name.to_lower_camel_case();
633 if is_array {
635 let phpdoc = php_phpdoc_type(&field.ty);
636 let nullable_prefix = if field.optional { "?" } else { "" };
637 content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
638 }
639 content.push_str(&format!(
640 " public function get{}(): {} {{ }}\n",
641 getter_name.to_pascal_case(),
642 return_type
643 ));
644 }
645
646 content.push_str("}\n\n");
647 }
648
649 for enum_def in &api.enums {
651 content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
652 for variant in &enum_def.variants {
653 content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
654 }
655 content.push_str("}\n\n");
656 }
657
658 if !api.functions.is_empty() {
663 content.push_str(&format!("class {}Api\n{{\n", class_name));
664 for func in &api.functions {
665 let return_type = php_type_fq(&func.return_type, &namespace);
666 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
667 let has_array_params = func
669 .params
670 .iter()
671 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
672 if has_array_params {
673 content.push_str(" /**\n");
674 for p in &func.params {
675 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
676 let nullable_prefix = if p.optional { "?" } else { "" };
677 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
678 }
679 content.push_str(&format!(" * @return {}\n", return_phpdoc));
680 content.push_str(" */\n");
681 }
682 let params: Vec<String> = func
683 .params
684 .iter()
685 .map(|p| {
686 let ptype = php_type_fq(&p.ty, &namespace);
687 if p.optional {
688 format!("?{} ${} = null", ptype, p.name)
689 } else {
690 format!("{} ${}", ptype, p.name)
691 }
692 })
693 .collect();
694 let stub_method_name = if func.is_async {
696 format!("{}_async", func.name).to_lower_camel_case()
697 } else {
698 func.name.to_lower_camel_case()
699 };
700 content.push_str(&format!(
701 " public static function {}({}): {} {{ }}\n",
702 stub_method_name,
703 params.join(", "),
704 return_type
705 ));
706 }
707 content.push_str("}\n\n");
708 }
709
710 content.push_str("} // end namespace\n");
712
713 let output_dir = config
715 .php
716 .as_ref()
717 .and_then(|p| p.stubs.as_ref())
718 .map(|s| s.output.to_string_lossy().to_string())
719 .unwrap_or_else(|| "packages/php/stubs/".to_string());
720
721 Ok(vec![GeneratedFile {
722 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
723 content,
724 generated_header: false,
725 }])
726 }
727
728 fn build_config(&self) -> Option<BuildConfig> {
729 Some(BuildConfig {
730 tool: "cargo",
731 crate_suffix: "-php",
732 depends_on_ffi: false,
733 post_build: vec![],
734 })
735 }
736}
737
738fn php_phpdoc_type(ty: &TypeRef) -> String {
741 match ty {
742 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
743 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
744 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
745 _ => php_type(ty),
746 }
747}
748
749fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
751 match ty {
752 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
753 TypeRef::Map(k, v) => format!(
754 "array<{}, {}>",
755 php_phpdoc_type_fq(k, namespace),
756 php_phpdoc_type_fq(v, namespace)
757 ),
758 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
759 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
760 _ => php_type(ty),
761 }
762}
763
764fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
766 match ty {
767 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
768 TypeRef::Optional(inner) => {
769 let inner_type = php_type_fq(inner, namespace);
770 if inner_type.starts_with('?') {
771 inner_type
772 } else {
773 format!("?{inner_type}")
774 }
775 }
776 _ => php_type(ty),
777 }
778}
779
780fn php_type(ty: &TypeRef) -> String {
782 match ty {
783 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
784 TypeRef::Primitive(p) => match p {
785 PrimitiveType::Bool => "bool".to_string(),
786 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
787 PrimitiveType::U8
788 | PrimitiveType::U16
789 | PrimitiveType::U32
790 | PrimitiveType::U64
791 | PrimitiveType::I8
792 | PrimitiveType::I16
793 | PrimitiveType::I32
794 | PrimitiveType::I64
795 | PrimitiveType::Usize
796 | PrimitiveType::Isize => "int".to_string(),
797 },
798 TypeRef::Optional(inner) => {
799 let inner_type = php_type(inner);
802 if inner_type.starts_with('?') {
803 inner_type
804 } else {
805 format!("?{inner_type}")
806 }
807 }
808 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
809 TypeRef::Named(name) => name.clone(),
810 TypeRef::Unit => "void".to_string(),
811 TypeRef::Duration => "float".to_string(),
812 }
813}