Skip to content

Commit 240b4fb

Browse files
committed
feat(interface): php_impl_interface macro #590
1 parent 0819246 commit 240b4fb

File tree

26 files changed

+1356
-96
lines changed

26 files changed

+1356
-96
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ parking_lot = { version = "0.12", features = ["arc_lock"] }
2222
cfg-if = "1.0"
2323
once_cell = "1.21"
2424
anyhow = { version = "1", optional = true }
25+
inventory = "0.3"
2526
ext-php-rs-derive = { version = "=0.11.5", path = "./crates/macros" }
2627

2728
[dev-dependencies]

crates/macros/src/class.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,15 @@ fn generate_registered_class_impl(
281281
use ::ext_php_rs::internal::class::PhpClassImpl;
282282
::ext_php_rs::internal::class::PhpClassImplCollector::<Self>::default().get_constants()
283283
}
284+
285+
#[inline]
286+
fn interface_implementations() -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> {
287+
let my_type_id = ::std::any::TypeId::of::<Self>();
288+
::ext_php_rs::inventory::iter::<::ext_php_rs::internal::class::InterfaceRegistration>()
289+
.filter(|reg| reg.class_type_id == my_type_id)
290+
.map(|reg| (reg.interface_getter)())
291+
.collect()
292+
}
284293
}
285294
}
286295
}

crates/macros/src/function.rs

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -317,46 +317,59 @@ impl<'a> Function<'a> {
317317
}
318318
}
319319

320-
fn build_returns(&self, call_type: Option<&CallType>) -> Option<TokenStream> {
321-
self.output.cloned().map(|mut output| {
322-
output.drop_lifetimes();
323-
324-
// If returning &Self or &mut Self from a method, use the class type
325-
// for return type information since we return `this` (ZendClassObject)
326-
if returns_self_ref(self.output)
327-
&& let Some(CallType::Method { class, .. }) = call_type
320+
fn build_returns(&self, call_type: Option<&CallType>) -> TokenStream {
321+
let Some(output) = self.output.cloned() else {
322+
// PHP magic methods __destruct and __clone cannot have return types
323+
// (only applies to class methods, not standalone functions)
324+
if matches!(call_type, Some(CallType::Method { .. }))
325+
&& (self.name == "__destruct" || self.name == "__clone")
328326
{
329-
return quote! {
330-
.returns(
331-
<&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::TYPE,
332-
false,
333-
<&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::NULLABLE,
334-
)
335-
};
327+
return quote! {};
336328
}
329+
// No return type means void in PHP
330+
return quote! {
331+
.returns(::ext_php_rs::flags::DataType::Void, false, false)
332+
};
333+
};
337334

338-
// If returning Self (new instance) from a method, replace Self with
339-
// the actual class type since Self won't resolve in generated code
340-
if returns_self(self.output)
341-
&& let Some(CallType::Method { class, .. }) = call_type
342-
{
343-
return quote! {
344-
.returns(
345-
<#class as ::ext_php_rs::convert::IntoZval>::TYPE,
346-
false,
347-
<#class as ::ext_php_rs::convert::IntoZval>::NULLABLE,
348-
)
349-
};
350-
}
335+
let mut output = output;
336+
output.drop_lifetimes();
351337

352-
quote! {
338+
// If returning &Self or &mut Self from a method, use the class type
339+
// for return type information since we return `this` (ZendClassObject)
340+
if returns_self_ref(self.output)
341+
&& let Some(CallType::Method { class, .. }) = call_type
342+
{
343+
return quote! {
353344
.returns(
354-
<#output as ::ext_php_rs::convert::IntoZval>::TYPE,
345+
<&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::TYPE,
355346
false,
356-
<#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
347+
<&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::NULLABLE,
357348
)
358-
}
359-
})
349+
};
350+
}
351+
352+
// If returning Self (new instance) from a method, replace Self with
353+
// the actual class type since Self won't resolve in generated code
354+
if returns_self(self.output)
355+
&& let Some(CallType::Method { class, .. }) = call_type
356+
{
357+
return quote! {
358+
.returns(
359+
<#class as ::ext_php_rs::convert::IntoZval>::TYPE,
360+
false,
361+
<#class as ::ext_php_rs::convert::IntoZval>::NULLABLE,
362+
)
363+
};
364+
}
365+
366+
quote! {
367+
.returns(
368+
<#output as ::ext_php_rs::convert::IntoZval>::TYPE,
369+
false,
370+
<#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
371+
)
372+
}
360373
}
361374

362375
fn build_result(
@@ -999,7 +1012,8 @@ fn expr_to_php_stub(expr: &Expr) -> String {
9991012
}
10001013
}
10011014

1002-
/// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
1015+
/// Returns true if the given type is nullable in PHP (i.e., it's an
1016+
/// `Option<T>`).
10031017
///
10041018
/// Note: Having a default value does NOT make a type nullable. A parameter with
10051019
/// a default value is optional (can be omitted), but passing `null` explicitly
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//! Implementation for the `#[php_impl_interface]` macro.
2+
//!
3+
//! This macro allows classes to implement PHP interfaces by implementing Rust
4+
//! traits that are marked with `#[php_interface]`.
5+
//!
6+
//! Uses the `inventory` crate for cross-crate interface discovery.
7+
8+
use proc_macro2::TokenStream;
9+
use quote::{format_ident, quote};
10+
use syn::ItemImpl;
11+
12+
use crate::prelude::*;
13+
14+
const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface";
15+
16+
/// Parses a trait impl block and generates the interface implementation
17+
/// registration.
18+
///
19+
/// # Arguments
20+
///
21+
/// * `input` - The trait impl block (e.g., `impl SomeTrait for SomeStruct { ...
22+
/// }`)
23+
///
24+
/// # Generated Code
25+
///
26+
/// The macro generates:
27+
/// 1. The original trait impl block (passed through unchanged)
28+
/// 2. An `inventory::submit!` call to register the interface implementation
29+
pub fn parser(input: &ItemImpl) -> Result<TokenStream> {
30+
// Extract the trait being implemented
31+
let Some((_, trait_path, _)) = &input.trait_ else {
32+
bail!(input => "`#[php_impl_interface]` can only be used on trait implementations (e.g., `impl SomeTrait for SomeStruct`)");
33+
};
34+
35+
// Get the last segment of the trait path (the trait name)
36+
let trait_ident = match trait_path.segments.last() {
37+
Some(segment) => &segment.ident,
38+
None => {
39+
bail!(trait_path => "Invalid trait path");
40+
}
41+
};
42+
43+
// Get the struct type being implemented
44+
let struct_ty = &input.self_ty;
45+
46+
// Generate the internal interface struct name (e.g., PhpInterfaceSomeTrait)
47+
let interface_struct_name = format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, trait_ident);
48+
49+
Ok(quote! {
50+
// Pass through the original trait implementation
51+
#input
52+
53+
// Register the interface implementation using inventory for cross-crate discovery
54+
::ext_php_rs::inventory::submit! {
55+
::ext_php_rs::internal::class::InterfaceRegistration {
56+
class_type_id: ::std::any::TypeId::of::<#struct_ty>(),
57+
interface_getter: || (
58+
|| <#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
59+
<#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
60+
),
61+
}
62+
}
63+
})
64+
}

