ext-php-rs-derive 0.12.1

Derive macros for ext-php-rs.
Documentation
//! Implementation for the `#[php_impl_interface]` macro.
//!
//! This macro allows classes to implement PHP interfaces by implementing Rust
//! traits that are marked with `#[php_interface]`.
//!
//! Uses the `inventory` crate for cross-crate interface discovery.
//! Method registration uses autoref specialization to avoid PHP symbol
//! resolution issues at binary load time.

use darling::FromMeta;
use darling::util::SpannedValue;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::{FnArg, ImplItem, ItemImpl, Pat, ReturnType};

use crate::interface::method_name_const;
use crate::parsing::{RenameRule, ident_to_php_name, reject_php_attrs};
use crate::prelude::*;

/// Attributes for the `#[php_impl_interface]` macro.
#[derive(FromMeta, Default, Debug)]
#[darling(default)]
pub struct PhpImplInterfaceArgs {
    /// Removed: method names come from the `#[php_interface]` trait.
    change_method_case: Option<SpannedValue<RenameRule>>,
}

const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface";

/// Parses a trait impl block and generates the interface implementation
/// registration.
///
/// # Arguments
///
/// * `args` - The macro arguments. None are accepted.
/// * `input` - The trait impl block (e.g., `impl SomeTrait for SomeStruct { ...
///   }`)
///
/// # Generated Code
///
/// The macro generates:
/// 1. The original trait impl block (passed through unchanged)
/// 2. An `inventory::submit!` call to register the interface implementation
/// 3. An `InterfaceMethodsProvider` trait implementation for method
///    registration
///
/// # Path Resolution
///
/// The macro preserves the full module path of the trait, so
/// `impl other::MyTrait for Foo` will correctly reference
/// `other::PhpInterfaceMyTrait`.
pub fn parser(args: &PhpImplInterfaceArgs, input: &ItemImpl) -> Result<TokenStream> {
    if let Some(change_method_case) = &args.change_method_case {
        bail!(change_method_case.span() => "`change_method_case` is not accepted here; method names come from the `#[php_interface]` trait.");
    }
    // Extract the trait being implemented
    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`)");
    };

    // Clone the trait path and modify the last segment to add PhpInterface prefix
    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");
        }
    }

    // Get the struct type being implemented
    let struct_ty = &input.self_ty;

    reject_php_attrs(
        &input.attrs,
        "`#[php_impl_interface]` blocks; pass them as macro arguments instead",
    )?;

    // Generate method builders for each trait method
    let mut method_builders = Vec::new();

    for item in &input.items {
        let attrs = match item {
            ImplItem::Fn(method) => &method.attrs,
            ImplItem::Const(c) => &c.attrs,
            ImplItem::Type(t) => &t.attrs,
            _ => continue,
        };
        reject_php_attrs(attrs, "`#[php_impl_interface]` items")?;
        let ImplItem::Fn(method) = item else {
            continue;
        };
        let method_ident = &method.sig.ident;
        let name_const = method_name_const(method_ident);
        let php_name = quote! { <#interface_struct_path>::#name_const };

        // Check if this is a static method (no self receiver)
        let has_self = method
            .sig
            .inputs
            .iter()
            .any(|arg| matches!(arg, FnArg::Receiver(_)));
        let is_static = !has_self;

        // Generate the method builder
        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! {
        // Pass through the original trait implementation
        #input

        // Register the interface implementation using inventory for cross-crate discovery
        ::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
                ),
            }
        }

        // Implement InterfaceMethodsProvider for the class (direct impl, not on reference)
        // This uses autoref specialization - the direct impl takes precedence over the
        // default reference impl.
        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),*
                ]
            }
        }
    })
}

