use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{FnArg, ImplItem, ItemImpl, Pat, ReturnType};
use crate::parsing::{MethodRename, RenameRule, ident_to_php_name};
use crate::prelude::*;
#[derive(FromMeta, Default, Debug, Copy, Clone)]
#[darling(default)]
pub struct PhpImplInterfaceArgs {
change_method_case: Option<RenameRule>,
}
const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface";
pub fn parser(args: PhpImplInterfaceArgs, input: &ItemImpl) -> Result<TokenStream> {
let change_method_case = args.change_method_case.unwrap_or(RenameRule::Camel);
let Some((_, trait_path, _)) = &input.trait_ else {
bail!(input => "`#[php_impl_interface]` can only be used on trait implementations (e.g., `impl SomeTrait for SomeStruct`)");
};
let mut interface_struct_path = trait_path.clone();
match interface_struct_path.segments.last_mut() {
Some(segment) => {
segment.ident = format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, segment.ident);
}
None => {
bail!(trait_path => "Invalid trait path");
}
}
let struct_ty = &input.self_ty;
let mut method_builders = Vec::new();
for item in &input.items {
let ImplItem::Fn(method) = item else {
continue;
};
let method_ident = &method.sig.ident;
let php_name = ident_to_php_name(method_ident);
let php_name = php_name.rename_method(change_method_case);
let has_self = method
.sig
.inputs
.iter()
.any(|arg| matches!(arg, FnArg::Receiver(_)));
let is_static = !has_self;
let builder = generate_method_builder(
&php_name,
struct_ty,
method_ident,
&method.sig.inputs,
&method.sig.output,
is_static,
);
method_builders.push(builder);
}
Ok(quote! {
#input
::ext_php_rs::inventory::submit! {
::ext_php_rs::internal::class::InterfaceRegistration {
class_type_id: ::std::any::TypeId::of::<#struct_ty>(),
interface_getter: || (
|| <#interface_struct_path as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
<#interface_struct_path as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
),
}
}
impl ::ext_php_rs::internal::class::InterfaceMethodsProvider<#struct_ty>
for ::ext_php_rs::internal::class::PhpClassImplCollector<#struct_ty>
{
fn get_interface_methods(self) -> ::std::vec::Vec<(
::ext_php_rs::builders::FunctionBuilder<'static>,
::ext_php_rs::flags::MethodFlags,
)> {
vec![
#(#method_builders),*
]
}
}
})
}
#[allow(clippy::too_many_lines)]
fn generate_method_builder(
php_name: &str,
struct_ty: &syn::Type,
method_ident: &syn::Ident,
inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
output: &ReturnType,
is_static: bool,
) -> TokenStream {
fn is_option_type(ty: &syn::Type) -> bool {
if let syn::Type::Path(type_path) = ty
&& let Some(segment) = type_path.path.segments.last()
{
return segment.ident == "Option";
}
false
}
let args: Vec<_> = inputs
.iter()
.filter_map(|arg| {
if let FnArg::Typed(pat_type) = arg
&& let Pat::Ident(pat_ident) = &*pat_type.pat
{
return Some((&pat_ident.ident, &pat_type.ty));
}
None
})
.collect();
let optional_boundary = {
let mut boundary = args.len();
for i in (0..args.len()).rev() {
if is_option_type(args[i].1) {
boundary = i;
} else {
break;
}
}
boundary
};
let required_args: Vec<_> = args[..optional_boundary].to_vec();
let optional_args: Vec<_> = args[optional_boundary..].to_vec();
let required_arg_declarations: Vec<_> = required_args
.iter()
.map(|(name, ty)| {
let php_name = ident_to_php_name(name);
quote! {
let mut #name = ::ext_php_rs::args::Arg::new(#php_name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE);
}
})
.collect();
let optional_arg_declarations: Vec<_> = optional_args
.iter()
.map(|(name, ty)| {
let php_name = ident_to_php_name(name);
quote! {
let mut #name = ::ext_php_rs::args::Arg::new(#php_name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE).allow_null();
}
})
.collect();
let required_arg_names: Vec<_> = required_args.iter().map(|(name, _)| *name).collect();
let optional_arg_names: Vec<_> = optional_args.iter().map(|(name, _)| *name).collect();
let arg_names: Vec<_> = args.iter().map(|(name, _)| name).collect();
let required_arg_value_accessors: Vec<_> = required_args
.iter()
.map(|(name, ty)| {
let php_name = ident_to_php_name(name);
if is_option_type(ty) {
quote! {
let #name: #ty = #name.val();
}
} else {
quote! {
let #name: #ty = match #name.val() {
Some(v) => v,
None => {
let msg = format!("Invalid value for argument `{}`", #php_name);
::ext_php_rs::exception::PhpException::default(msg.into())
.throw()
.expect("Failed to throw PHP exception.");
return;
}
};
}
}
})
.collect();
let optional_arg_value_accessors: Vec<_> = optional_args
.iter()
.map(|(name, ty)| {
quote! {
let #name: #ty = #name.val();
}
})
.collect();
let required_arg_builders: Vec<_> = required_args
.iter()
.map(|(name, ty)| {
let php_name = ident_to_php_name(name);
quote! {
.arg(::ext_php_rs::args::Arg::new(#php_name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE))
}
})
.collect();
let optional_arg_builders: Vec<_> = optional_args
.iter()
.map(|(name, ty)| {
let php_name = ident_to_php_name(name);
quote! {
.arg(::ext_php_rs::args::Arg::new(#php_name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE).allow_null())
}
})
.collect();
let flags = if is_static {
quote! { ::ext_php_rs::flags::MethodFlags::Public | ::ext_php_rs::flags::MethodFlags::Static }
} else {
quote! { ::ext_php_rs::flags::MethodFlags::Public }
};
let returns_call = match output {
ReturnType::Default => quote! {
.returns(::ext_php_rs::flags::DataType::Void, false, false)
},
ReturnType::Type(_, ty) => {
quote! {
.returns(
<#ty as ::ext_php_rs::convert::IntoZval>::TYPE,
false,
<#ty as ::ext_php_rs::convert::IntoZval>::NULLABLE,
)
}
}
};
let is_void = matches!(output, ReturnType::Default);
let result_handling = if is_void {
quote! {
}
} else {
quote! {
if let Err(e) = result.set_zval(retval, false) {
let e: ::ext_php_rs::exception::PhpException = e.into();
e.throw().expect("Failed to throw PHP exception.");
}
}
};
let handler_body = if is_static {
if is_void {
quote! {
#(#required_arg_declarations)*
#(#optional_arg_declarations)*
let parse = ex.parser()
#(.arg(&mut #required_arg_names))*
.not_required()
#(.arg(&mut #optional_arg_names))*
.parse();
if parse.is_err() {
return;
}
#(#required_arg_value_accessors)*
#(#optional_arg_value_accessors)*
<#struct_ty>::#method_ident(#(#arg_names),*);
}
} else {
quote! {
#(#required_arg_declarations)*
#(#optional_arg_declarations)*
let parse = ex.parser()
#(.arg(&mut #required_arg_names))*
.not_required()
#(.arg(&mut #optional_arg_names))*
.parse();
if parse.is_err() {
return;
}
#(#required_arg_value_accessors)*
#(#optional_arg_value_accessors)*
let result = <#struct_ty>::#method_ident(#(#arg_names),*);
#result_handling
}
}
} else if is_void {
quote! {
let (parse, this) = ex.parser_method::<#struct_ty>();
let this = match this {
Some(this) => this,
None => {
::ext_php_rs::exception::PhpException::default("Failed to get $this".into())
.throw()
.expect("Failed to throw PHP exception.");
return;
}
};
#(#required_arg_declarations)*
#(#optional_arg_declarations)*
let parse_result = parse
#(.arg(&mut #required_arg_names))*
.not_required()
#(.arg(&mut #optional_arg_names))*
.parse();
if parse_result.is_err() {
return;
}
#(#required_arg_value_accessors)*
#(#optional_arg_value_accessors)*
this.#method_ident(#(#arg_names),*);
}
} else {
quote! {
let (parse, this) = ex.parser_method::<#struct_ty>();
let this = match this {
Some(this) => this,
None => {
::ext_php_rs::exception::PhpException::default("Failed to get $this".into())
.throw()
.expect("Failed to throw PHP exception.");
return;
}
};
#(#required_arg_declarations)*
#(#optional_arg_declarations)*
let parse_result = parse
#(.arg(&mut #required_arg_names))*
.not_required()
#(.arg(&mut #optional_arg_names))*
.parse();
if parse_result.is_err() {
return;
}
#(#required_arg_value_accessors)*
#(#optional_arg_value_accessors)*
let result = this.#method_ident(#(#arg_names),*);
#result_handling
}
};
quote! {
(
::ext_php_rs::builders::FunctionBuilder::new(#php_name, {
::ext_php_rs::zend_fastcall! {
extern fn handler(
ex: &mut ::ext_php_rs::zend::ExecuteData,
retval: &mut ::ext_php_rs::types::Zval,
) {
use ::ext_php_rs::convert::IntoZval;
use ::ext_php_rs::zend::try_catch;
use ::std::panic::AssertUnwindSafe;
let catch_result = try_catch(AssertUnwindSafe(|| {
#handler_body
}));
if catch_result.is_err() {
::ext_php_rs::zend::run_bailout_cleanups();
unsafe {
::ext_php_rs::zend::bailout();
}
}
}
}
handler
})
#(#required_arg_builders)*
.not_required()
#(#optional_arg_builders)*
#returns_call,
#flags
)
}
}