crates/macros/src/interface.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use darling::FromAttributes;
88
use darling::util::Flag;
99
use proc_macro2::TokenStream;
1010
use quote::{ToTokens, format_ident, quote};
11-
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn};
11+
use syn::{Expr, Ident, ItemTrait, Path, TraitItem, TraitItemConst, TraitItemFn, TypeParamBound};
1212

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

50+
/// Represents a supertrait that should be converted to an interface extension.
51+
/// These are automatically detected from Rust trait bounds (e.g., `trait Foo:
52+
/// Bar`).
53+
struct SupertraitInterface {
54+
/// The name of the supertrait's PHP interface struct (e.g.,
55+
/// `PhpInterfaceBar`)
56+
interface_struct_name: Ident,
57+
}
58+
59+
impl ToTokens for SupertraitInterface {
60+
fn to_tokens(&self, tokens: &mut TokenStream) {
61+
let interface_struct_name = &self.interface_struct_name;
62+
quote! {
63+
(
64+
|| <#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(),
65+
<#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME
66+
)
67+
}
68+
.to_tokens(tokens);
69+
}
70+
}
71+
5072
struct InterfaceData<'a> {
5173
ident: &'a Ident,
5274
name: String,
5375
path: Path,
76+
/// Extends from `#[php(extends(...))]` attributes
5477
extends: Vec<ClassEntryAttribute>,
78+
/// Extends from Rust trait bounds (supertraits)
79+
supertrait_extends: Vec<SupertraitInterface>,
5580
constructor: Option<Function<'a>>,
5681
methods: Vec<FnBuilder>,
5782
constants: Vec<Constant<'a>>,
@@ -64,6 +89,7 @@ impl ToTokens for InterfaceData<'_> {
6489
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{}", self.ident);
6590
let name = &self.name;
6691
let implements = &self.extends;
92+
let supertrait_implements = &self.supertrait_extends;
6793
let methods_sig = &self.methods;
6894
let constants = &self.constants;
6995
let docs = &self.docs;
@@ -88,8 +114,10 @@ impl ToTokens for InterfaceData<'_> {
88114

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

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

95123
const DOC_COMMENTS: &'static [&'static str] = &[
@@ -207,11 +235,16 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
207235
let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{ident}");
208236
let ts = quote! { #interface_name };
209237
let path: Path = syn::parse2(ts)?;
238+
239+
// Parse supertraits to automatically generate interface inheritance
240+
let supertrait_extends = parse_supertraits(&self.supertraits);
241+
210242
let mut data = InterfaceData {
211243
ident,
212244
name,
213245
path,
214246
extends: attrs.extends,
247+
supertrait_extends,
215248
constructor: None,
216249
methods: Vec::default(),
217250
constants: Vec::default(),
@@ -239,6 +272,31 @@ impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait {
239272
}
240273
}
241274

275+
/// Parses the supertraits of a trait definition and converts them to interface
276+
/// extensions. For a trait like `trait Foo: Bar + Baz`, this will generate
277+
/// references to `PhpInterfaceBar` and `PhpInterfaceBaz`.
278+
fn parse_supertraits(
279+
supertraits: &syn::punctuated::Punctuated<TypeParamBound, syn::token::Plus>,
280+
) -> Vec<SupertraitInterface> {
281+
supertraits
282+
.iter()
283+
.filter_map(|bound| {
284+
if let TypeParamBound::Trait(trait_bound) = bound {
285+
// Get the last segment of the trait path (the trait name)
286+
let trait_name = trait_bound.path.segments.last()?;
287+
// Generate the PHP interface struct name
288+
let interface_struct_name =
289+
format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, trait_name.ident);
290+
Some(SupertraitInterface {
291+
interface_struct_name,
292+
})
293+
} else {
294+
None
295+
}
296+
})
297+
.collect()
298+
}
299+
242300
#[derive(FromAttributes, Default, Debug)]
243301
#[darling(default, attributes(php), forward_attrs(doc))]
244302
pub struct PhpFunctionInterfaceAttribute {

0 commit comments

Comments
 (0)