Skip to main content

alef_backend_php/gen_bindings/
mod.rs

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 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        // Build the inner module content (types, methods, conversions)
83        let mut builder = RustFileBuilder::new();
84        builder.add_import("ext_php_rs::prelude::*");
85
86        // Import serde_json when available (needed for serde-based param conversion)
87        if has_serde {
88            builder.add_import("serde_json");
89        }
90
91        // Import traits needed for trait method dispatch
92        for trait_path in generators::collect_trait_imports(api) {
93            builder.add_import(&trait_path);
94        }
95
96        // Only import HashMap when Map-typed fields or returns are present
97        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        // Custom module declarations
110        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        // Check if any function or method is async
116        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        // Check if we have opaque types and add Arc import if needed
124        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                // gen_struct adds #[derive(Default)] when typ.has_default is true,
140                // so no separate Default impl is needed.
141                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        // From/Into conversions with PHP-specific i64 casts.
167        // Types with enum Named fields (or that reference such types transitively) can't
168        // have binding->core From impls because PHP maps enums to String and there's no
169        // From<String> for the core enum type. Core->binding is always safe.
170        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        // Build transitive set of types that can't have binding->core From
179        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        // Transitively mark types that reference enum-tainted types
186        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            // binding->core: only when not enum-tainted
200            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                // Enum-tainted types can't use field-by-field From (no From<String> for core enum),
210                // but when serde is available we bridge via JSON serialization round-trip.
211                builder.add_item(&gen_serde_bridge_from(typ, &core_import));
212            } else if enum_tainted.contains(&typ.name) {
213                // Enum-tainted types: generate From with string->enum parsing for enum-Named
214                // fields, using first variant as fallback. Data-variant enum fields fill
215                // data fields with Default::default().
216                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            // core->binding: always (enum->String via format, sanitized fields via format)
226            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        // Error converter functions
237        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        // Add feature gate as inner attribute — entire crate is gated
244        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        // Generate PHP wrapper class
266        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        // Determine namespace
271        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        // Generate wrapper methods for functions
284        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            // Parameters
291            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(&params.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}