alef_backend_php/gen_bindings/
mod.rs1mod 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 heck::ToPascalCase;
15use std::path::PathBuf;
16
17use functions::{gen_async_function, gen_function};
18use helpers::{
19 gen_convertible_enum_tainted, gen_enum_tainted_from_binding_to_core, gen_serde_bridge_from, gen_tokio_runtime,
20 has_enum_named_field, references_named_type,
21};
22use types::{gen_enum_constants, gen_opaque_struct_methods, gen_php_struct, gen_struct_methods};
23
24pub struct PhpBackend;
25
26impl PhpBackend {
27 fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
28 RustBindingConfig {
29 struct_attrs: &["php_class"],
30 field_attrs: &[],
31 struct_derives: &["Clone"],
32 method_block_attr: Some("php_impl"),
33 constructor_attr: "",
34 static_attr: None,
35 function_attr: "#[php_function]",
36 enum_attrs: &[],
37 enum_derives: &[],
38 needs_signature: false,
39 signature_prefix: "",
40 signature_suffix: "",
41 core_import,
42 async_pattern: AsyncPattern::TokioBlockOn,
43 has_serde,
44 type_name_prefix: "",
45 }
46 }
47}
48
49impl Backend for PhpBackend {
50 fn name(&self) -> &str {
51 "php"
52 }
53
54 fn language(&self) -> Language {
55 Language::Php
56 }
57
58 fn capabilities(&self) -> Capabilities {
59 Capabilities {
60 supports_async: true,
61 supports_classes: true,
62 supports_enums: true,
63 supports_option: true,
64 supports_result: true,
65 ..Capabilities::default()
66 }
67 }
68
69 fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
70 let enum_names = api.enums.iter().map(|e| e.name.clone()).collect();
71 let mapper = PhpMapper { enum_names };
72 let core_import = config.core_import();
73
74 let output_dir = resolve_output_dir(
75 config.output.php.as_ref(),
76 &config.crate_config.name,
77 "crates/{name}-php/src/",
78 );
79 let has_serde = detect_serde_available(&output_dir);
80 let cfg = Self::binding_config(&core_import, has_serde);
81
82 let mut builder = RustFileBuilder::new();
84 builder.add_import("ext_php_rs::prelude::*");
85
86 if has_serde {
88 builder.add_import("serde_json");
89 }
90
91 for trait_path in generators::collect_trait_imports(api) {
93 builder.add_import(&trait_path);
94 }
95
96 let has_maps = api.types.iter().any(|t| {
98 t.fields
99 .iter()
100 .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
101 }) || api
102 .functions
103 .iter()
104 .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
105 if has_maps {
106 builder.add_import("std::collections::HashMap");
107 }
108
109 let custom_mods = config.custom_modules.for_language(Language::Php);
111 for module in custom_mods {
112 builder.add_item(&format!("pub mod {module};"));
113 }
114
115 let has_async =
117 api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
118
119 if has_async {
120 builder.add_item(&gen_tokio_runtime());
121 }
122
123 let opaque_types: AHashSet<String> = api
125 .types
126 .iter()
127 .filter(|t| t.is_opaque)
128 .map(|t| t.name.clone())
129 .collect();
130 if !opaque_types.is_empty() {
131 builder.add_import("std::sync::Arc");
132 }
133
134 for typ in &api.types {
135 if typ.is_opaque {
136 builder.add_item(&generators::gen_opaque_struct(typ, &cfg));
137 builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &opaque_types, &core_import));
138 } else {
139 builder.add_item(&gen_php_struct(typ, &mapper, &cfg));
140 if typ.has_default {
141 builder.add_item(&generators::gen_struct_default_impl(typ, ""));
142 }
143 builder.add_item(&gen_struct_methods(
144 typ,
145 &mapper,
146 has_serde,
147 &core_import,
148 &opaque_types,
149 ));
150 }
151 }
152
153 for enum_def in &api.enums {
154 builder.add_item(&gen_enum_constants(enum_def));
155 }
156
157 for func in &api.functions {
158 if func.is_async {
159 builder.add_item(&gen_async_function(func, &mapper, &opaque_types, &core_import));
160 } else {
161 builder.add_item(&gen_function(func, &mapper, &opaque_types, &core_import));
162 }
163 }
164
165 let convertible = alef_codegen::conversions::convertible_types(api);
166 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
167 let enum_names_ref = &mapper.enum_names;
172 let php_conv_config = ConversionConfig {
173 cast_large_ints_to_i64: true,
174 enum_string_names: Some(enum_names_ref),
175 json_to_string: true,
176 include_cfg_metadata: false,
177 ..Default::default()
178 };
179 let mut enum_tainted: AHashSet<String> = AHashSet::new();
181 for typ in &api.types {
182 if has_enum_named_field(typ, enum_names_ref) {
183 enum_tainted.insert(typ.name.clone());
184 }
185 }
186 let mut changed = true;
188 while changed {
189 changed = false;
190 for typ in &api.types {
191 if !enum_tainted.contains(&typ.name)
192 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
193 {
194 enum_tainted.insert(typ.name.clone());
195 changed = true;
196 }
197 }
198 }
199 let convertible_tainted = gen_convertible_enum_tainted(&api.types, &enum_tainted, enum_names_ref, &api.enums);
202 for typ in &api.types {
203 if !enum_tainted.contains(&typ.name)
205 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
206 {
207 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
208 typ,
209 &core_import,
210 &php_conv_config,
211 ));
212 } else if enum_tainted.contains(&typ.name) && has_serde {
213 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
216 } else if convertible_tainted.contains(&typ.name) {
217 builder.add_item(&gen_enum_tainted_from_binding_to_core(
220 typ,
221 &core_import,
222 enum_names_ref,
223 &enum_tainted,
224 &php_conv_config,
225 &api.enums,
226 ));
227 }
228 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
230 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
231 typ,
232 &core_import,
233 &opaque_types,
234 &php_conv_config,
235 ));
236 }
237 }
238
239 for error in &api.errors {
241 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
242 }
243
244 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
245
246 let php_config = config.php.as_ref();
248 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
249 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
250 builder.add_inner_attribute(&format!(
251 "cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
252 ));
253 }
254
255 let content = builder.build();
256
257 Ok(vec![GeneratedFile {
258 path: PathBuf::from(&output_dir).join("lib.rs"),
259 content,
260 generated_header: false,
261 }])
262 }
263
264 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
265 let extension_name = config.php_extension_name();
266 let class_name = extension_name.to_pascal_case();
267
268 let mut content = String::from("<?php\n");
270 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
271 content.push_str("declare(strict_types=1);\n\n");
272
273 let namespace = if extension_name.contains('_') {
275 let parts: Vec<&str> = extension_name.split('_').collect();
276 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
277 ns_parts.join("\\")
278 } else {
279 class_name.clone()
280 };
281
282 content.push_str(&format!("namespace {};\n\n", namespace));
283 content.push_str(&format!("final class {}\n", class_name));
284 content.push_str("{\n");
285
286 for func in &api.functions {
288 content.push_str(" /**\n");
289 content.push_str(&format!(" * {}\n", func.doc.lines().next().unwrap_or("Function")));
290 content.push_str(" */\n");
291 content.push_str(&format!(" public static function {}(", func.name));
292
293 let params: Vec<String> = func
295 .params
296 .iter()
297 .map(|p| {
298 if p.optional {
299 format!("?${} = null", p.name)
300 } else {
301 format!("${}", p.name)
302 }
303 })
304 .collect();
305 content.push_str(¶ms.join(", "));
306 content.push_str(")\n");
307 content.push_str(" {\n");
308 content.push_str(&format!(
309 " return \\{}({}); // delegate to extension function\n",
310 func.name,
311 func.params
312 .iter()
313 .map(|p| format!("${}", p.name))
314 .collect::<Vec<_>>()
315 .join(", ")
316 ));
317 content.push_str(" }\n\n");
318 }
319
320 content.push_str("}\n");
321
322 let output_dir = resolve_output_dir(
323 config.output.php.as_ref(),
324 &config.crate_config.name,
325 "packages/php/src/",
326 );
327
328 Ok(vec![GeneratedFile {
329 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
330 content,
331 generated_header: false,
332 }])
333 }
334
335 fn build_config(&self) -> Option<BuildConfig> {
336 Some(BuildConfig {
337 tool: "cargo",
338 crate_suffix: "-php",
339 depends_on_ffi: false,
340 post_build: vec![],
341 })
342 }
343}