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_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        // 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                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        // From/Into conversions with PHP-specific i64 casts.
168        // Types with enum Named fields (or that reference such types transitively) can't
169        // have binding->core From impls because PHP maps enums to String and there's no
170        // From<String> for the core enum type. Core->binding is always safe.
171        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        // Build transitive set of types that can't have binding->core From
180        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        // Transitively mark types that reference enum-tainted types
187        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        // Compute which enum-tainted types can have binding->core From generated
200        // (excludes types referencing enums with data variants).
201        let convertible_tainted = gen_convertible_enum_tainted(&api.types, &enum_tainted, enum_names_ref, &api.enums);
202        for typ in &api.types {
203            // binding->core: only when not enum-tainted
204            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                // Enum-tainted types can't use field-by-field From (no From<String> for core enum),
214                // but when serde is available we bridge via JSON serialization round-trip.
215                builder.add_item(&gen_serde_bridge_from(typ, &core_import));
216            } else if convertible_tainted.contains(&typ.name) {
217                // Enum-tainted types with only unit-variant enums: generate From with
218                // string->enum parsing for enum-Named fields, using first variant as fallback.
219                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            // core->binding: always (enum->String via format, sanitized fields via format)
229            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        // Error converter functions
240        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        // Add feature gate as inner attribute — entire crate is gated
247        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        // Generate PHP wrapper class
269        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        // Determine namespace
274        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        // Generate wrapper methods for functions
287        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            // Parameters
294            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(&params.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}