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::{Language, ResolvedCrateConfig, 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 crate::naming::php_autoload_namespace;
20use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
21use helpers::{gen_enum_tainted_from_binding_to_core, gen_tokio_runtime, has_enum_named_field, references_named_type};
22use types::{
23 gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods,
24 gen_opaque_struct_methods, gen_php_struct, is_tagged_data_enum,
25};
26
27pub struct PhpBackend;
28
29impl PhpBackend {
30 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
31 RustBindingConfig {
32 struct_attrs: &["php_class"],
33 field_attrs: &[],
34 struct_derives: &["Clone"],
35 method_block_attr: Some("php_impl"),
36 constructor_attr: "",
37 static_attr: None,
38 function_attr: "#[php_function]",
39 enum_attrs: &[],
40 enum_derives: &[],
41 needs_signature: false,
42 signature_prefix: "",
43 signature_suffix: "",
44 core_import,
45 async_pattern: AsyncPattern::TokioBlockOn,
46 has_serde,
47 type_name_prefix: "",
48 option_duration_on_defaults: true,
49 opaque_type_names: &[],
50 skip_impl_constructor: false,
51 cast_uints_to_i32: false,
52 cast_large_ints_to_f64: false,
53 named_non_opaque_params_by_ref: false,
54 lossy_skip_types: &[],
55 }
56 }
57}
58
59impl Backend for PhpBackend {
60 fn name(&self) -> &str {
61 "php"
62 }
63
64 fn language(&self) -> Language {
65 Language::Php
66 }
67
68 fn capabilities(&self) -> Capabilities {
69 Capabilities {
70 supports_async: false,
71 supports_classes: true,
72 supports_enums: true,
73 supports_option: true,
74 supports_result: true,
75 ..Capabilities::default()
76 }
77 }
78
79 fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
80 let data_enum_names: AHashSet<String> = api
82 .enums
83 .iter()
84 .filter(|e| is_tagged_data_enum(e))
85 .map(|e| e.name.clone())
86 .collect();
87 let enum_names: AHashSet<String> = api
88 .enums
89 .iter()
90 .filter(|e| !is_tagged_data_enum(e))
91 .map(|e| e.name.clone())
92 .collect();
93 let mapper = PhpMapper {
94 enum_names: enum_names.clone(),
95 data_enum_names: data_enum_names.clone(),
96 };
97 let core_import = config.core_import_name();
98
99 let php_config = config.php.as_ref();
101 let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
102 let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
103
104 let output_dir = resolve_output_dir(config.output_paths.get("php"), &config.name, "crates/{name}-php/src/");
105 let has_serde = detect_serde_available(&output_dir);
106
107 let bridge_type_aliases_php: Vec<String> = config
113 .trait_bridges
114 .iter()
115 .filter_map(|b| b.type_alias.clone())
116 .collect();
117 let bridge_type_aliases_set: AHashSet<String> = bridge_type_aliases_php.iter().cloned().collect();
118 let mut opaque_names_vec_php: Vec<String> = api
119 .types
120 .iter()
121 .filter(|t| t.is_opaque)
122 .map(|t| t.name.clone())
123 .collect();
124 opaque_names_vec_php.extend(bridge_type_aliases_php);
125
126 let mut cfg = Self::binding_config(&core_import, has_serde);
127 cfg.opaque_type_names = &opaque_names_vec_php;
128
129 let mut builder = RustFileBuilder::new().with_generated_header();
131 builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
132 builder.add_inner_attribute("allow(unsafe_code)");
133 builder.add_inner_attribute("allow(non_snake_case)");
135 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, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy)");
136 builder.add_import("ext_php_rs::prelude::*");
137
138 if has_serde {
140 builder.add_import("serde_json");
141 }
142
143 for trait_path in generators::collect_trait_imports(api) {
145 builder.add_import(&trait_path);
146 }
147
148 let has_maps = api.types.iter().any(|t| {
150 t.fields
151 .iter()
152 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
153 }) || api
154 .functions
155 .iter()
156 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
157 if has_maps {
158 builder.add_import("std::collections::HashMap");
159 }
160
161 let custom_mods = config.custom_modules.for_language(Language::Php);
163 for module in custom_mods {
164 builder.add_item(&format!("pub mod {module};"));
165 }
166
167 let has_async =
169 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
170
171 if has_async {
172 builder.add_item(&gen_tokio_runtime());
173 }
174
175 let opaque_types: AHashSet<String> = api
177 .types
178 .iter()
179 .filter(|t| t.is_opaque)
180 .map(|t| t.name.clone())
181 .collect();
182 if !opaque_types.is_empty() {
183 builder.add_import("std::sync::Arc");
184 }
185
186 let extension_name = config.php_extension_name();
189 let php_namespace = php_autoload_namespace(config);
190
191 let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
193
194 for adapter in &config.adapters {
196 match adapter.pattern {
197 alef_core::config::AdapterPattern::Streaming => {
198 let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
199 if let Some(struct_code) = adapter_bodies.get(&key) {
200 builder.add_item(struct_code);
201 }
202 }
203 alef_core::config::AdapterPattern::CallbackBridge => {
204 let struct_key = format!("{}.__bridge_struct__", adapter.name);
205 let impl_key = format!("{}.__bridge_impl__", adapter.name);
206 if let Some(struct_code) = adapter_bodies.get(&struct_key) {
207 builder.add_item(struct_code);
208 }
209 if let Some(impl_code) = adapter_bodies.get(&impl_key) {
210 builder.add_item(impl_code);
211 }
212 }
213 _ => {}
214 }
215 }
216
217 for typ in api
218 .types
219 .iter()
220 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
221 {
222 if typ.is_opaque {
223 let ns_escaped = php_namespace.replace('\\', "\\\\");
227 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
228 let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
229 let opaque_cfg = RustBindingConfig {
230 struct_attrs: &opaque_attr_arr,
231 ..cfg
232 };
233 builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
234 builder.add_item(&gen_opaque_struct_methods(
235 typ,
236 &mapper,
237 &opaque_types,
238 &core_import,
239 &adapter_bodies,
240 ));
241 } else {
242 builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
245 builder.add_item(&types::gen_struct_methods_with_exclude(
246 typ,
247 &mapper,
248 has_serde,
249 &core_import,
250 &opaque_types,
251 &enum_names,
252 &api.enums,
253 &exclude_functions,
254 &bridge_type_aliases_set,
255 ));
256 }
257 }
258
259 for enum_def in &api.enums {
260 if is_tagged_data_enum(enum_def) {
261 builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
263 builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
264 } else {
265 builder.add_item(&gen_enum_constants(enum_def));
266 }
267 }
268
269 let included_functions: Vec<_> = api
274 .functions
275 .iter()
276 .filter(|f| !exclude_functions.contains(&f.name))
277 .collect();
278 if !included_functions.is_empty() {
279 let facade_class_name = extension_name.to_pascal_case();
280 let mut method_items: Vec<String> = Vec::new();
283 for func in included_functions {
284 let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
285 if let Some((param_idx, bridge_cfg)) = bridge_param {
286 method_items.push(crate::trait_bridge::gen_bridge_function(
287 func,
288 param_idx,
289 bridge_cfg,
290 &mapper,
291 &opaque_types,
292 &core_import,
293 ));
294 } else if func.is_async {
295 method_items.push(gen_async_function_as_static_method(
296 func,
297 &mapper,
298 &opaque_types,
299 &core_import,
300 &config.trait_bridges,
301 ));
302 } else {
303 method_items.push(gen_function_as_static_method(
304 func,
305 &mapper,
306 &opaque_types,
307 &core_import,
308 &config.trait_bridges,
309 has_serde,
310 ));
311 }
312 }
313
314 let methods_joined = method_items
315 .iter()
316 .map(|m| {
317 m.lines()
319 .map(|l| {
320 if l.is_empty() {
321 String::new()
322 } else {
323 format!(" {l}")
324 }
325 })
326 .collect::<Vec<_>>()
327 .join("\n")
328 })
329 .collect::<Vec<_>>()
330 .join("\n\n");
331 let php_api_class_name = format!("{facade_class_name}Api");
334 let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
336 let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
337 let facade_struct = format!(
338 "#[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}}"
339 );
340 builder.add_item(&facade_struct);
341
342 for bridge_cfg in &config.trait_bridges {
344 if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
345 let bridge = crate::trait_bridge::gen_trait_bridge(
346 trait_type,
347 bridge_cfg,
348 &core_import,
349 &config.error_type_name(),
350 &config.error_constructor_expr(),
351 api,
352 );
353 for imp in &bridge.imports {
354 builder.add_import(imp);
355 }
356 builder.add_item(&bridge.code);
357 }
358 }
359 }
360
361 let convertible = alef_codegen::conversions::convertible_types(api);
362 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
363 let input_types = alef_codegen::conversions::input_type_names(api);
364 let enum_names_ref = &mapper.enum_names;
369 let bridge_skip_types: Vec<String> = config
370 .trait_bridges
371 .iter()
372 .filter_map(|b| b.type_alias.clone())
373 .collect();
374 let php_conv_config = ConversionConfig {
375 cast_large_ints_to_i64: true,
376 enum_string_names: Some(enum_names_ref),
377 json_to_string: true,
378 include_cfg_metadata: false,
379 option_duration_on_defaults: true,
380 from_binding_skip_types: &bridge_skip_types,
381 ..Default::default()
382 };
383 let mut enum_tainted: AHashSet<String> = AHashSet::new();
385 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
386 if has_enum_named_field(typ, enum_names_ref) {
387 enum_tainted.insert(typ.name.clone());
388 }
389 }
390 let mut changed = true;
392 while changed {
393 changed = false;
394 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
395 if !enum_tainted.contains(&typ.name)
396 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
397 {
398 enum_tainted.insert(typ.name.clone());
399 changed = true;
400 }
401 }
402 }
403 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
404 if input_types.contains(&typ.name)
406 && !enum_tainted.contains(&typ.name)
407 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
408 {
409 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
410 typ,
411 &core_import,
412 &php_conv_config,
413 ));
414 } else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
415 builder.add_item(&gen_enum_tainted_from_binding_to_core(
422 typ,
423 &core_import,
424 enum_names_ref,
425 &enum_tainted,
426 &php_conv_config,
427 &api.enums,
428 &bridge_type_aliases_set,
429 ));
430 }
431 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
433 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
434 typ,
435 &core_import,
436 &opaque_types,
437 &php_conv_config,
438 ));
439 }
440 }
441
442 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
444 builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
445 }
446
447 for error in &api.errors {
449 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
450 }
451
452 if has_serde {
455 builder.add_item("mod serde_defaults {\n pub fn bool_true() -> bool { true }\n}");
456 }
457
458 let php_config = config.php.as_ref();
464 builder.add_inner_attribute("cfg_attr(windows, feature(abi_vectorcall))");
465
466 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
470 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
471 }
472
473 let mut class_registrations = String::new();
476 for typ in api
477 .types
478 .iter()
479 .filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
480 {
481 class_registrations.push_str(&format!("\n .class::<{}>()", typ.name));
482 }
483 if !api.functions.is_empty() {
485 let facade_class_name = extension_name.to_pascal_case();
486 class_registrations.push_str(&format!("\n .class::<{facade_class_name}Api>()"));
487 }
488 for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
491 class_registrations.push_str(&format!("\n .class::<{}>()", enum_def.name));
492 }
493 builder.add_item(&format!(
494 "#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
495 ));
496
497 let mut content = builder.build();
498
499 for bridge in &config.trait_bridges {
504 if let Some(field_name) = bridge.resolved_options_field() {
505 let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
506 let type_alias = bridge.type_alias.as_deref().unwrap_or("VisitorHandle");
507 let options_type = bridge.options_type.as_deref().unwrap_or("ConversionOptions");
508 let builder_type = format!("{}Builder", options_type);
509 let bridge_struct = format!("Php{}Bridge", bridge.trait_name);
510
511 let old_method = format!(
517 " pub fn {field_name}(&self, {param_name}: Option<&{type_alias}>) -> {builder_type} {{\n Self {{ inner: Arc::new((*self.inner).clone().{field_name}({param_name}.as_ref().map(|v| &v.inner))) }}\n }}"
518 );
519 let new_method = format!(
520 " pub fn {field_name}(&self, {param_name}: &mut ext_php_rs::types::ZendObject) -> {builder_type} {{\n let bridge = {bridge_struct}::new({param_name});\n let handle: html_to_markdown_rs::visitor::VisitorHandle = std::rc::Rc::new(std::cell::RefCell::new(bridge));\n Self {{ inner: Arc::new((*self.inner).clone().{field_name}(Some(handle))) }}\n }}"
521 );
522
523 content = content.replace(&old_method, &new_method);
524 }
525 }
526
527 Ok(vec![GeneratedFile {
528 path: PathBuf::from(&output_dir).join("lib.rs"),
529 content,
530 generated_header: false,
531 }])
532 }
533
534 fn generate_public_api(
535 &self,
536 api: &ApiSurface,
537 config: &ResolvedCrateConfig,
538 ) -> anyhow::Result<Vec<GeneratedFile>> {
539 let extension_name = config.php_extension_name();
540 let class_name = extension_name.to_pascal_case();
541
542 let mut content = String::from("<?php\n\n");
544 content.push_str(&hash::header(CommentStyle::DoubleSlash));
545 content.push_str("declare(strict_types=1);\n\n");
546
547 let namespace = php_autoload_namespace(config);
549
550 content.push_str(&format!("namespace {};\n\n", namespace));
551 content.push_str(&format!("final class {}\n", class_name));
552 content.push_str("{\n");
553
554 let bridge_param_names_pub: ahash::AHashSet<&str> = config
556 .trait_bridges
557 .iter()
558 .filter_map(|b| b.param_name.as_deref())
559 .collect();
560
561 let no_arg_constructor_types: AHashSet<String> = api
566 .types
567 .iter()
568 .filter(|t| t.fields.iter().all(|f| f.optional))
569 .map(|t| t.name.clone())
570 .collect();
571
572 for func in &api.functions {
574 let method_name = func.name.to_lower_camel_case();
580 let return_php_type = php_type(&func.return_type);
581
582 let visible_params: Vec<_> = func
584 .params
585 .iter()
586 .filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
587 .collect();
588
589 content.push_str(" /**\n");
591 for line in func.doc.lines() {
592 if line.is_empty() {
593 content.push_str(" *\n");
594 } else {
595 content.push_str(&format!(" * {}\n", line));
596 }
597 }
598 if func.doc.is_empty() {
599 content.push_str(&format!(" * {}.\n", method_name));
600 }
601 content.push_str(" *\n");
602 for p in &visible_params {
603 let ptype = php_phpdoc_type(&p.ty);
604 let nullable_prefix = if p.optional { "?" } else { "" };
605 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
606 }
607 let return_phpdoc = php_phpdoc_type(&func.return_type);
608 content.push_str(&format!(" * @return {}\n", return_phpdoc));
609 if func.error_type.is_some() {
610 content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
611 }
612 content.push_str(" */\n");
613
614 let is_optional_config_param = |p: &alef_core::ir::ParamDef| -> bool {
625 if let TypeRef::Named(name) = &p.ty {
626 (name.ends_with("Config") || name.as_str() == "config")
627 && no_arg_constructor_types.contains(name.as_str())
628 } else {
629 false
630 }
631 };
632
633 let mut first_optional_idx = None;
634 for (idx, p) in visible_params.iter().enumerate() {
635 if p.optional || is_optional_config_param(p) {
636 first_optional_idx = Some(idx);
637 break;
638 }
639 }
640
641 content.push_str(&format!(" public static function {}(", method_name));
642
643 let params: Vec<String> = visible_params
644 .iter()
645 .enumerate()
646 .map(|(idx, p)| {
647 let ptype = php_type(&p.ty);
648 let should_be_optional = p.optional
653 || is_optional_config_param(p)
654 || first_optional_idx.is_some_and(|first| idx >= first);
655 if should_be_optional {
656 format!("?{} ${} = null", ptype, p.name)
657 } else {
658 format!("{} ${}", ptype, p.name)
659 }
660 })
661 .collect();
662 content.push_str(¶ms.join(", "));
663 content.push_str(&format!("): {}\n", return_php_type));
664 content.push_str(" {\n");
665 let ext_method_name = if func.is_async {
670 format!("{}_async", func.name).to_lower_camel_case()
671 } else {
672 func.name.to_lower_camel_case()
673 };
674 let is_void = matches!(&func.return_type, TypeRef::Unit);
675 let call_params = visible_params
683 .iter()
684 .enumerate()
685 .map(|(idx, p)| {
686 let should_be_optional = p.optional
687 || is_optional_config_param(p)
688 || first_optional_idx.is_some_and(|first| idx >= first);
689 if should_be_optional && is_optional_config_param(p) {
690 if let TypeRef::Named(type_name) = &p.ty {
691 return format!("${} ?? new {}()", p.name, type_name);
692 }
693 }
694 format!("${}", p.name)
695 })
696 .collect::<Vec<_>>()
697 .join(", ");
698 let call_expr = format!(
699 "\\{}\\{}Api::{}({})",
700 namespace, class_name, ext_method_name, call_params
701 );
702 if is_void {
703 content.push_str(&format!(
704 " {}; // delegate to native extension class\n",
705 call_expr
706 ));
707 } else {
708 content.push_str(&format!(
709 " return {}; // delegate to native extension class\n",
710 call_expr
711 ));
712 }
713 content.push_str(" }\n\n");
714 }
715
716 content.push_str("}\n");
717
718 let output_dir = config
722 .php
723 .as_ref()
724 .and_then(|p| p.stubs.as_ref())
725 .map(|s| s.output.to_string_lossy().to_string())
726 .unwrap_or_else(|| "packages/php/src/".to_string());
727
728 Ok(vec![GeneratedFile {
729 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
730 content,
731 generated_header: false,
732 }])
733 }
734
735 fn generate_type_stubs(
736 &self,
737 api: &ApiSurface,
738 config: &ResolvedCrateConfig,
739 ) -> anyhow::Result<Vec<GeneratedFile>> {
740 let extension_name = config.php_extension_name();
741 let class_name = extension_name.to_pascal_case();
742
743 let namespace = php_autoload_namespace(config);
745
746 let mut content = String::from("<?php\n\n");
751 content.push_str(&hash::header(CommentStyle::DoubleSlash));
752 content.push_str("// Type stubs for the native PHP extension — declares classes\n");
753 content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
754 content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
755 content.push_str("declare(strict_types=1);\n\n");
756 content.push_str(&format!("namespace {} {{\n\n", namespace));
758
759 content.push_str(&format!(
761 "class {}Exception extends \\RuntimeException\n{{\n",
762 class_name
763 ));
764 content.push_str(
765 " public function getErrorCode(): int { throw new \\RuntimeException('Not implemented.'); }\n",
766 );
767 content.push_str("}\n\n");
768
769 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
771 if typ.is_opaque {
772 if !typ.doc.is_empty() {
773 content.push_str("/**\n");
774 for line in typ.doc.lines() {
775 if line.is_empty() {
776 content.push_str(" *\n");
777 } else {
778 content.push_str(&format!(" * {}\n", line));
779 }
780 }
781 content.push_str(" */\n");
782 }
783 content.push_str(&format!("class {}\n{{\n", typ.name));
784 content.push_str("}\n\n");
786 }
787 }
788
789 for typ in api.types.iter().filter(|typ| !typ.is_trait) {
791 if typ.is_opaque || typ.fields.is_empty() {
792 continue;
793 }
794 if !typ.doc.is_empty() {
795 content.push_str("/**\n");
796 for line in typ.doc.lines() {
797 if line.is_empty() {
798 content.push_str(" *\n");
799 } else {
800 content.push_str(&format!(" * {}\n", line));
801 }
802 }
803 content.push_str(" */\n");
804 }
805 content.push_str(&format!("class {}\n{{\n", typ.name));
806
807 for field in &typ.fields {
809 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
810 let prop_type = if field.optional {
811 let inner = php_type(&field.ty);
812 if inner.starts_with('?') {
813 inner
814 } else {
815 format!("?{inner}")
816 }
817 } else {
818 php_type(&field.ty)
819 };
820 if is_array {
821 let phpdoc = php_phpdoc_type(&field.ty);
822 let nullable_prefix = if field.optional { "?" } else { "" };
823 content.push_str(&format!(" /** @var {}{} */\n", nullable_prefix, phpdoc));
824 }
825 content.push_str(&format!(" public {} ${};\n", prop_type, field.name));
826 }
827 content.push('\n');
828
829 let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
833 sorted_fields.sort_by_key(|f| f.optional);
834
835 let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
838 .iter()
839 .copied()
840 .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
841 .collect();
842 if !array_fields.is_empty() {
843 content.push_str(" /**\n");
844 for f in &array_fields {
845 let phpdoc = php_phpdoc_type(&f.ty);
846 let nullable_prefix = if f.optional { "?" } else { "" };
847 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
848 }
849 content.push_str(" */\n");
850 }
851
852 let params: Vec<String> = sorted_fields
853 .iter()
854 .map(|f| {
855 let ptype = php_type(&f.ty);
856 let nullable = if f.optional && !ptype.starts_with('?') {
857 format!("?{ptype}")
858 } else {
859 ptype
860 };
861 let default = if f.optional { " = null" } else { "" };
862 format!(" {} ${}{}", nullable, f.name, default)
863 })
864 .collect();
865 content.push_str(" public function __construct(\n");
866 content.push_str(¶ms.join(",\n"));
867 content.push_str("\n ) { }\n\n");
868
869 for field in &typ.fields {
871 let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
872 let return_type = if field.optional {
873 let inner = php_type(&field.ty);
874 if inner.starts_with('?') {
875 inner
876 } else {
877 format!("?{inner}")
878 }
879 } else {
880 php_type(&field.ty)
881 };
882 let getter_name = field.name.to_lower_camel_case();
883 if is_array {
885 let phpdoc = php_phpdoc_type(&field.ty);
886 let nullable_prefix = if field.optional { "?" } else { "" };
887 content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
888 }
889 let is_void_getter = return_type == "void";
890 let getter_body = if is_void_getter {
891 "{ }".to_string()
892 } else {
893 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
894 };
895 content.push_str(&format!(
896 " public function get{}(): {} {getter_body}\n",
897 getter_name.to_pascal_case(),
898 return_type
899 ));
900 }
901
902 content.push_str("}\n\n");
903 }
904
905 for enum_def in &api.enums {
908 if is_tagged_data_enum(enum_def) {
909 if !enum_def.doc.is_empty() {
911 content.push_str("/**\n");
912 for line in enum_def.doc.lines() {
913 if line.is_empty() {
914 content.push_str(" *\n");
915 } else {
916 content.push_str(&format!(" * {}\n", line));
917 }
918 }
919 content.push_str(" */\n");
920 }
921 content.push_str(&format!("class {}\n{{\n", enum_def.name));
922 content.push_str("}\n\n");
923 } else {
924 content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
926 for variant in &enum_def.variants {
927 content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
928 }
929 content.push_str("}\n\n");
930 }
931 }
932
933 if !api.functions.is_empty() {
938 let bridge_param_names_stubs: ahash::AHashSet<&str> = config
940 .trait_bridges
941 .iter()
942 .filter_map(|b| b.param_name.as_deref())
943 .collect();
944
945 content.push_str(&format!("class {}Api\n{{\n", class_name));
946 for func in &api.functions {
947 let return_type = php_type_fq(&func.return_type, &namespace);
948 let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
949 let visible_params: Vec<_> = func
951 .params
952 .iter()
953 .filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
954 .collect();
955 let has_array_params = visible_params
962 .iter()
963 .any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
964 let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
965 || matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
966 if has_array_params || has_array_return {
967 content.push_str(" /**\n");
968 for p in &visible_params {
969 let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
970 let nullable_prefix = if p.optional { "?" } else { "" };
971 content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
972 }
973 content.push_str(&format!(" * @return {}\n", return_phpdoc));
974 content.push_str(" */\n");
975 }
976 let params: Vec<String> = visible_params
977 .iter()
978 .map(|p| {
979 let ptype = php_type_fq(&p.ty, &namespace);
980 if p.optional {
981 format!("?{} ${} = null", ptype, p.name)
982 } else {
983 format!("{} ${}", ptype, p.name)
984 }
985 })
986 .collect();
987 let stub_method_name = if func.is_async {
989 format!("{}_async", func.name).to_lower_camel_case()
990 } else {
991 func.name.to_lower_camel_case()
992 };
993 let is_void_stub = return_type == "void";
994 let stub_body = if is_void_stub {
995 "{ }".to_string()
996 } else {
997 "{ throw new \\RuntimeException('Not implemented.'); }".to_string()
998 };
999 content.push_str(&format!(
1000 " public static function {}({}): {} {stub_body}\n",
1001 stub_method_name,
1002 params.join(", "),
1003 return_type
1004 ));
1005 }
1006 content.push_str("}\n\n");
1007 }
1008
1009 content.push_str("} // end namespace\n");
1011
1012 let output_dir = config
1014 .php
1015 .as_ref()
1016 .and_then(|p| p.stubs.as_ref())
1017 .map(|s| s.output.to_string_lossy().to_string())
1018 .unwrap_or_else(|| "packages/php/stubs/".to_string());
1019
1020 Ok(vec![GeneratedFile {
1021 path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
1022 content,
1023 generated_header: false,
1024 }])
1025 }
1026
1027 fn build_config(&self) -> Option<BuildConfig> {
1028 Some(BuildConfig {
1029 tool: "cargo",
1030 crate_suffix: "-php",
1031 build_dep: BuildDependency::None,
1032 post_build: vec![],
1033 })
1034 }
1035}
1036
1037fn php_phpdoc_type(ty: &TypeRef) -> String {
1040 match ty {
1041 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
1042 TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
1043 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
1044 _ => php_type(ty),
1045 }
1046}
1047
1048fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
1050 match ty {
1051 TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
1052 TypeRef::Map(k, v) => format!(
1053 "array<{}, {}>",
1054 php_phpdoc_type_fq(k, namespace),
1055 php_phpdoc_type_fq(v, namespace)
1056 ),
1057 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1058 TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
1059 _ => php_type(ty),
1060 }
1061}
1062
1063fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
1065 match ty {
1066 TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
1067 TypeRef::Optional(inner) => {
1068 let inner_type = php_type_fq(inner, namespace);
1069 if inner_type.starts_with('?') {
1070 inner_type
1071 } else {
1072 format!("?{inner_type}")
1073 }
1074 }
1075 _ => php_type(ty),
1076 }
1077}
1078
1079fn php_type(ty: &TypeRef) -> String {
1081 match ty {
1082 TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
1083 TypeRef::Primitive(p) => match p {
1084 PrimitiveType::Bool => "bool".to_string(),
1085 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
1086 PrimitiveType::U8
1087 | PrimitiveType::U16
1088 | PrimitiveType::U32
1089 | PrimitiveType::U64
1090 | PrimitiveType::I8
1091 | PrimitiveType::I16
1092 | PrimitiveType::I32
1093 | PrimitiveType::I64
1094 | PrimitiveType::Usize
1095 | PrimitiveType::Isize => "int".to_string(),
1096 },
1097 TypeRef::Optional(inner) => {
1098 let inner_type = php_type(inner);
1101 if inner_type.starts_with('?') {
1102 inner_type
1103 } else {
1104 format!("?{inner_type}")
1105 }
1106 }
1107 TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
1108 TypeRef::Named(name) => name.clone(),
1109 TypeRef::Unit => "void".to_string(),
1110 TypeRef::Duration => "float".to_string(),
1111 }
1112}