Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ parking_lot = { version = "0.12", features = ["arc_lock"] }
cfg-if = "1.0"
once_cell = "1.21"
anyhow = { version = "1", optional = true }
inventory = "0.3"
ext-php-rs-derive = { version = "=0.11.5", path = "./crates/macros" }

[dev-dependencies]
Expand Down
9 changes: 9 additions & 0 deletions crates/macros/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,15 @@ fn generate_registered_class_impl(
use ::ext_php_rs::internal::class::PhpClassImpl;
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constants()
}

#[inline]
fn interface_implementations() -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> {
let my_type_id = ::std::any::TypeId::of::<Self>();
::ext_php_rs::inventory::iter::<::ext_php_rs::internal::class::InterfaceRegistration>()
.filter(|reg| reg.class_type_id == my_type_id)
.map(|reg| (reg.interface_getter)())
.collect()
}
}
}
}
3 changes: 2 additions & 1 deletion crates/macros/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,8 @@ fn expr_to_php_stub(expr: &Expr) -> String {
}
}

/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
/// Returns true if the given type is nullable in PHP (i.e., it's an
/// `Option<T>`).
///
/// Note: Having a default value does NOT make a type nullable. A parameter with
/// a default value is optional (can be omitted), but passing `null` explicitly
Expand Down
64 changes: 64 additions & 0 deletions crates/macros/src/impl_interface.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//! 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.

use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use syn::ItemImpl;

use crate::prelude::*;

const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface";

/// Parses a trait impl block and generates the interface implementation
/// registration.
///
/// # Arguments
///
/// * `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
pub fn parser(input: &ItemImpl) -> Result<TokenStream> {
// 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`)");
};

// Get the last segment of the trait path (the trait name)
let trait_ident = match trait_path.segments.last() {
Some(segment) => &segment.ident,
None => {
bail!(trait_path => "Invalid trait path");
}
};

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

// Generate the internal interface struct name (e.g., PhpInterfaceSomeTrait)
let interface_struct_name = format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, trait_ident);

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_name as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
<#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
),
}
}
})
}
60 changes: 59 additions & 1 deletion crates/macros/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use darling::FromAttributes;
use darling::util::Flag;
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn};
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn, TypeParamBound};

use crate::impl_::{FnBuilder, MethodModifier};
use crate::parsing::{
Expand Down Expand Up @@ -47,11 +47,36 @@ trait Parse<'a, T> {
fn parse(&'a mut self) -> Result<T>;
}

/// Represents a supertrait that should be converted to an interface extension.
/// These are automatically detected from Rust trait bounds (e.g., `trait Foo:
/// Bar`).
struct SupertraitInterface {
/// The name of the supertrait's PHP interface struct (e.g.,
/// `PhpInterfaceBar`)
interface_struct_name: Ident,
}

impl ToTokens for SupertraitInterface {
fn to_tokens(&self, tokens: &mut TokenStream) {
let interface_struct_name = &self.interface_struct_name;
quote! {
(
|| <#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
<#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
)
}
.to_tokens(tokens);
}
}

struct InterfaceData<'a> {
ident: &'a Ident,
name: String,
path: Path,
/// Extends from `#[php(extends(...))]` attributes
extends: Vec<ClassEntryAttribute>,
/// Extends from Rust trait bounds (supertraits)
supertrait_extends: Vec<SupertraitInterface>,
constructor: Option<Function<'a>>,
methods: Vec<FnBuilder>,
constants: Vec<Constant<'a>>,
Expand All @@ -64,6 +89,7 @@ impl ToTokens for InterfaceData<'_> {
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{}", self.ident);
let name = &self.name;
let implements = &self.extends;
let supertrait_implements = &self.supertrait_extends;
let methods_sig = &self.methods;
let constants = &self.constants;
let docs = &self.docs;
Expand All @@ -88,8 +114,10 @@ impl ToTokens for InterfaceData<'_> {

const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface;

// Interface inheritance from both explicit #[php(extends(...))] and Rust trait bounds
const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[
#(#implements,)*
#(#supertrait_implements,)*
];

const DOC_COMMENTS: &'static [&'static str] = &[
Expand Down Expand Up @@ -207,11 +235,16 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{ident}");
let ts = quote! { #interface_name };
let path: Path = syn::parse2(ts)?;

// Parse supertraits to automatically generate interface inheritance
let supertrait_extends = parse_supertraits(&self.supertraits);

let mut data = InterfaceData {
ident,
name,
path,
extends: attrs.extends,
supertrait_extends,
constructor: None,
methods: Vec::default(),
constants: Vec::default(),
Expand Down Expand Up @@ -239,6 +272,31 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
}
}

/// Parses the supertraits of a trait definition and converts them to interface
/// extensions. For a trait like `trait Foo: Bar + Baz`, this will generate
/// references to `PhpInterfaceBar` and `PhpInterfaceBaz`.
fn parse_supertraits(
supertraits: &syn::punctuated::Punctuated<TypeParamBound, syn::token::Plus>,
) -> Vec<SupertraitInterface> {
supertraits
.iter()
.filter_map(|bound| {
if let TypeParamBound::Trait(trait_bound) = bound {
// Get the last segment of the trait path (the trait name)
let trait_name = trait_bound.path.segments.last()?;
// Generate the PHP interface struct name
let interface_struct_name =
format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, trait_name.ident);
Some(SupertraitInterface {
interface_struct_name,
})
} else {
None
}
})
.collect()
}

#[derive(FromAttributes, Default, Debug)]
#[darling(default, attributes(php), forward_attrs(doc))]
pub struct PhpFunctionInterfaceAttribute {
Expand Down
Loading
Loading