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_enum_tainted_from_binding_to_core, gen_serde_bridge_from, gen_tokio_runtime, has_enum_named_field,
20 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));
142 builder.add_item(&gen_struct_methods(
143 typ,
144 &mapper,
145 has_serde,
146 &core_import,
147 &opaque_types,
148 ));
149 }
150 }
151
152 for enum_def in &api.enums {
153 builder.add_item(&gen_enum_constants(enum_def));
154 }
155
156 for func in &api.functions {
157 if func.is_async {
158 builder.add_item(&gen_async_function(func, &mapper, &opaque_types, &core_import));
159 } else {
160 builder.add_item(&gen_function(func, &mapper, &opaque_types, &core_import));
161 }
162 }
163
164 let convertible = alef_codegen::conversions::convertible_types(api);
165 let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
166 let enum_names_ref = &mapper.enum_names;
171 let php_conv_config = ConversionConfig {
172 cast_large_ints_to_i64: true,
173 enum_string_names: Some(enum_names_ref),
174 json_to_string: true,
175 include_cfg_metadata: false,
176 ..Default::default()
177 };
178 let mut enum_tainted: AHashSet<String> = AHashSet::new();
180 for typ in &api.types {
181 if has_enum_named_field(typ, enum_names_ref) {
182 enum_tainted.insert(typ.name.clone());
183 }
184 }
185 let mut changed = true;
187 while changed {
188 changed = false;
189 for typ in &api.types {
190 if !enum_tainted.contains(&typ.name)
191 && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
192 {
193 enum_tainted.insert(typ.name.clone());
194 changed = true;
195 }
196 }
197 }
198 for typ in &api.types {
199 if !enum_tainted.contains(&typ.name)
201 && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
202 {
203 builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
204 typ,
205 &core_import,
206 &php_conv_config,
207 ));
208 } else if enum_tainted.contains(&typ.name) && has_serde {
209 builder.add_item(&gen_serde_bridge_from(typ, &core_import));
212 } else if enum_tainted.contains(&typ.name) {
213 builder.add_item(&gen_enum_tainted_from_binding_to_core(
217 typ,
218 &core_import,
219 enum_names_ref,
220 &enum_tainted,
221 &php_conv_config,
222 &api.enums,
223 ));
224 }
225 if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
227 builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
228 typ,
229 &core_import,
230 &opaque_types,
231 &php_conv_config,
232 ));
233 }
234 }
235
236 for error in &api.errors {
238 builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
239 }
240
241 let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
242
243 let php_config = config.php.as_ref();
245 if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
246 builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
247 builder.add_inner_attribute(&format!(
248 "cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
249 ));
250 }
251
252 let content = builder.build();
253
254 Ok(vec![GeneratedFile {
255 path: PathBuf::from(&output_dir).join("lib.rs"),
256 content,
257 generated_header: false,
258 }])
259 }
260
261 fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
262 let extension_name = config.php_extension_name();
263 let class_name = extension_name.to_pascal_case();
264
265 let mut content = String::from("<?php\n");
267 content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
268 content.push_str("declare(strict_types=1);\n\n");
269
270 let namespace = if extension_name.contains('_') {
272 let parts: Vec<&str> = extension_name.split('_').collect();
273 let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
274 ns_parts.join("\\")
275 } else {
276 class_name.clone()
277 };
278
279 content.push_str(&format!("namespace {};\n\n", namespace));
280 content.push_str(&format!("final class {}\n", class_name));
281 content.push_str("{\n");
282
283 for func in &api.functions {
285 content.push_str(" /**\n");
286 content.push_str(&format!(" * {}\n", func.doc.lines().next().unwrap_or("Function")));
287 content.push_str(" */\n");
288 content.push_str(&format!(" public static function {}(", func.name));
289
290 let params: Vec<String> = func
292 .params
293 .iter()
294 .map(|p| {
295 if p.optional {
296 format!("?${} = null", p.name)
297 } else {
298 format!("${}", p.name)
299 }
300 })
301 .collect();
302 content.push_str(¶ms.join(", "));
303 content.push_str(")\n");
304 content.push_str(" {\n");
305 content.push_str(&format!(
306 " return \\{}({}); // delegate to extension function\n",
307 func.name,
308 func.params
309 .iter()
310 .map(|p| format!("${}", p.name))
311 .collect::<Vec<_>>()
312 .join(", ")
313 ));
314 content.push_str(" }\n\n");
315 }
316
317 content.push_str("}\n");
318
319 let output_dir = resolve_output_dir(
320 config.output.php.as_ref(),
321 &config.crate_config.name,
322 "packages/php/src/",
323 );
324
325 Ok(vec![GeneratedFile {
326 path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
327 content,
328 generated_header: false,
329 }])
330 }
331
332 fn build_config(&self) -> Option<BuildConfig> {
333 Some(BuildConfig {
334 tool: "cargo",
335 crate_suffix: "-php",
336 depends_on_ffi: false,
337 post_build: vec![],
338 })
339 }
340}