mod functions;
mod helpers;
mod types;
use crate::type_map::PhpMapper;
use ahash::AHashSet;
use alef_codegen::builder::RustFileBuilder;
use alef_codegen::conversions::ConversionConfig;
use alef_codegen::generators::RustBindingConfig;
use alef_codegen::generators::{self, AsyncPattern};
use alef_core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use alef_core::config::{AlefConfig, Language, detect_serde_available, resolve_output_dir};
use alef_core::hash::{self, CommentStyle};
use alef_core::ir::ApiSurface;
use alef_core::ir::{PrimitiveType, TypeRef};
use heck::{ToLowerCamelCase, ToPascalCase};
use std::path::PathBuf;
use functions::{gen_async_function_as_static_method, gen_function_as_static_method};
use helpers::{
gen_enum_tainted_from_binding_to_core, gen_serde_bridge_from, gen_tokio_runtime, has_enum_named_field,
references_named_type,
};
use types::{
gen_enum_constants, gen_flat_data_enum, gen_flat_data_enum_from_impls, gen_flat_data_enum_methods,
gen_opaque_struct_methods, gen_php_struct, is_tagged_data_enum,
};
pub struct PhpBackend;
impl PhpBackend {
fn binding_config(core_import: &str, has_serde: bool) -> RustBindingConfig<'_> {
RustBindingConfig {
struct_attrs: &["php_class"],
field_attrs: &[],
struct_derives: &["Clone"],
method_block_attr: Some("php_impl"),
constructor_attr: "",
static_attr: None,
function_attr: "#[php_function]",
enum_attrs: &[],
enum_derives: &[],
needs_signature: false,
signature_prefix: "",
signature_suffix: "",
core_import,
async_pattern: AsyncPattern::TokioBlockOn,
has_serde,
type_name_prefix: "",
option_duration_on_defaults: true,
opaque_type_names: &[],
}
}
}
impl Backend for PhpBackend {
fn name(&self) -> &str {
"php"
}
fn language(&self) -> Language {
Language::Php
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
..Capabilities::default()
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let data_enum_names: AHashSet<String> = api
.enums
.iter()
.filter(|e| is_tagged_data_enum(e))
.map(|e| e.name.clone())
.collect();
let enum_names: AHashSet<String> = api
.enums
.iter()
.filter(|e| !is_tagged_data_enum(e))
.map(|e| e.name.clone())
.collect();
let mapper = PhpMapper {
enum_names: enum_names.clone(),
data_enum_names: data_enum_names.clone(),
};
let core_import = config.core_import();
let php_config = config.php.as_ref();
let exclude_functions = php_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
let exclude_types = php_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
let output_dir = resolve_output_dir(
config.output.php.as_ref(),
&config.crate_config.name,
"crates/{name}-php/src/",
);
let has_serde = detect_serde_available(&output_dir);
let cfg = Self::binding_config(&core_import, has_serde);
let mut builder = RustFileBuilder::new().with_generated_header();
builder.add_inner_attribute("allow(dead_code, unused_imports, unused_variables)");
builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unnecessary_cast, clippy::unused_unit, clippy::unwrap_or_default, clippy::derivable_impls, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions)");
builder.add_import("ext_php_rs::prelude::*");
if has_serde {
builder.add_import("serde_json");
}
for trait_path in generators::collect_trait_imports(api) {
builder.add_import(&trait_path);
}
let has_maps = api.types.iter().any(|t| {
t.fields
.iter()
.any(|f| matches!(&f.ty, alef_core::ir::TypeRef::Map(_, _)))
}) || api
.functions
.iter()
.any(|f| matches!(&f.return_type, alef_core::ir::TypeRef::Map(_, _)));
if has_maps {
builder.add_import("std::collections::HashMap");
}
let custom_mods = config.custom_modules.for_language(Language::Php);
for module in custom_mods {
builder.add_item(&format!("pub mod {module};"));
}
let has_async =
api.functions.iter().any(|f| f.is_async) || api.types.iter().any(|t| t.methods.iter().any(|m| m.is_async));
if has_async {
builder.add_item(&gen_tokio_runtime());
}
let opaque_types: AHashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque)
.map(|t| t.name.clone())
.collect();
if !opaque_types.is_empty() {
builder.add_import("std::sync::Arc");
}
let extension_name = config.php_extension_name();
let php_namespace = if extension_name.contains('_') {
let parts: Vec<&str> = extension_name.split('_').collect();
let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
ns_parts.join("\\")
} else {
extension_name.to_pascal_case()
};
let adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Php)?;
for adapter in &config.adapters {
match adapter.pattern {
alef_core::config::AdapterPattern::Streaming => {
let key = format!("{}.__stream_struct__", adapter.item_type.as_deref().unwrap_or(""));
if let Some(struct_code) = adapter_bodies.get(&key) {
builder.add_item(struct_code);
}
}
alef_core::config::AdapterPattern::CallbackBridge => {
let struct_key = format!("{}.__bridge_struct__", adapter.name);
let impl_key = format!("{}.__bridge_impl__", adapter.name);
if let Some(struct_code) = adapter_bodies.get(&struct_key) {
builder.add_item(struct_code);
}
if let Some(impl_code) = adapter_bodies.get(&impl_key) {
builder.add_item(impl_code);
}
}
_ => {}
}
}
for typ in api
.types
.iter()
.filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
{
if typ.is_opaque {
let ns_escaped = php_namespace.replace('\\', "\\\\");
let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped, typ.name);
let opaque_attr_arr = ["php_class", php_name_attr.as_str()];
let opaque_cfg = RustBindingConfig {
struct_attrs: &opaque_attr_arr,
..cfg
};
builder.add_item(&generators::gen_opaque_struct(typ, &opaque_cfg));
builder.add_item(&gen_opaque_struct_methods(
typ,
&mapper,
&opaque_types,
&core_import,
&adapter_bodies,
));
} else {
builder.add_item(&gen_php_struct(typ, &mapper, &cfg, Some(&php_namespace), &enum_names));
builder.add_item(&types::gen_struct_methods_with_exclude(
typ,
&mapper,
has_serde,
&core_import,
&opaque_types,
&enum_names,
&api.enums,
&exclude_functions,
));
}
}
for enum_def in &api.enums {
if is_tagged_data_enum(enum_def) {
builder.add_item(&gen_flat_data_enum(enum_def, &mapper, Some(&php_namespace)));
builder.add_item(&gen_flat_data_enum_methods(enum_def, &mapper));
} else {
builder.add_item(&gen_enum_constants(enum_def));
}
}
let included_functions: Vec<_> = api
.functions
.iter()
.filter(|f| !exclude_functions.contains(&f.name))
.collect();
if !included_functions.is_empty() {
let facade_class_name = extension_name.to_pascal_case();
let mut method_items: Vec<String> = Vec::new();
for func in included_functions {
let bridge_param = crate::trait_bridge::find_bridge_param(func, &config.trait_bridges);
if let Some((param_idx, bridge_cfg)) = bridge_param {
method_items.push(crate::trait_bridge::gen_bridge_function(
func,
param_idx,
bridge_cfg,
&mapper,
&opaque_types,
&core_import,
));
} else if func.is_async {
method_items.push(gen_async_function_as_static_method(
func,
&mapper,
&opaque_types,
&core_import,
&config.trait_bridges,
));
} else {
method_items.push(gen_function_as_static_method(
func,
&mapper,
&opaque_types,
&core_import,
&config.trait_bridges,
has_serde,
));
}
}
let methods_joined = method_items
.iter()
.map(|m| {
m.lines()
.map(|l| {
if l.is_empty() {
String::new()
} else {
format!(" {l}")
}
})
.collect::<Vec<_>>()
.join("\n")
})
.collect::<Vec<_>>()
.join("\n\n");
let php_api_class_name = format!("{facade_class_name}Api");
let ns_escaped_facade = php_namespace.replace('\\', "\\\\");
let php_name_attr = format!("php(name = \"{}\\\\{}\")", ns_escaped_facade, php_api_class_name);
let facade_struct = format!(
"#[php_class]\n#[{php_name_attr}]\npub struct {facade_class_name}Api;\n\n#[php_impl]\nimpl {facade_class_name}Api {{\n{methods_joined}\n}}"
);
builder.add_item(&facade_struct);
for bridge_cfg in &config.trait_bridges {
if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
let bridge = crate::trait_bridge::gen_trait_bridge(
trait_type,
bridge_cfg,
&core_import,
&config.error_type(),
&config.error_constructor(),
api,
);
for imp in &bridge.imports {
builder.add_import(imp);
}
builder.add_item(&bridge.code);
}
}
}
let convertible = alef_codegen::conversions::convertible_types(api);
let core_to_binding = alef_codegen::conversions::core_to_binding_convertible_types(api);
let input_types = alef_codegen::conversions::input_type_names(api);
let enum_names_ref = &mapper.enum_names;
let php_conv_config = ConversionConfig {
cast_large_ints_to_i64: true,
enum_string_names: Some(enum_names_ref),
json_to_string: true,
include_cfg_metadata: false,
option_duration_on_defaults: true,
..Default::default()
};
let mut enum_tainted: AHashSet<String> = AHashSet::new();
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if has_enum_named_field(typ, enum_names_ref) {
enum_tainted.insert(typ.name.clone());
}
}
let mut changed = true;
while changed {
changed = false;
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if !enum_tainted.contains(&typ.name)
&& typ.fields.iter().any(|f| references_named_type(&f.ty, &enum_tainted))
{
enum_tainted.insert(typ.name.clone());
changed = true;
}
}
}
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if input_types.contains(&typ.name)
&& !enum_tainted.contains(&typ.name)
&& alef_codegen::conversions::can_generate_conversion(typ, &convertible)
{
builder.add_item(&alef_codegen::conversions::gen_from_binding_to_core_cfg(
typ,
&core_import,
&php_conv_config,
));
} else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) && has_serde {
builder.add_item(&gen_serde_bridge_from(typ, &core_import));
} else if input_types.contains(&typ.name) && enum_tainted.contains(&typ.name) {
builder.add_item(&gen_enum_tainted_from_binding_to_core(
typ,
&core_import,
enum_names_ref,
&enum_tainted,
&php_conv_config,
&api.enums,
));
}
if alef_codegen::conversions::can_generate_conversion(typ, &core_to_binding) {
builder.add_item(&alef_codegen::conversions::gen_from_core_to_binding_cfg(
typ,
&core_import,
&opaque_types,
&php_conv_config,
));
}
}
for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
builder.add_item(&gen_flat_data_enum_from_impls(enum_def, &core_import));
}
for error in &api.errors {
builder.add_item(&alef_codegen::error_gen::gen_php_error_converter(error, &core_import));
}
let php_config = config.php.as_ref();
if let Some(feature_name) = php_config.and_then(|c| c.feature_gate.as_deref()) {
builder.add_inner_attribute(&format!("cfg(feature = \"{feature_name}\")"));
builder.add_inner_attribute(&format!(
"cfg_attr(all(windows, target_env = \"msvc\", feature = \"{feature_name}\"), feature(abi_vectorcall))"
));
}
let mut class_registrations = String::new();
for typ in api
.types
.iter()
.filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
{
class_registrations.push_str(&format!("\n .class::<{}>()", typ.name));
}
if !api.functions.is_empty() {
let facade_class_name = extension_name.to_pascal_case();
class_registrations.push_str(&format!("\n .class::<{facade_class_name}Api>()"));
}
for enum_def in api.enums.iter().filter(|e| is_tagged_data_enum(e)) {
class_registrations.push_str(&format!("\n .class::<{}>()", enum_def.name));
}
builder.add_item(&format!(
"#[php_module]\npub fn get_module(module: ModuleBuilder) -> ModuleBuilder {{\n module{class_registrations}\n}}"
));
let content = builder.build();
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join("lib.rs"),
content,
generated_header: false,
}])
}
fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let extension_name = config.php_extension_name();
let class_name = extension_name.to_pascal_case();
let mut content = String::from("<?php\n");
content.push_str(&hash::header(CommentStyle::DoubleSlash));
content.push_str("declare(strict_types=1);\n\n");
let namespace = if extension_name.contains('_') {
let parts: Vec<&str> = extension_name.split('_').collect();
let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
ns_parts.join("\\")
} else {
class_name.clone()
};
content.push_str(&format!("namespace {};\n\n", namespace));
content.push_str(&format!("final class {}\n", class_name));
content.push_str("{\n");
let bridge_param_names_pub: ahash::AHashSet<&str> = config
.trait_bridges
.iter()
.filter_map(|b| b.param_name.as_deref())
.collect();
for func in &api.functions {
let method_name = func.name.to_lower_camel_case();
let return_php_type = php_type(&func.return_type);
let visible_params: Vec<_> = func
.params
.iter()
.filter(|p| !bridge_param_names_pub.contains(p.name.as_str()))
.collect();
content.push_str(" /**\n");
for line in func.doc.lines() {
if line.is_empty() {
content.push_str(" *\n");
} else {
content.push_str(&format!(" * {}\n", line));
}
}
if func.doc.is_empty() {
content.push_str(&format!(" * {}.\n", method_name));
}
content.push_str(" *\n");
for p in &visible_params {
let ptype = php_phpdoc_type(&p.ty);
let nullable_prefix = if p.optional { "?" } else { "" };
content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
}
let return_phpdoc = php_phpdoc_type(&func.return_type);
content.push_str(&format!(" * @return {}\n", return_phpdoc));
if func.error_type.is_some() {
content.push_str(&format!(" * @throws \\{}\\{}Exception\n", namespace, class_name));
}
content.push_str(" */\n");
let mut sorted_visible_params = visible_params.clone();
sorted_visible_params.sort_by_key(|p| p.optional);
content.push_str(&format!(" public static function {}(", method_name));
let params: Vec<String> = sorted_visible_params
.iter()
.map(|p| {
let ptype = php_type(&p.ty);
if p.optional {
format!("?{} ${} = null", ptype, p.name)
} else {
format!("{} ${}", ptype, p.name)
}
})
.collect();
content.push_str(¶ms.join(", "));
content.push_str(&format!("): {}\n", return_php_type));
content.push_str(" {\n");
let ext_method_name = if func.is_async {
format!("{}_async", func.name).to_lower_camel_case()
} else {
func.name.to_lower_camel_case()
};
let is_void = matches!(&func.return_type, TypeRef::Unit);
let call_expr = format!(
"\\{}\\{}Api::{}({})",
namespace,
class_name,
ext_method_name,
sorted_visible_params
.iter()
.map(|p| format!("${}", p.name))
.collect::<Vec<_>>()
.join(", ")
);
if is_void {
content.push_str(&format!(
" {}; // delegate to native extension class\n",
call_expr
));
} else {
content.push_str(&format!(
" return {}; // delegate to native extension class\n",
call_expr
));
}
content.push_str(" }\n\n");
}
content.push_str("}\n");
let output_dir = config
.php
.as_ref()
.and_then(|p| p.stubs.as_ref())
.map(|s| s.output.to_string_lossy().to_string())
.unwrap_or_else(|| "packages/php/src/".to_string());
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join(format!("{}.php", class_name)),
content,
generated_header: false,
}])
}
fn generate_type_stubs(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let extension_name = config.php_extension_name();
let class_name = extension_name.to_pascal_case();
let namespace = if extension_name.contains('_') {
let parts: Vec<&str> = extension_name.split('_').collect();
let ns_parts: Vec<String> = parts.iter().map(|p| p.to_pascal_case()).collect();
ns_parts.join("\\")
} else {
class_name.clone()
};
let mut content = String::from("<?php\n\n");
content.push_str(&hash::header(CommentStyle::DoubleSlash));
content.push_str("// Type stubs for the native PHP extension — declares classes\n");
content.push_str("// provided at runtime by the compiled Rust extension (.so/.dll).\n");
content.push_str("// Include this in phpstan.neon scanFiles for static analysis.\n\n");
content.push_str("declare(strict_types=1);\n\n");
content.push_str(&format!("namespace {} {{\n\n", namespace));
content.push_str(&format!(
"class {}Exception extends \\RuntimeException\n{{\n",
class_name
));
content.push_str(" public function getErrorCode(): int { }\n");
content.push_str("}\n\n");
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if typ.is_opaque {
if !typ.doc.is_empty() {
content.push_str("/**\n");
for line in typ.doc.lines() {
if line.is_empty() {
content.push_str(" *\n");
} else {
content.push_str(&format!(" * {}\n", line));
}
}
content.push_str(" */\n");
}
content.push_str(&format!("class {}\n{{\n", typ.name));
content.push_str("}\n\n");
}
}
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if typ.is_opaque || typ.fields.is_empty() {
continue;
}
if !typ.doc.is_empty() {
content.push_str("/**\n");
for line in typ.doc.lines() {
if line.is_empty() {
content.push_str(" *\n");
} else {
content.push_str(&format!(" * {}\n", line));
}
}
content.push_str(" */\n");
}
content.push_str(&format!("class {}\n{{\n", typ.name));
for field in &typ.fields {
let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
let prop_type = if field.optional {
let inner = php_type(&field.ty);
if inner.starts_with('?') {
inner
} else {
format!("?{inner}")
}
} else {
php_type(&field.ty)
};
if is_array {
let phpdoc = php_phpdoc_type(&field.ty);
let nullable_prefix = if field.optional { "?" } else { "" };
content.push_str(&format!(" /** @var {}{} */\n", nullable_prefix, phpdoc));
}
content.push_str(&format!(" public {} ${};\n", prop_type, field.name));
}
content.push('\n');
let mut sorted_fields: Vec<&alef_core::ir::FieldDef> = typ.fields.iter().collect();
sorted_fields.sort_by_key(|f| f.optional);
let array_fields: Vec<&alef_core::ir::FieldDef> = sorted_fields
.iter()
.copied()
.filter(|f| matches!(&f.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)))
.collect();
if !array_fields.is_empty() {
content.push_str(" /**\n");
for f in &array_fields {
let phpdoc = php_phpdoc_type(&f.ty);
let nullable_prefix = if f.optional { "?" } else { "" };
content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, phpdoc, f.name));
}
content.push_str(" */\n");
}
let params: Vec<String> = sorted_fields
.iter()
.map(|f| {
let ptype = php_type(&f.ty);
let nullable = if f.optional && !ptype.starts_with('?') {
format!("?{ptype}")
} else {
ptype
};
let default = if f.optional { " = null" } else { "" };
format!(" {} ${}{}", nullable, f.name, default)
})
.collect();
content.push_str(" public function __construct(\n");
content.push_str(¶ms.join(",\n"));
content.push_str("\n ) { }\n\n");
for field in &typ.fields {
let is_array = matches!(&field.ty, TypeRef::Vec(_) | TypeRef::Map(_, _));
let return_type = if field.optional {
let inner = php_type(&field.ty);
if inner.starts_with('?') {
inner
} else {
format!("?{inner}")
}
} else {
php_type(&field.ty)
};
let getter_name = field.name.to_lower_camel_case();
if is_array {
let phpdoc = php_phpdoc_type(&field.ty);
let nullable_prefix = if field.optional { "?" } else { "" };
content.push_str(&format!(" /** @return {}{} */\n", nullable_prefix, phpdoc));
}
content.push_str(&format!(
" public function get{}(): {} {{ }}\n",
getter_name.to_pascal_case(),
return_type
));
}
content.push_str("}\n\n");
}
for enum_def in &api.enums {
content.push_str(&format!("enum {}: string\n{{\n", enum_def.name));
for variant in &enum_def.variants {
content.push_str(&format!(" case {} = '{}';\n", variant.name, variant.name));
}
content.push_str("}\n\n");
}
if !api.functions.is_empty() {
let bridge_param_names_stubs: ahash::AHashSet<&str> = config
.trait_bridges
.iter()
.filter_map(|b| b.param_name.as_deref())
.collect();
content.push_str(&format!("class {}Api\n{{\n", class_name));
for func in &api.functions {
let return_type = php_type_fq(&func.return_type, &namespace);
let return_phpdoc = php_phpdoc_type_fq(&func.return_type, &namespace);
let visible_params: Vec<_> = func
.params
.iter()
.filter(|p| !bridge_param_names_stubs.contains(p.name.as_str()))
.collect();
let mut sorted_visible_params = visible_params.clone();
sorted_visible_params.sort_by_key(|p| p.optional);
let has_array_params = visible_params
.iter()
.any(|p| matches!(&p.ty, TypeRef::Vec(_) | TypeRef::Map(_, _)));
let has_array_return = matches!(&func.return_type, TypeRef::Vec(_) | TypeRef::Map(_, _))
|| matches!(&func.return_type, TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Vec(_) | TypeRef::Map(_, _)));
if has_array_params || has_array_return {
content.push_str(" /**\n");
for p in &visible_params {
let ptype = php_phpdoc_type_fq(&p.ty, &namespace);
let nullable_prefix = if p.optional { "?" } else { "" };
content.push_str(&format!(" * @param {}{} ${}\n", nullable_prefix, ptype, p.name));
}
content.push_str(&format!(" * @return {}\n", return_phpdoc));
content.push_str(" */\n");
}
let params: Vec<String> = sorted_visible_params
.iter()
.map(|p| {
let ptype = php_type_fq(&p.ty, &namespace);
if p.optional {
format!("?{} ${} = null", ptype, p.name)
} else {
format!("{} ${}", ptype, p.name)
}
})
.collect();
let stub_method_name = if func.is_async {
format!("{}_async", func.name).to_lower_camel_case()
} else {
func.name.to_lower_camel_case()
};
content.push_str(&format!(
" public static function {}({}): {} {{ }}\n",
stub_method_name,
params.join(", "),
return_type
));
}
content.push_str("}\n\n");
}
content.push_str("} // end namespace\n");
let output_dir = config
.php
.as_ref()
.and_then(|p| p.stubs.as_ref())
.map(|s| s.output.to_string_lossy().to_string())
.unwrap_or_else(|| "packages/php/stubs/".to_string());
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join(format!("{}_extension.php", extension_name)),
content,
generated_header: false,
}])
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "cargo",
crate_suffix: "-php",
build_dep: BuildDependency::None,
post_build: vec![],
})
}
}
fn php_phpdoc_type(ty: &TypeRef) -> String {
match ty {
TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type(inner)),
TypeRef::Map(k, v) => format!("array<{}, {}>", php_phpdoc_type(k), php_phpdoc_type(v)),
TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type(inner)),
_ => php_type(ty),
}
}
fn php_phpdoc_type_fq(ty: &TypeRef, namespace: &str) -> String {
match ty {
TypeRef::Vec(inner) => format!("array<{}>", php_phpdoc_type_fq(inner, namespace)),
TypeRef::Map(k, v) => format!(
"array<{}, {}>",
php_phpdoc_type_fq(k, namespace),
php_phpdoc_type_fq(v, namespace)
),
TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
TypeRef::Optional(inner) => format!("?{}", php_phpdoc_type_fq(inner, namespace)),
_ => php_type(ty),
}
}
fn php_type_fq(ty: &TypeRef, namespace: &str) -> String {
match ty {
TypeRef::Named(name) => format!("\\{}\\{}", namespace, name),
TypeRef::Optional(inner) => {
let inner_type = php_type_fq(inner, namespace);
if inner_type.starts_with('?') {
inner_type
} else {
format!("?{inner_type}")
}
}
_ => php_type(ty),
}
}
fn php_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String | TypeRef::Char | TypeRef::Json | TypeRef::Bytes | TypeRef::Path => "string".to_string(),
TypeRef::Primitive(p) => match p {
PrimitiveType::Bool => "bool".to_string(),
PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
PrimitiveType::U8
| PrimitiveType::U16
| PrimitiveType::U32
| PrimitiveType::U64
| PrimitiveType::I8
| PrimitiveType::I16
| PrimitiveType::I32
| PrimitiveType::I64
| PrimitiveType::Usize
| PrimitiveType::Isize => "int".to_string(),
},
TypeRef::Optional(inner) => {
let inner_type = php_type(inner);
if inner_type.starts_with('?') {
inner_type
} else {
format!("?{inner_type}")
}
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => "array".to_string(),
TypeRef::Named(name) => name.clone(),
TypeRef::Unit => "void".to_string(),
TypeRef::Duration => "float".to_string(),
}
}