/// Generates a method builder expression (`FunctionBuilder`, `MethodFlags`).
/// The handler is defined inside the `FunctionBuilder::new()` call, so it's
/// only instantiated when `get_interface_methods()` is called at runtime.
#[allow(clippy::too_many_lines)]
fn generate_method_builder(
    php_name: &TokenStream,
    struct_ty: &syn::Type,
    method_ident: &syn::Ident,
    inputs: &syn::punctuated::Punctuated<FnArg, syn::token::Comma>,
    output: &ReturnType,
    is_static: bool,
) -> TokenStream {
    // Collect non-self arguments in order
    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();

    // The required count follows each parameter's `FromZvalMut::NULLABLE`, so
    // an alias of `Option<T>` behaves like `Option<T>`. An `Option<T>` before a
    // required parameter stays required.
    let omittable = args
        .iter()
        .map(|(_, ty)| quote! { <#ty as ::ext_php_rs::convert::FromZvalMut>::NULLABLE });
    let required = quote! {
        const __REQUIRED: usize = ::ext_php_rs::args::required_count(&[#(#omittable),*]);
    };

    let arg_declarations: Vec<_> = args
        .iter()
        .map(|(name, ty)| {
            let php_name = ident_to_php_name(name);
            quote! {
                let mut #name = ::ext_php_rs::args::Arg::of::<#ty>(#php_name);
            }
        })
        .collect();

    let arg_names: Vec<_> = args.iter().map(|(name, _)| name).collect();

    // Value accessors that extract values from the Arg objects after parsing.
    // An omitted argument takes `FromZvalMut::from_missing()`.
    let arg_value_accessors: Vec<_> = args
        .iter()
        .map(|(name, ty)| {
            let php_name = ident_to_php_name(name);
            quote! {
                let #name: #ty = match match #name.zval() {
                    Some(zval) => <#ty as ::ext_php_rs::convert::FromZvalMut>::from_zval_mut(zval.dereference_mut()),
                    None => <#ty as ::ext_php_rs::convert::FromZvalMut>::from_missing(),
                } {
                    Some(value) => value,
                    None => {
                        let msg = format!("Invalid value for argument `{}`", #php_name);
                        ::ext_php_rs::exception::PhpException::from_message(msg.into())
                            .throw();
                        return;
                    }
                };
            }
        })
        .collect();

    // Generate .arg() calls for PHP reflection metadata (for FunctionBuilder)
    let arg_builders: Vec<_> = args
        .iter()
        .map(|(name, ty)| {
            let php_name = ident_to_php_name(name);
            quote! {
                .arg(::ext_php_rs::args::Arg::of::<#ty>(#php_name))
            }
        })
        .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 }
    };

    // Generate the .returns() call - void for no return type, or the actual type
    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,
                )
            }
        }
    };

    // Check if return type is void
    let is_void = matches!(output, ReturnType::Default);

    // Generate the method call and result handling based on whether it's void
    let result_handling = if is_void {
        quote! {
            // Void method - just call it, don't try to set return value
        }
    } else {
        quote! {
            if let Err(e) = result.set_zval(retval, false) {
                let e: ::ext_php_rs::exception::PhpException = e.into();
                e.throw();
            }
        }
    };

    let handler_body = if is_static {
        if is_void {
            quote! {
                #(#arg_declarations)*
                let parse = ex.parser()
                    #(.arg(&mut #arg_names))*
                    .required_args(__REQUIRED)
                    .parse();
                if parse.is_err() {
                    return;
                }
                #(#arg_value_accessors)*
                <#struct_ty>::#method_ident(#(#arg_names),*);
            }
        } else {
            quote! {
                #(#arg_declarations)*
                let parse = ex.parser()
                    #(.arg(&mut #arg_names))*
                    .required_args(__REQUIRED)
                    .parse();
                if parse.is_err() {
                    return;
                }
                #(#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::from_message("Failed to get $this".into())
                        .throw();
                    return;
                }
            };
            #(#arg_declarations)*
            let parse_result = parse
                #(.arg(&mut #arg_names))*
                .required_args(__REQUIRED)
                .parse();
            if parse_result.is_err() {
                return;
            }
            #(#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::from_message("Failed to get $this".into())
                        .throw();
                    return;
                }
            };
            #(#arg_declarations)*
            let parse_result = parse
                #(.arg(&mut #arg_names))*
                .required_args(__REQUIRED)
                .parse();
            if parse_result.is_err() {
                return;
            }
            #(#arg_value_accessors)*
            let result = this.#method_ident(#(#arg_names),*);
            #result_handling
        }
    };

    quote! {
        ({
            #required
            ::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;

                        ::ext_php_rs::zend::run_handler(::std::panic::AssertUnwindSafe(|| {
                            #handler_body
                        }));
                    }
                }
                handler
            })
            #(#arg_builders)*
            .required_args(__REQUIRED)
            #returns_call
        },
            #flags
        )
    }
}