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 alef_core::ir::{PrimitiveType, TypeRef};
15use heck::{ToLowerCamelCase, ToPascalCase};
16use std::path::PathBuf;
17
18use functions::{gen_async_function, gen_function};
19use helpers::{
20    gen_enum_tainted_from_binding_to_core, gen_serde_bridge_from, gen_tokio_runtime, has_enum_named_field,
21    references_named_type,
22};
23use types::{gen_enum_constants, gen_opaque_struct_methods, gen_php_struct, gen_struct_methods};
24
25pub struct PhpBackend;
26
27impl PhpBackend {
28    fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
29        RustBindingConfig {
30            struct_attrs: &["php_class"],
31            field_attrs: &[],
32            struct_derives: &["Clone"],
33            method_block_attr: Some("php_impl"),
34            constructor_attr: "",
35            static_attr: None,
36            function_attr: "#[php_function]",
37            enum_attrs: &[],
38            enum_derives: &[],
39            needs_signature: false,
40            signature_prefix: "",
41            signature_suffix: "",
42            core_import,
43            async_pattern: AsyncPattern::TokioBlockOn,
44            has_serde,
45            type_name_prefix: "",
46            option_duration_on_defaults: true,
47        }
48    }
49}
50
51impl Backend for PhpBackend {
52    fn name(&self) -> &str {
53        "php"
54    }
55
56    fn language(&self) -> Language {
57        Language::Php
58    }
59
60    fn capabilities(&self) -> Capabilities {
61        Capabilities {
62            supports_async: true,
63            supports_classes: true,
64            supports_enums: true,
65            supports_option: true,
66            supports_result: true,
67            ..Capabilities::default()
68        }
69    }
70
71    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
72        let enum_names = api.enums.iter().map(|e| e.name.clone()).collect();
73        let mapper = PhpMapper { enum_names };
74        let core_import = config.core_import();
75
76        let output_dir = resolve_output_dir(
77            config.output.php.as_ref(),
78            &config.crate_config.name,
79            "crates/{name}-php/src/",
80        );
81        let has_serde = detect_serde_available(&output_dir);
82        let cfg = Self::binding_config(&core_import, has_serde);
83
84        // Build the inner module content (types, methods, conversions)
85        let mut builder = RustFileBuilder::new();
86        builder.add_import("ext_php_rs::prelude::*");
87
88        // Import serde_json when available (needed for serde-based param conversion)
89        if has_serde {
90            builder.add_import("serde_json");
91        }
92
93        // Import traits needed for trait method dispatch
94        for trait_path in generators::collect_trait_imports(api) {
95            builder.add_import(&trait_path);
96        }
97
98        // Only import HashMap when Map-typed fields or returns are present
99        let has_maps = api.types.iter().any(|t| {
100            t.fields
101                .iter()
102                .any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
103        }) || api
104            .functions
105            .iter()
106            .any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
107        if has_maps {
108            builder.add_import("std::collections::HashMap");
109        }
110
111        // Custom module declarations
112        let custom_mods = config.custom_modules.for_language(Language::Php);
113        for module in custom_mods {
114            builder.add_item(&format!("pub mod {module};"));
115        }
116
117        // Check if any function or method is async
118        let has_async =
119            api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
120
121        if has_async {
122            builder.add_item(&gen_tokio_runtime());
123        }
124
125        // Check if we have opaque types and add Arc import if needed
126        let opaque_types: AHashSet<String> = api
127            .types
128            .iter()
129            .filter(|t| t.is_opaque)
130            .map(|t| t.name.clone())
131            .collect();
132        if !opaque_types.is_empty() {
133            builder.add_import("std::sync::Arc");
134        }
135
136        // Compute the PHP namespace for namespaced class registration.
137        // Uses the same logic as `generate_public_api` / `generate_type_stubs`.
138        let extension_name = config.php_extension_name();
139        let php_namespace = if extension_name.contains('_') {
140            let parts: Vec<&str> = extension_name.split('_').collect();
141            let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
142            ns_parts.join("\\")
143        } else {
144            extension_name.to_pascal_case()
145        };
146
147        for typ in &api.types {
148            if typ.is_opaque {
149                // Generate the opaque struct with separate #[php_class] and
150                // #[php(name = "Ns\\Type")] attributes (ext-php-rs 0.15+ syntax).
151                let php_name_attr = format!("php(name = \"{}\\\\{}\")", php_namespace, typ.name);
152                let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
153                let opaque_cfg = RustBindingConfig {
154                    struct_attrs: &opaque_attr_arr,
155                    ..cfg
156                };
157                builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
158                builder.add_item(&gen_opaque_struct_methods(typ, &mapper, &opaque_types, &core_import));
159            } else {
160                // gen_struct adds #[derive(Default)] when typ.has_default is true,
161                // so no separate Default impl is needed.
162                builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace)));
163                builder.add_item(&gen_struct_methods(
164                    typ,
165                    &mapper,
166                    has_serde,
167                    &core_import,
168                    &opaque_types,
169                ));
170            }
171        }
172
173        for enum_def in &api.enums {
174            builder.add_item(&gen_enum_constants(enum_def));
175        }
176
177        for func in &api.functions {
178            if func.is_async {
179                builder.add_item(&gen_async_function(func, &mapper, &opaque_types, &core_import));
180            } else {
181                builder.add_item(&gen_function(func, &mapper, &opaque_types, &core_import));
182            }
183        }
184
185        let convertible = alef_codegen::conversions::convertible_types(api);
186        let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
187        // From/Into conversions with PHP-specific i64 casts.
188        // Types with enum Named fields (or that reference such types transitively) can't
189        // have binding->core From impls because PHP maps enums to String and there's no
190        // From<String> for the core enum type. Core->binding is always safe.
191        let enum_names_ref = &mapper.enum_names;
192        let php_conv_config = ConversionConfig {
193            cast_large_ints_to_i64: true,
194            enum_string_names: Some(enum_names_ref),
195            json_to_string: true,
196            include_cfg_metadata: false,
197            option_duration_on_defaults: true,
198            ..Default::default()
199        };
200        // Build transitive set of types that can't have binding->core From
201        let mut enum_tainted: AHashSet<String> = AHashSet::new();
202        for typ in &api.types {
203            if has_enum_named_field(typ, enum_names_ref) {
204                enum_tainted.insert(typ.name.clone());
205            }
206        }
207        // Transitively mark types that reference enum-tainted types
208        let mut changed = true;
209        while changed {
210            changed = false;
211            for typ in &api.types {
212                if !enum_tainted.contains(&typ.name)
213                    && typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
214                {
215                    enum_tainted.insert(typ.name.clone());
216                    changed = true;
217                }
218            }
219        }
220        for typ in &api.types {
221            // binding->core: only when not enum-tainted
222            if !enum_tainted.contains(&typ.name)
223                && alef_codegen::conversions::can_generate_conversion(typ, &convertible)
224            {
225                builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
226                    typ,
227                    &core_import,
228                    &php_conv_config,
229                ));
230            } else if enum_tainted.contains(&typ.name) && has_serde {
231                // Enum-tainted types can't use field-by-field From (no From<String> for core enum),
232                // but when serde is available we bridge via JSON serialization round-trip.
233                builder.add_item(&gen_serde_bridge_from(typ, &core_import));
234            } else if enum_tainted.contains(&typ.name) {
235                // Enum-tainted types: generate From with string->enum parsing for enum-Named
236                // fields, using first variant as fallback. Data-variant enum fields fill
237                // data fields with Default::default().
238                builder.add_item(&gen_enum_tainted_from_binding_to_core(
239                    typ,
240                    &core_import,
241                    enum_names_ref,
242                    &enum_tainted,
243                    &php_conv_config,
244                    &api.enums,
245                ));
246            }
247            // core->binding: always (enum->String via format, sanitized fields via format)
248            if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
249                builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
250                    typ,
251                    &core_import,
252                    &opaque_types,
253                    &php_conv_config,
254                ));
255            }
256        }
257
258        // Error converter functions
259        for error in &api.errors {
260            builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
261        }
262
263        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
264
265        // Add feature gate as inner attribute — entire crate is gated
266        let php_config = config.php.as_ref();
267        if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
268            builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
269            builder.add_inner_attribute(&format!(
270                "cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
271            ));
272        }
273
274        // PHP module entry point — required for ext-php-rs to register the extension
275        builder.add_item("#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {\n    module\n}");
276
277        let content = builder.build();
278
279        Ok(vec![GeneratedFile {
280            path: PathBuf::from(&output_dir).join("lib.rs"),
281            content,
282            generated_header: false,
283        }])
284    }
285
286    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
287        let extension_name = config.php_extension_name();
288        let class_name = extension_name.to_pascal_case();
289
290        // Generate PHP wrapper class
291        let mut content = String::from("<?php\n");
292        content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
293        content.push_str("declare(strict_types=1);\n\n");
294
295        // Determine namespace
296        let namespace = if extension_name.contains('_') {
297            let parts: Vec<&str> = extension_name.split('_').collect();
298            let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
299            ns_parts.join("\\")
300        } else {
301            class_name.clone()
302        };
303
304        content.push_str(&format!("namespace {};\n\n", namespace));
305        content.push_str(&format!("final class {}\n", class_name));
306        content.push_str("{\n");
307
308        // Generate wrapper methods for functions
309        for func in &api.functions {
310            let method_name = func.name.to_lower_camel_case();
311            let return_php_type = php_type(&func.return_type);
312
313            // PHPDoc block
314            content.push_str("    /**\n");
315            for line in func.doc.lines() {
316                if line.is_empty() {
317                    content.push_str("     *\n");
318                } else {
319                    content.push_str(&format!("     * {}\n", line));
320                }
321            }
322            if func.doc.is_empty() {
323                content.push_str(&format!("     * {}.\n", method_name));
324            }
325            content.push_str("     *\n");
326            for p in &func.params {
327                let ptype = php_phpdoc_type(&p.ty);
328                let nullable_prefix = if p.optional { "?" } else { "" };
329                content.push_str(&format!("     * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
330            }
331            let return_phpdoc = php_phpdoc_type(&func.return_type);
332            content.push_str(&format!("     * @return {}\n", return_phpdoc));
333            if func.error_type.is_some() {
334                content.push_str(&format!("     * @throws \\{}\\{}Exception\n", namespace, class_name));
335            }
336            content.push_str("     */\n");
337
338            // Method signature with type hints
339            content.push_str(&format!("    public static function {}(", method_name));
340
341            let params: Vec<String> = func
342                .params
343                .iter()
344                .map(|p| {
345                    let ptype = php_type(&p.ty);
346                    if p.optional {
347                        format!("?{} ${} = null", ptype, p.name)
348                    } else {
349                        format!("{} ${}", ptype, p.name)
350                    }
351                })
352                .collect();
353            content.push_str(&params.join(", "));
354            content.push_str(&format!("): {}\n", return_php_type));
355            content.push_str("    {\n");
356            // Async functions are registered in the extension with an `_async` suffix
357            // (see gen_async_function which generates `pub fn {name}_async`).
358            let ext_func_name = if func.is_async {
359                format!("{}_async", func.name)
360            } else {
361                func.name.clone()
362            };
363            content.push_str(&format!(
364                "        return \\{}({}); // delegate to extension function\n",
365                ext_func_name,
366                func.params
367                    .iter()
368                    .map(|p| format!("${}", p.name))
369                    .collect::<Vec<_>>()
370                    .join(", ")
371            ));
372            content.push_str("    }\n\n");
373        }
374
375        content.push_str("}\n");
376
377        // Use PHP stubs output path if configured, otherwise fall back to packages/php/src/.
378        // This is intentionally separate from config.output.php, which controls the Rust binding
379        // crate output directory (e.g., crates/kreuzcrawl-php/src/).
380        let output_dir = config
381            .php
382            .as_ref()
383            .and_then(|p| p.stubs.as_ref())
384            .map(|s| s.output.to_string_lossy().to_string())
385            .unwrap_or_else(|| "packages/php/src/".to_string());
386
387        Ok(vec![GeneratedFile {
388            path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
389            content,
390            generated_header: false,
391        }])
392    }
393
394    fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
395        let extension_name = config.php_extension_name();
396        let class_name = extension_name.to_pascal_case();
397
398        // Determine namespace (same logic as generate_public_api)
399        let namespace = if extension_name.contains('_') {
400            let parts: Vec<&str> = extension_name.split('_').collect();
401            let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
402            ns_parts.join("\\")
403        } else {
404            class_name.clone()
405        };
406
407        let mut content = String::from("<?php\n");
408        content.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n");
409        content.push_str("// Type stubs for the native PHP extension — declares classes\n");
410        content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
411        content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
412        content.push_str("declare(strict_types=1);\n\n");
413        // Use bracketed namespace syntax so we can add global-namespace function stubs later.
414        content.push_str(&format!("namespace {} {{\n\n", namespace));
415
416        // Exception class
417        content.push_str(&format!(
418            "class {}Exception extends \\RuntimeException\n{{\n",
419            class_name
420        ));
421        content.push_str("    public function getErrorCode(): int { }\n");
422        content.push_str("}\n\n");
423
424        // Opaque handle classes
425        for typ in &api.types {
426            if typ.is_opaque {
427                if !typ.doc.is_empty() {
428                    content.push_str("/**\n");
429                    for line in typ.doc.lines() {
430                        if line.is_empty() {
431                            content.push_str(" *\n");
432                        } else {
433                            content.push_str(&format!(" * {}\n", line));
434                        }
435                    }
436                    content.push_str(" */\n");
437                }
438                content.push_str(&format!("class {}\n{{\n", typ.name));
439                // Opaque handles have no public constructors in PHP
440                content.push_str("}\n\n");
441            }
442        }
443
444        // Record / struct types (non-opaque with fields)
445        for typ in &api.types {
446            if typ.is_opaque || typ.fields.is_empty() {
447                continue;
448            }
449            if !typ.doc.is_empty() {
450                content.push_str("/**\n");
451                for line in typ.doc.lines() {
452                    if line.is_empty() {
453                        content.push_str(" *\n");
454                    } else {
455                        content.push_str(&format!(" * {}\n", line));
456                    }
457                }
458                content.push_str(" */\n");
459            }
460            content.push_str(&format!("class {}\n{{\n", typ.name));
461
462            // Constructor with typed parameters.
463            // PHP requires required parameters to come before optional ones, so sort
464            // the fields: required first, then optional (preserving relative order within each group).
465            let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
466            sorted_fields.sort_by_key(|f| f.optional);
467
468            // Emit PHPDoc before the constructor for any array-typed fields so PHPStan
469            // understands the generic element type (e.g. `@param array<string> $items`).
470            let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
471                .iter()
472                .copied()
473                .filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
474                .collect();
475            if !array_fields.is_empty() {
476                content.push_str("    /**\n");
477                for f in &array_fields {
478                    let phpdoc = php_phpdoc_type(&f.ty);
479                    let nullable_prefix = if f.optional { "?" } else { "" };
480                    content.push_str(&format!("     * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
481                }
482                content.push_str("     */\n");
483            }
484
485            let params: Vec<String> = sorted_fields
486                .iter()
487                .map(|f| {
488                    let ptype = php_type(&f.ty);
489                    let nullable = if f.optional { format!("?{}", ptype) } else { ptype };
490                    let default = if f.optional { " = null" } else { "" };
491                    format!("        {} ${}{}", nullable, f.name, default)
492                })
493                .collect();
494            content.push_str("    public function __construct(\n");
495            content.push_str(&params.join(",\n"));
496            content.push_str("\n    ) { }\n\n");
497
498            // Getter methods for each field
499            for field in &typ.fields {
500                let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
501                let return_type = if field.optional {
502                    format!("?{}", php_type(&field.ty))
503                } else {
504                    php_type(&field.ty)
505                };
506                let getter_name = field.name.to_lower_camel_case();
507                // Emit PHPDoc for array return types so PHPStan knows the element type.
508                if is_array {
509                    let phpdoc = php_phpdoc_type(&field.ty);
510                    let nullable_prefix = if field.optional { "?" } else { "" };
511                    content.push_str(&format!("    /** @return {}{} */\n", nullable_prefix, phpdoc));
512                }
513                content.push_str(&format!(
514                    "    public function get{}(): {} {{ }}\n",
515                    getter_name.to_pascal_case(),
516                    return_type
517                ));
518            }
519
520            content.push_str("}\n\n");
521        }
522
523        // Enum constants (PHP 8.1+ enums)
524        for enum_def in &api.enums {
525            content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
526            for variant in &enum_def.variants {
527                content.push_str(&format!("    case {} = '{}';\n", variant.name, variant.name));
528            }
529            content.push_str("}\n\n");
530        }
531
532        // Close the namespaced block
533        content.push_str("} // end namespace\n\n");
534
535        // Extension function stubs (global namespace).
536        // The facade class delegates to these via `\func_name(...)`.
537        content.push_str("namespace {\n\n");
538
539        for func in &api.functions {
540            let return_type = php_type_fq(&func.return_type, &namespace);
541            let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
542            content.push_str("/**\n");
543            // PHPDoc params with fully-qualified types
544            for p in &func.params {
545                let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
546                let nullable_prefix = if p.optional { "?" } else { "" };
547                content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
548            }
549            content.push_str(&format!(" * @return {}\n */\n", return_phpdoc));
550
551            let params: Vec<String> = func
552                .params
553                .iter()
554                .map(|p| {
555                    let ptype = php_type_fq(&p.ty, &namespace);
556                    if p.optional {
557                        format!("?{} ${} = null", ptype, p.name)
558                    } else {
559                        format!("{} ${}", ptype, p.name)
560                    }
561                })
562                .collect();
563            // Async functions are registered in the extension with an `_async` suffix.
564            let stub_func_name = if func.is_async {
565                format!("{}_async", func.name)
566            } else {
567                func.name.clone()
568            };
569            content.push_str(&format!(
570                "function {}({}): {} {{ }}\n\n",
571                stub_func_name,
572                params.join(", "),
573                return_type
574            ));
575        }
576
577        content.push_str("}\n");
578
579        // Use stubs output path if configured, otherwise packages/php/stubs/
580        let output_dir = config
581            .php
582            .as_ref()
583            .and_then(|p| p.stubs.as_ref())
584            .map(|s| s.output.to_string_lossy().to_string())
585            .unwrap_or_else(|| "packages/php/stubs/".to_string());
586
587        Ok(vec![GeneratedFile {
588            path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
589            content,
590            generated_header: false,
591        }])
592    }
593
594    fn build_config(&self) -> Option<BuildConfig> {
595        Some(BuildConfig {
596            tool: "cargo",
597            crate_suffix: "-php",
598            depends_on_ffi: false,
599            post_build: vec![],
600        })
601    }
602}
603
604/// Map an IR [`TypeRef`] to a PHPDoc type string with generic parameters (e.g., `array<string>`).
605/// PHPStan at level `max` requires iterable value types in PHPDoc annotations.
606fn php_phpdoc_type(ty: &TypeRef) -> String {
607    match ty {
608        TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
609        TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
610        TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
611        _ => php_type(ty),
612    }
613}
614
615/// Map an IR [`TypeRef`] to a fully-qualified PHPDoc type string with generics (e.g., `array<\Ns\T>`).
616fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
617    match ty {
618        TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
619        TypeRef::Map(k, v) => format!(
620            "array<{}, {}>",
621            php_phpdoc_type_fq(k, namespace),
622            php_phpdoc_type_fq(v, namespace)
623        ),
624        TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
625        TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
626        _ => php_type(ty),
627    }
628}
629
630/// Map an IR [`TypeRef`] to a fully-qualified PHP type-hint string for use outside the namespace.
631fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
632    match ty {
633        TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
634        TypeRef::Optional(inner) => {
635            let inner_type = php_type_fq(inner, namespace);
636            format!("?{}", inner_type)
637        }
638        _ => php_type(ty),
639    }
640}
641
642/// Map an IR [`TypeRef`] to a PHP type-hint string.
643fn php_type(ty: &TypeRef) -> String {
644    match ty {
645        TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
646        TypeRef::Primitive(p) => match p {
647            PrimitiveType::Bool => "bool".to_string(),
648            PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
649            PrimitiveType::U8
650            | PrimitiveType::U16
651            | PrimitiveType::U32
652            | PrimitiveType::U64
653            | PrimitiveType::I8
654            | PrimitiveType::I16
655            | PrimitiveType::I32
656            | PrimitiveType::I64
657            | PrimitiveType::Usize
658            | PrimitiveType::Isize => "int".to_string(),
659        },
660        TypeRef::Optional(inner) => {
661            let inner_type = php_type(inner);
662            format!("?{}", inner_type)
663        }
664        TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
665        TypeRef::Named(name) => name.clone(),
666        TypeRef::Unit => "void".to_string(),
667        TypeRef::Duration => "float".to_string(),
668    }
669}