From 7febcb0a37789069079d402100bbae04fe33c84b Mon Sep 17 00:00:00 2001 From: Vasily Zorin Date: Mon, 22 Dec 2025 03:22:21 +0700 Subject: [PATCH] feat(interface): php_impl_interface macro #590 --- build.rs | 21 +- crates/macros/src/class.rs | 6 + crates/macros/src/impl_interface.rs | 65 +++ crates/macros/src/interface.rs | 60 ++- crates/macros/src/lib.rs | 385 ++++++++++++++++++ crates/macros/tests/expand/class.expanded.rs | 6 + guide/src/macros/classes.md | 163 ++++++++ guide/src/macros/interface.md | 148 +++++++ src/builders/class.rs | 26 +- src/builders/ini.rs | 3 +- src/builders/module.rs | 17 +- src/builders/sapi.rs | 28 +- src/class.rs | 13 + src/closure.rs | 3 +- src/embed/mod.rs | 5 +- src/enum_.rs | 9 +- src/internal/class.rs | 31 +- src/lib.rs | 8 +- src/types/array/conversions/mod.rs | 6 +- src/types/zval.rs | 4 +- tests/src/integration/interface/interface.php | 102 ++++- tests/src/integration/interface/mod.rs | 220 +++++++++- tests/src/integration/mod.rs | 3 +- 23 files changed, 1268 insertions(+), 64 deletions(-) create mode 100644 crates/macros/src/impl_interface.rs diff --git a/build.rs b/build.rs index c740b3c4ed..ccfd2deb8f 100644 --- a/build.rs +++ b/build.rs @@ -231,19 +231,16 @@ fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result::default().get_constants() } + + #[inline] + fn interface_implementations() -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> { + // TODO: Implement cross-crate interface discovery + ::std::vec::Vec::new() + } } } } diff --git a/crates/macros/src/impl_interface.rs b/crates/macros/src/impl_interface.rs new file mode 100644 index 0000000000..720fa97f93 --- /dev/null +++ b/crates/macros/src/impl_interface.rs @@ -0,0 +1,65 @@ +//! 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]`. + +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 implementation of `PhpInterfaceImpl` that registers the interface +pub fn parser(input: &ItemImpl) -> Result { + // 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 + + // Implement PhpInterfaceImpl for the struct to register the interface + impl ::ext_php_rs::internal::class::PhpInterfaceImpl<#struct_ty> + for ::ext_php_rs::internal::class::PhpInterfaceImplCollector<#struct_ty> + { + fn get_interfaces(self) -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> { + vec![ + ( + || <#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(), + <#interface_struct_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME + ) + ] + } + } + }) +} diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 6e9a54a4f9..596002cf52 100644 --- a/crates/macros/src/interface.rs +++ b/crates/macros/src/interface.rs @@ -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::{PhpRename, RenameRule, Visibility}; @@ -45,11 +45,36 @@ trait Parse<'a, T> { fn parse(&'a mut self) -> Result; } +/// 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, + /// Extends from Rust trait bounds (supertraits) + supertrait_extends: Vec, constructor: Option>, methods: Vec, constants: Vec>, @@ -62,6 +87,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; @@ -86,8 +112,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] = &[ @@ -202,11 +230,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(), @@ -234,6 +267,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, +) -> Vec { + 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 { diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 87cde609f2..edb51da2b5 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -8,6 +8,7 @@ mod fastcall; mod function; mod helpers; mod impl_; +mod impl_interface; mod interface; mod module; mod parsing; @@ -274,6 +275,172 @@ extern crate proc_macro; /// echo Counter::$count; // 2 /// echo Counter::getCount(); // 2 /// ``` +/// +/// ## Implementing Iterator +/// +/// To make a Rust class usable with PHP's `foreach` loop, implement the +/// [`Iterator`](https://www.php.net/manual/en/class.iterator.php) interface. +/// This requires implementing five methods: `current()`, `key()`, `next()`, +/// `rewind()`, and `valid()`. +/// +/// The following example creates a `RangeIterator` that iterates over a range +/// of integers: +/// +/// ````rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{prelude::*, zend::ce}; +/// +/// #[php_class] +/// #[php(implements(ce = ce::iterator, stub = "\\Iterator"))] +/// pub struct RangeIterator { +/// start: i64, +/// end: i64, +/// current: i64, +/// index: i64, +/// } +/// +/// #[php_impl] +/// impl RangeIterator { +/// /// Create a new range iterator from start to end (inclusive). +/// pub fn __construct(start: i64, end: i64) -> Self { +/// Self { +/// start, +/// end, +/// current: start, +/// index: 0, +/// } +/// } +/// +/// /// Return the current element. +/// pub fn current(&self) -> i64 { +/// self.current +/// } +/// +/// /// Return the key of the current element. +/// pub fn key(&self) -> i64 { +/// self.index +/// } +/// +/// /// Move forward to next element. +/// pub fn next(&mut self) { +/// self.current += 1; +/// self.index += 1; +/// } +/// +/// /// Rewind the Iterator to the first element. +/// pub fn rewind(&mut self) { +/// self.current = self.start; +/// self.index = 0; +/// } +/// +/// /// Checks if current position is valid. +/// pub fn valid(&self) -> bool { +/// self.current <= self.end +/// } +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.class::() +/// } +/// # fn main() {} +/// ```` +/// +/// Using the iterator in PHP: +/// +/// ```php +/// $value) { +/// echo "$key => $value\n"; +/// } +/// // Output: +/// // 0 => 1 +/// // 1 => 2 +/// // 2 => 3 +/// // 3 => 4 +/// // 4 => 5 +/// +/// // Works with iterator functions +/// $arr = iterator_to_array(new RangeIterator(10, 12)); +/// // [0 => 10, 1 => 11, 2 => 12] +/// +/// $count = iterator_count(new RangeIterator(1, 100)); +/// // 100 +/// ``` +/// +/// ### Iterator with Mixed Types +/// +/// You can return different types for keys and values. The following example +/// uses string keys: +/// +/// ````rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{prelude::*, zend::ce}; +/// +/// #[php_class] +/// #[php(implements(ce = ce::iterator, stub = "\\Iterator"))] +/// pub struct MapIterator { +/// keys: Vec, +/// values: Vec, +/// index: usize, +/// } +/// +/// #[php_impl] +/// impl MapIterator { +/// pub fn __construct() -> Self { +/// Self { +/// keys: vec!["first".into(), "second".into(), "third".into()], +/// values: vec!["one".into(), "two".into(), "three".into()], +/// index: 0, +/// } +/// } +/// +/// pub fn current(&self) -> Option { +/// self.values.get(self.index).cloned() +/// } +/// +/// pub fn key(&self) -> Option { +/// self.keys.get(self.index).cloned() +/// } +/// +/// pub fn next(&mut self) { +/// self.index += 1; +/// } +/// +/// pub fn rewind(&mut self) { +/// self.index = 0; +/// } +/// +/// pub fn valid(&self) -> bool { +/// self.index < self.keys.len() +/// } +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module.class::() +/// } +/// # fn main() {} +/// ```` +/// +/// ```php +/// $value) { +/// echo "$key => $value\n"; +/// } +/// // Output: +/// // first => one +/// // second => two +/// // third => three +/// ``` // END DOCS FROM classes.md #[proc_macro_attribute] pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { @@ -489,6 +656,159 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// } /// } /// ``` +/// +/// ## Interface Inheritance +/// +/// PHP interfaces can extend other interfaces. You can achieve this in two +/// ways: +/// +/// ### Using `#[php(extends(...))]` +/// +/// Use the `extends` attribute to extend a built-in PHP interface or another +/// interface: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// use ext_php_rs::zend::ce; +/// +/// #[php_interface] +/// #[php(extends(ce = ce::throwable, stub = "\\Throwable"))] +/// #[php(name = "MyException")] +/// trait MyExceptionInterface { +/// fn get_error_code(&self) -> i32; +/// } +/// +/// # fn main() {} +/// ``` +/// +/// ### Using Rust Trait Bounds +/// +/// You can also use Rust's trait bound syntax. When a trait marked with +/// `#[php_interface]` has supertraits, the PHP interface will automatically +/// extend those parent interfaces: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_interface] +/// #[php(name = "Rust\\ParentInterface")] +/// trait ParentInterface { +/// fn parent_method(&self) -> String; +/// } +/// +/// // ChildInterface extends ParentInterface in PHP +/// #[php_interface] +/// #[php(name = "Rust\\ChildInterface")] +/// trait ChildInterface: ParentInterface { +/// fn child_method(&self) -> String; +/// } +/// +/// #[php_module] +/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .interface::() +/// .interface::() +/// } +/// +/// # fn main() {} +/// ``` +/// +/// In PHP: +/// +/// ```php +/// String; +/// } +/// +/// // Define a class +/// #[php_class] +/// #[php(name = "Rust\\Greeter")] +/// pub struct Greeter { +/// name: String, +/// } +/// +/// #[php_impl] +/// impl Greeter { +/// pub fn __construct(name: String) -> Self { +/// Self { name } +/// } +/// } +/// +/// // Implement the interface for the class +/// #[php_impl_interface] +/// impl Greetable for Greeter { +/// fn greet(&self) -> String { +/// format!("Hello, {}!", self.name) +/// } +/// } +/// +/// #[php_module] +/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .interface::() +/// .class::() +/// } +/// +/// # fn main() {} +/// ``` +/// +/// Using in PHP: +/// +/// ```php +/// greet() method is available +/// } +/// +/// greet($greeter); +/// ``` +/// +/// ## When to Use +/// +/// - Use `#[php_impl_interface]` for custom interfaces you define with +/// `#[php_interface]` +/// - Use `#[php(implements(ce = ...))]` on `#[php_class]` for built-in PHP +/// interfaces like `Iterator`, `ArrayAccess`, `Countable`, etc. +/// +/// See the [Classes documentation](./classes.md#implementing-an-interface) for +/// examples of implementing built-in interfaces. // END DOCS FROM interface.md #[proc_macro_attribute] pub fn php_interface(args: TokenStream, input: TokenStream) -> TokenStream { @@ -1051,6 +1371,67 @@ fn php_impl_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { impl_::parser(input).unwrap_or_else(|e| e.to_compile_error()) } +/// # `#[php_impl_interface]` Attribute +/// +/// Marks a trait implementation as implementing a PHP interface. This allows +/// Rust structs marked with `#[php_class]` to implement Rust traits marked +/// with `#[php_interface]`, and have PHP recognize the relationship. +/// +/// ## Usage +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::prelude::*; +/// +/// #[php_interface] +/// trait MyInterface { +/// fn my_method(&self); +/// } +/// +/// #[php_class] +/// struct MyClass; +/// +/// #[php_impl_interface] +/// impl MyInterface for MyClass { +/// fn my_method(&self) { +/// // Implementation +/// } +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .interface::() +/// .class::() +/// } +/// # fn main() {} +/// ``` +/// +/// After registration, PHP's `is_a($obj, 'MyInterface')` will return `true` +/// for instances of `MyClass`. +/// +/// ## Requirements +/// +/// - The trait must be marked with `#[php_interface]` +/// - The struct must be marked with `#[php_class]` +/// - The interface must be registered before the class in the module builder +#[proc_macro_attribute] +pub fn php_impl_interface(args: TokenStream, input: TokenStream) -> TokenStream { + php_impl_interface_internal(args.into(), input.into()).into() +} + +#[allow(clippy::needless_pass_by_value)] +fn php_impl_interface_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { + let input = parse_macro_input2!(input as ItemImpl); + if !args.is_empty() { + return err!(input => "`#[php_impl_interface]` does not accept arguments.") + .to_compile_error(); + } + + impl_interface::parser(&input).unwrap_or_else(|e| e.to_compile_error()) +} + // BEGIN DOCS FROM extern.md /// # `#[php_extern]` Attribute /// @@ -1453,6 +1834,10 @@ mod tests { ("php_extern", php_extern_internal as AttributeFn), ("php_function", php_function_internal as AttributeFn), ("php_impl", php_impl_internal as AttributeFn), + ( + "php_impl_interface", + php_impl_interface_internal as AttributeFn, + ), ("php_module", php_module_internal as AttributeFn), ], ) diff --git a/crates/macros/tests/expand/class.expanded.rs b/crates/macros/tests/expand/class.expanded.rs index 88ac1df6ca..56fc5c5584 100644 --- a/crates/macros/tests/expand/class.expanded.rs +++ b/crates/macros/tests/expand/class.expanded.rs @@ -73,4 +73,10 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass { ::ext_php_rs::internal::class::PhpClassImplCollector::::default() .get_constants() } + #[inline] + fn interface_implementations() -> ::std::vec::Vec< + ::ext_php_rs::class::ClassEntryInfo, + > { + ::std::vec::Vec::new() + } } diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 22b3064961..e342afc007 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -243,3 +243,166 @@ Counter::increment(); echo Counter::$count; // 2 echo Counter::getCount(); // 2 ``` + +## Implementing Iterator + +To make a Rust class usable with PHP's `foreach` loop, implement the +[`Iterator`](https://www.php.net/manual/en/class.iterator.php) interface. +This requires implementing five methods: `current()`, `key()`, `next()`, `rewind()`, and `valid()`. + +The following example creates a `RangeIterator` that iterates over a range of integers: + +````rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::{prelude::*, zend::ce}; + +#[php_class] +#[php(implements(ce = ce::iterator, stub = "\\Iterator"))] +pub struct RangeIterator { + start: i64, + end: i64, + current: i64, + index: i64, +} + +#[php_impl] +impl RangeIterator { + /// Create a new range iterator from start to end (inclusive). + pub fn __construct(start: i64, end: i64) -> Self { + Self { + start, + end, + current: start, + index: 0, + } + } + + /// Return the current element. + pub fn current(&self) -> i64 { + self.current + } + + /// Return the key of the current element. + pub fn key(&self) -> i64 { + self.index + } + + /// Move forward to next element. + pub fn next(&mut self) { + self.current += 1; + self.index += 1; + } + + /// Rewind the Iterator to the first element. + pub fn rewind(&mut self) { + self.current = self.start; + self.index = 0; + } + + /// Checks if current position is valid. + pub fn valid(&self) -> bool { + self.current <= self.end + } +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.class::() +} +# fn main() {} +```` + +Using the iterator in PHP: + +```php + $value) { + echo "$key => $value\n"; +} +// Output: +// 0 => 1 +// 1 => 2 +// 2 => 3 +// 3 => 4 +// 4 => 5 + +// Works with iterator functions +$arr = iterator_to_array(new RangeIterator(10, 12)); +// [0 => 10, 1 => 11, 2 => 12] + +$count = iterator_count(new RangeIterator(1, 100)); +// 100 +``` + +### Iterator with Mixed Types + +You can return different types for keys and values. The following example uses string keys: + +````rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::{prelude::*, zend::ce}; + +#[php_class] +#[php(implements(ce = ce::iterator, stub = "\\Iterator"))] +pub struct MapIterator { + keys: Vec, + values: Vec, + index: usize, +} + +#[php_impl] +impl MapIterator { + pub fn __construct() -> Self { + Self { + keys: vec!["first".into(), "second".into(), "third".into()], + values: vec!["one".into(), "two".into(), "three".into()], + index: 0, + } + } + + pub fn current(&self) -> Option { + self.values.get(self.index).cloned() + } + + pub fn key(&self) -> Option { + self.keys.get(self.index).cloned() + } + + pub fn next(&mut self) { + self.index += 1; + } + + pub fn rewind(&mut self) { + self.index = 0; + } + + pub fn valid(&self) -> bool { + self.index < self.keys.len() + } +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.class::() +} +# fn main() {} +```` + +```php + $value) { + echo "$key => $value\n"; +} +// Output: +// first => one +// second => two +// third => three +``` diff --git a/guide/src/macros/interface.md b/guide/src/macros/interface.md index 6ca88898f2..6537a677d1 100644 --- a/guide/src/macros/interface.md +++ b/guide/src/macros/interface.md @@ -71,3 +71,151 @@ class B implements Rust\TestInterface { } ``` + +## Interface Inheritance + +PHP interfaces can extend other interfaces. You can achieve this in two ways: + +### Using `#[php(extends(...))]` + +Use the `extends` attribute to extend a built-in PHP interface or another interface: + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; +use ext_php_rs::zend::ce; + +#[php_interface] +#[php(extends(ce = ce::throwable, stub = "\\Throwable"))] +#[php(name = "MyException")] +trait MyExceptionInterface { + fn get_error_code(&self) -> i32; +} + +# fn main() {} +``` + +### Using Rust Trait Bounds + +You can also use Rust's trait bound syntax. When a trait marked with `#[php_interface]` +has supertraits, the PHP interface will automatically extend those parent interfaces: + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::prelude::*; + +#[php_interface] +#[php(name = "Rust\\ParentInterface")] +trait ParentInterface { + fn parent_method(&self) -> String; +} + +// ChildInterface extends ParentInterface in PHP +#[php_interface] +#[php(name = "Rust\\ChildInterface")] +trait ChildInterface: ParentInterface { + fn child_method(&self) -> String; +} + +#[php_module] +pub fn module(module: ModuleBuilder) -> ModuleBuilder { + module + .interface::() + .interface::() +} + +# fn main() {} +``` + +In PHP: + +```php + String; +} + +// Define a class +#[php_class] +#[php(name = "Rust\\Greeter")] +pub struct Greeter { + name: String, +} + +#[php_impl] +impl Greeter { + pub fn __construct(name: String) -> Self { + Self { name } + } +} + +// Implement the interface for the class +#[php_impl_interface] +impl Greetable for Greeter { + fn greet(&self) -> String { + format!("Hello, {}!", self.name) + } +} + +#[php_module] +pub fn module(module: ModuleBuilder) -> ModuleBuilder { + module + .interface::() + .class::() +} + +# fn main() {} +``` + +Using in PHP: + +```php +greet() method is available +} + +greet($greeter); +``` + +## When to Use + +- Use `#[php_impl_interface]` for custom interfaces you define with `#[php_interface]` +- Use `#[php(implements(ce = ...))]` on `#[php_class]` for built-in PHP interfaces + like `Iterator`, `ArrayAccess`, `Countable`, etc. + +See the [Classes documentation](./classes.md#implementing-an-interface) for examples +of implementing built-in interfaces. diff --git a/src/builders/class.rs b/src/builders/class.rs index 6656506418..64772b3339 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -241,7 +241,9 @@ impl ClassBuilder { self.object_override = Some(create_object::); let is_interface = T::FLAGS.contains(ClassFlags::Interface); - let (func, visibility) = if let Some(ConstructorMeta { + // For interfaces: only add __construct if explicitly declared + // For classes: always add __construct (PHP needs it for object creation) + if let Some(ConstructorMeta { build_fn, flags, .. }) = T::constructor() { @@ -250,20 +252,16 @@ impl ClassBuilder { } else { FunctionBuilder::new("__construct", constructor::) }; - - (build_fn(func), flags.unwrap_or(MethodFlags::Public)) + let visibility = flags.unwrap_or(MethodFlags::Public); + self.method(build_fn(func), visibility) + } else if is_interface { + // Don't add default constructor for interfaces + self } else { - ( - if is_interface { - FunctionBuilder::new_abstract("__construct") - } else { - FunctionBuilder::new("__construct", constructor::) - }, - MethodFlags::Public, - ) - }; - - self.method(func, visibility) + // Add default constructor for classes + let func = FunctionBuilder::new("__construct", constructor::); + self.method(func, MethodFlags::Public) + } } /// Function to register the class with PHP. This function is called after diff --git a/src/builders/ini.rs b/src/builders/ini.rs index ca71a53b54..f42aabde90 100644 --- a/src/builders/ini.rs +++ b/src/builders/ini.rs @@ -218,7 +218,8 @@ impl AsRef for IniBuilder { } } -// Ensure the C buffer is properly deinitialized when the builder goes out of scope. +// Ensure the C buffer is properly deinitialized when the builder goes out of +// scope. impl Drop for IniBuilder { fn drop(&mut self) { if !self.value.is_null() { diff --git a/src/builders/module.rs b/src/builders/module.rs index 88b69b796e..d31e626dcd 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -243,9 +243,16 @@ impl ModuleBuilder<'_> { if let Some(parent) = T::EXTENDS { builder = builder.extends(parent); } + // Interfaces declared via #[php(implements(...))] attribute for interface in T::IMPLEMENTS { builder = builder.implements(*interface); } + // Interfaces from #[php_impl_interface] trait implementations. + // This method is generated by #[php_class] in the user's crate, + // so it can see the specialized PhpInterfaceImpl implementations. + for interface in T::interface_implementations() { + builder = builder.implements(interface); + } for (name, value, docs) in T::constants() { builder = builder .dyn_constant(*name, *value, docs) @@ -327,14 +334,16 @@ impl ModuleStartup { val.register_constant(&name, mod_num)?; } - self.classes.into_iter().map(|c| c()).for_each(|c| { - c.register().expect("Failed to build class"); - }); - + // Interfaces must be registered before classes so that classes can implement + // them self.interfaces.into_iter().map(|c| c()).for_each(|c| { c.register().expect("Failed to build interface"); }); + self.classes.into_iter().map(|c| c()).for_each(|c| { + c.register().expect("Failed to build class"); + }); + #[cfg(feature = "enum")] self.enums .into_iter() diff --git a/src/builders/sapi.rs b/src/builders/sapi.rs index 1de957209c..a9fc8898f0 100644 --- a/src/builders/sapi.rs +++ b/src/builders/sapi.rs @@ -168,7 +168,8 @@ impl SapiBuilder { /// /// # Parameters /// - /// * `func` - The function to be called when PHP gets an environment variable. + /// * `func` - The function to be called when PHP gets an environment + /// variable. pub fn getenv_function(mut self, func: SapiGetEnvFunc) -> Self { self.module.getenv = Some(func); self @@ -196,7 +197,8 @@ impl SapiBuilder { /// Sets the send headers function for this SAPI /// - /// This function is called once when all headers are finalized and ready to send. + /// This function is called once when all headers are finalized and ready to + /// send. /// /// # Arguments /// @@ -230,7 +232,8 @@ impl SapiBuilder { /// /// # Parameters /// - /// * `func` - The function to be called when PHP registers server variables. + /// * `func` - The function to be called when PHP registers server + /// variables. pub fn register_server_variables_function( mut self, func: SapiRegisterServerVariablesFunc, @@ -291,8 +294,8 @@ impl SapiBuilder { /// Sets the pre-request init function for this SAPI /// - /// This function is called before request activation and before POST data is read. - /// It is typically used for .user.ini processing. + /// This function is called before request activation and before POST data + /// is read. It is typically used for .user.ini processing. /// /// # Parameters /// @@ -455,7 +458,8 @@ pub type SapiGetUidFunc = extern "C" fn(uid: *mut uid_t) -> c_int; /// A function to be called when PHP gets the gid pub type SapiGetGidFunc = extern "C" fn(gid: *mut gid_t) -> c_int; -/// A function to be called before request activation (used for .user.ini processing) +/// A function to be called before request activation (used for .user.ini +/// processing) #[cfg(php85)] pub type SapiPreRequestInitFunc = extern "C" fn() -> c_int; @@ -485,8 +489,9 @@ mod test { extern "C" fn test_getenv(_name: *const c_char, _name_length: usize) -> *mut c_char { ptr::null_mut() } - // Note: C-variadic functions are unstable in Rust, so we can't test this properly - // extern "C" fn test_sapi_error(_type: c_int, _error_msg: *const c_char, _args: ...) {} + // Note: C-variadic functions are unstable in Rust, so we can't test this + // properly extern "C" fn test_sapi_error(_type: c_int, _error_msg: *const + // c_char, _args: ...) {} extern "C" fn test_send_header(_header: *mut sapi_header_struct, _server_context: *mut c_void) { } extern "C" fn test_send_headers(_sapi_headers: *mut sapi_headers_struct) -> c_int { @@ -633,9 +638,10 @@ mod test { ); } - // Note: Cannot test sapi_error_function because C-variadic functions are unstable in Rust - // The sapi_error field accepts a function with variadic arguments which cannot be - // created in stable Rust. However, the builder method itself works correctly. + // Note: Cannot test sapi_error_function because C-variadic functions are + // unstable in Rust The sapi_error field accepts a function with variadic + // arguments which cannot be created in stable Rust. However, the builder + // method itself works correctly. #[test] fn test_send_header_function() { diff --git a/src/class.rs b/src/class.rs index 3301a821ec..4431889568 100644 --- a/src/class.rs +++ b/src/class.rs @@ -87,6 +87,19 @@ pub trait RegisteredClass: Sized + 'static { )] { &[] } + + /// Returns interfaces from `#[php_impl_interface]` trait implementations. + /// + /// This method is generated by the `#[php_class]` macro and compiled in the + /// user's crate context, allowing it to see specialized implementations of + /// `PhpInterfaceImpl` from `#[php_impl_interface]`. + /// + /// The default implementation returns an empty vector. The macro overrides + /// this to call `PhpInterfaceImplCollector::::default().get_interfaces()`. + #[must_use] + fn interface_implementations() -> Vec { + Vec::new() + } } /// Stores metadata about a classes Rust constructor, including the function diff --git a/src/closure.rs b/src/closure.rs index f184ff8f4d..f58c9a6a07 100644 --- a/src/closure.rs +++ b/src/closure.rs @@ -116,7 +116,8 @@ impl Closure { /// function. /// /// If the class has already been built, this function returns early without - /// doing anything. This allows for safe repeated calls in test environments. + /// doing anything. This allows for safe repeated calls in test + /// environments. /// /// # Panics /// diff --git a/src/embed/mod.rs b/src/embed/mod.rs index 97df7e0aa0..e0648cb3c9 100644 --- a/src/embed/mod.rs +++ b/src/embed/mod.rs @@ -289,8 +289,9 @@ mod tests { #[test] fn test_eval_bailout() { Embed::run(|| { - // TODO: For PHP 8.5, this needs to be replaced, as `E_USER_ERROR` is deprecated. - // Currently, this seems to still be the best way to trigger a bailout. + // TODO: For PHP 8.5, this needs to be replaced, as `E_USER_ERROR` is + // deprecated. Currently, this seems to still be the best way + // to trigger a bailout. let result = Embed::eval("trigger_error(\"Fatal error\", E_USER_ERROR);"); assert!(result.is_err()); diff --git a/src/enum_.rs b/src/enum_.rs index 5868a876d6..8bcbd6970c 100644 --- a/src/enum_.rs +++ b/src/enum_.rs @@ -1,4 +1,5 @@ -//! This module defines the `PhpEnum` trait and related types for Rust enums that are exported to PHP. +//! This module defines the `PhpEnum` trait and related types for Rust enums +//! that are exported to PHP. use std::ptr; use crate::{ @@ -19,7 +20,8 @@ pub trait RegisteredEnum { /// # Errors /// - /// - [`Error::InvalidProperty`] if the enum does not have a case with the given name, an error is returned. + /// - [`Error::InvalidProperty`] if the enum does not have a case with the + /// given name, an error is returned. fn from_name(name: &str) -> Result where Self: Sized; @@ -125,7 +127,8 @@ impl EnumCase { } } -/// Represents the discriminant of an enum case in PHP, which can be either an integer or a string. +/// Represents the discriminant of an enum case in PHP, which can be either an +/// integer or a string. #[derive(Debug, PartialEq, Eq)] pub enum Discriminant { /// An integer discriminant. diff --git a/src/internal/class.rs b/src/internal/class.rs index e126a56f8d..adcd945699 100644 --- a/src/internal/class.rs +++ b/src/internal/class.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, marker::PhantomData}; use crate::{ builders::FunctionBuilder, - class::{ConstructorMeta, RegisteredClass}, + class::{ClassEntryInfo, ConstructorMeta, RegisteredClass}, convert::{IntoZval, IntoZvalDyn}, describe::DocComments, flags::MethodFlags, @@ -62,3 +62,32 @@ impl IntoZval for PhpClassImplCollector { unreachable!(); } } + +/// Collector used to collect interface implementations for PHP classes. +/// This is separate from `PhpClassImplCollector` to allow multiple interface +/// implementations via `#[php_impl_interface]`. +pub struct PhpInterfaceImplCollector(PhantomData); + +impl Default for PhpInterfaceImplCollector { + #[inline] + fn default() -> Self { + Self(PhantomData) + } +} + +/// Trait for collecting interface implementations. +/// Implemented by `#[php_impl_interface]` macro. +pub trait PhpInterfaceImpl { + /// Returns the list of interfaces this class implements via trait + /// implementations. + fn get_interfaces(self) -> Vec; +} + +/// Default implementation for classes without interface implementations. +/// Uses dtolnay's autoref specialization pattern. +impl PhpInterfaceImpl for &'_ PhpInterfaceImplCollector { + #[inline] + fn get_interfaces(self) -> Vec { + Vec::new() + } +} diff --git a/src/lib.rs b/src/lib.rs index 3da2710b05..a018267b34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,8 +56,8 @@ pub mod prelude { pub use crate::php_println; pub use crate::types::ZendCallable; pub use crate::{ - ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface, - php_module, wrap_constant, wrap_function, zend_fastcall, + ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_impl_interface, + php_interface, php_module, wrap_constant, wrap_function, zend_fastcall, }; } @@ -73,6 +73,6 @@ pub const PHP_ZTS: bool = cfg!(php_zts); #[cfg(feature = "enum")] pub use ext_php_rs_derive::php_enum; pub use ext_php_rs_derive::{ - ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface, - php_module, wrap_constant, wrap_function, zend_fastcall, + ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_impl_interface, + php_interface, php_module, wrap_constant, wrap_function, zend_fastcall, }; diff --git a/src/types/array/conversions/mod.rs b/src/types/array/conversions/mod.rs index fbcc4c7f04..4d4cb40864 100644 --- a/src/types/array/conversions/mod.rs +++ b/src/types/array/conversions/mod.rs @@ -1,8 +1,8 @@ //! Collection type conversions for `ZendHashTable`. //! -//! This module provides conversions between Rust collection types and PHP arrays -//! (represented as `ZendHashTable`). Each collection type has its own module for -//! better organization and maintainability. +//! This module provides conversions between Rust collection types and PHP +//! arrays (represented as `ZendHashTable`). Each collection type has its own +//! module for better organization and maintainability. //! //! ## Supported Collections //! diff --git a/src/types/zval.rs b/src/types/zval.rs index ac7d734fb3..460254ebfd 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -497,8 +497,8 @@ impl Zval { self.get_type() == DataType::Ptr } - /// Returns true if the zval is a scalar value (integer, float, string, or bool), - /// false otherwise. + /// Returns true if the zval is a scalar value (integer, float, string, or + /// bool), false otherwise. /// /// This is equivalent to PHP's `is_scalar()` function. #[must_use] diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php index 1266bc4e9e..a25c6c0ead 100644 --- a/tests/src/integration/interface/interface.php +++ b/tests/src/integration/interface/interface.php @@ -2,8 +2,8 @@ declare(strict_types=1); +// Test existing functionality: Interface existence and explicit extends assert(interface_exists('ExtPhpRs\Interface\EmptyObjectInterface'), 'Interface not exist'); - assert(is_a('ExtPhpRs\Interface\EmptyObjectInterface', Throwable::class, true), 'Interface could extend Throwable'); @@ -36,3 +36,103 @@ public function setValue(?int $value = 0) { assert($f->refToLikeThisClass('TEST', $f) === 'TEST - TEST | TEST - TEST'); assert(ExtPhpRs\Interface\EmptyObjectInterface::STRING_CONST === 'STRING_CONST'); assert(ExtPhpRs\Interface\EmptyObjectInterface::USIZE_CONST === 200); + +// Test Feature 1: Interface inheritance via Rust trait bounds +assert(interface_exists('ExtPhpRs\Interface\ParentInterface'), 'ParentInterface should exist'); +assert(interface_exists('ExtPhpRs\Interface\ChildInterface'), 'ChildInterface should exist'); +assert( + is_a('ExtPhpRs\Interface\ChildInterface', 'ExtPhpRs\Interface\ParentInterface', true), + 'ChildInterface should extend ParentInterface via Rust trait bounds' +); + +// ============================================================================ +// Test Feature 2: Implementing PHP's built-in Iterator interface (issue #308) +// This demonstrates how Rust objects can be used with PHP's foreach loop +// ============================================================================ + +// Test RangeIterator - a simple numeric range iterator +assert(class_exists('ExtPhpRs\Interface\RangeIterator'), 'RangeIterator class should exist'); + +$range = new ExtPhpRs\Interface\RangeIterator(1, 5); +assert($range instanceof Iterator, 'RangeIterator should implement Iterator interface'); +assert($range instanceof Traversable, 'RangeIterator should implement Traversable interface'); + +// Test foreach functionality with RangeIterator +$collected = []; +foreach ($range as $key => $value) { + $collected[$key] = $value; +} +assert($collected === [0 => 1, 1 => 2, 2 => 3, 3 => 4, 4 => 5], 'RangeIterator should iterate correctly'); + +// Test that we can iterate multiple times (rewind works) +$sum = 0; +foreach ($range as $value) { + $sum += $value; +} +assert($sum === 15, 'RangeIterator should be rewindable and sum to 15'); + +// Test empty range +$emptyRange = new ExtPhpRs\Interface\RangeIterator(5, 1); +$emptyCollected = []; +foreach ($emptyRange as $value) { + $emptyCollected[] = $value; +} +assert($emptyCollected === [], 'Empty range should produce no iterations'); + +// Test single element range +$singleRange = new ExtPhpRs\Interface\RangeIterator(42, 42); +$singleCollected = []; +foreach ($singleRange as $key => $value) { + $singleCollected[$key] = $value; +} +assert($singleCollected === [0 => 42], 'Single element range should work'); + +// Test MapIterator - string keys and values +assert(class_exists('ExtPhpRs\Interface\MapIterator'), 'MapIterator class should exist'); + +$map = new ExtPhpRs\Interface\MapIterator(); +assert($map instanceof Iterator, 'MapIterator should implement Iterator interface'); + +$mapCollected = []; +foreach ($map as $key => $value) { + $mapCollected[$key] = $value; +} +assert($mapCollected === ['first' => 'one', 'second' => 'two', 'third' => 'three'], + 'MapIterator should iterate with string keys and values'); + +// Test VecIterator - dynamic content iterator +assert(class_exists('ExtPhpRs\Interface\VecIterator'), 'VecIterator class should exist'); + +$vec = new ExtPhpRs\Interface\VecIterator(); +assert($vec instanceof Iterator, 'VecIterator should implement Iterator interface'); + +// Test empty iterator +$emptyVecCollected = []; +foreach ($vec as $value) { + $emptyVecCollected[] = $value; +} +assert($emptyVecCollected === [], 'Empty VecIterator should produce no iterations'); + +// Add items and iterate (VecIterator stores i64 values) +$vec->push(100); +$vec->push(200); +$vec->push(300); + +$vecCollected = []; +foreach ($vec as $key => $value) { + $vecCollected[$key] = $value; +} +assert(count($vecCollected) === 3, 'VecIterator should have 3 items'); +assert($vecCollected[0] === 100, 'First item should be 100'); +assert($vecCollected[1] === 200, 'Second item should be 200'); +assert($vecCollected[2] === 300, 'Third item should be 300'); + +// Test iterator_to_array() function works +$range2 = new ExtPhpRs\Interface\RangeIterator(10, 12); +$arr = iterator_to_array($range2); +assert($arr === [0 => 10, 1 => 11, 2 => 12], 'iterator_to_array should work with RangeIterator'); + +// Test iterator_count() function works +$range3 = new ExtPhpRs\Interface\RangeIterator(1, 100); +$count = iterator_count($range3); +assert($count === 100, 'iterator_count should return 100 for range 1-100'); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 620e5ac4a9..93bdf9b271 100644 --- a/tests/src/integration/interface/mod.rs +++ b/tests/src/integration/interface/mod.rs @@ -1,5 +1,4 @@ -use ext_php_rs::php_interface; -use ext_php_rs::prelude::ModuleBuilder; +use ext_php_rs::prelude::*; use ext_php_rs::types::ZendClassObject; use ext_php_rs::zend::ce; @@ -26,8 +25,223 @@ pub trait EmptyObjectTrait { fn set_value(&mut self, value: i32); } +// ============================================================================ +// Test Feature 3: Implementing PHP's built-in Iterator interface +// This addresses GitHub issue #308 - Iterator from Rust +// ============================================================================ + +/// A simple range iterator that demonstrates implementing PHP's Iterator +/// interface. This allows the class to be used with PHP's foreach loop. +/// +/// Usage in PHP: +/// ```php +/// $range = new RangeIterator(1, 5); +/// foreach ($range as $key => $value) { +/// echo "$key => $value\n"; +/// } +/// // Output: +/// // 0 => 1 +/// // 1 => 2 +/// // 2 => 3 +/// // 3 => 4 +/// // 4 => 5 +/// ``` +#[php_class] +#[php(name = "ExtPhpRs\\Interface\\RangeIterator")] +#[php(implements(ce = ce::iterator, stub = "\\Iterator"))] +pub struct RangeIterator { + start: i64, + end: i64, + current: i64, + index: i64, +} + +#[php_impl] +impl RangeIterator { + /// Create a new range iterator from start to end (inclusive). + pub fn __construct(start: i64, end: i64) -> Self { + Self { + start, + end, + current: start, + index: 0, + } + } + + /// Return the current element. + /// PHP Iterator interface method. + pub fn current(&self) -> i64 { + self.current + } + + /// Return the key of the current element. + /// PHP Iterator interface method. + pub fn key(&self) -> i64 { + self.index + } + + /// Move forward to next element. + /// PHP Iterator interface method. + pub fn next(&mut self) { + self.current += 1; + self.index += 1; + } + + /// Rewind the Iterator to the first element. + /// PHP Iterator interface method. + pub fn rewind(&mut self) { + self.current = self.start; + self.index = 0; + } + + /// Checks if current position is valid. + /// PHP Iterator interface method. + pub fn valid(&self) -> bool { + self.current <= self.end + } +} + +/// An iterator over string key-value pairs to demonstrate mixed types. +#[php_class] +#[php(name = "ExtPhpRs\\Interface\\MapIterator")] +#[php(implements(ce = ce::iterator, stub = "\\Iterator"))] +pub struct MapIterator { + keys: Vec, + values: Vec, + index: usize, +} + +#[php_impl] +impl MapIterator { + /// Create a new map iterator with predefined data. + pub fn __construct() -> Self { + Self { + keys: vec![ + "first".to_string(), + "second".to_string(), + "third".to_string(), + ], + values: vec!["one".to_string(), "two".to_string(), "three".to_string()], + index: 0, + } + } + + /// Return the current element. + pub fn current(&self) -> Option { + self.values.get(self.index).cloned() + } + + /// Return the key of the current element. + pub fn key(&self) -> Option { + self.keys.get(self.index).cloned() + } + + /// Move forward to next element. + pub fn next(&mut self) { + self.index += 1; + } + + /// Rewind the Iterator to the first element. + pub fn rewind(&mut self) { + self.index = 0; + } + + /// Checks if current position is valid. + pub fn valid(&self) -> bool { + self.index < self.keys.len() + } +} + +/// An iterator that wraps a Rust Vec and exposes it to PHP. +#[php_class] +#[php(name = "ExtPhpRs\\Interface\\VecIterator")] +#[php(implements(ce = ce::iterator, stub = "\\Iterator"))] +pub struct VecIterator { + items: Vec, + index: usize, +} + +#[php_impl] +impl VecIterator { + /// Create a new empty vec iterator. + pub fn __construct() -> Self { + Self { + items: Vec::new(), + index: 0, + } + } + + /// Add an item to the iterator. + pub fn push(&mut self, item: i64) { + self.items.push(item); + } + + /// Return the current element. + pub fn current(&self) -> Option { + self.items.get(self.index).copied() + } + + /// Return the key of the current element. + pub fn key(&self) -> usize { + self.index + } + + /// Move forward to next element. + pub fn next(&mut self) { + self.index += 1; + } + + /// Rewind the Iterator to the first element. + pub fn rewind(&mut self) { + self.index = 0; + } + + /// Checks if current position is valid. + pub fn valid(&self) -> bool { + self.index < self.items.len() + } + + /// Get the number of items. + pub fn count(&self) -> usize { + self.items.len() + } +} + +// Note: The Greeter/Greetable test has been removed because +// #[php_impl_interface] doesn't work cross-crate due to Rust's +// trait specialization limitations. The #[php(implements(...))] +// approach also has issues with custom interfaces not being +// registered in time. +// +// For now, use built-in PHP interfaces like Iterator (see below) +// or use the #[php(extends(...))] attribute with parent classes. + +// Test Feature 2: Interface inheritance via trait bounds +// Define a parent interface +#[php_interface] +#[php(name = "ExtPhpRs\\Interface\\ParentInterface")] +#[allow(dead_code)] +pub trait ParentInterface { + fn parent_method(&self) -> String; +} + +// Define a child interface that extends the parent via Rust trait bounds +#[php_interface] +#[php(name = "ExtPhpRs\\Interface\\ChildInterface")] +#[allow(dead_code)] +pub trait ChildInterface: ParentInterface { + fn child_method(&self) -> String; +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { - builder.interface::() + builder + .interface::() + .interface::() + .interface::() + // Iterator examples for issue #308 + .class::() + .class::() + .class::() } #[cfg(test)] diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index d58388e5a3..e8dedccc08 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -39,7 +39,8 @@ mod test { command.arg("--release"); // Build features list dynamically based on compiled features - // Note: Using vec_init_then_push pattern here is intentional due to conditional compilation + // Note: Using vec_init_then_push pattern here is intentional due to conditional + // compilation #[allow(clippy::vec_init_then_push)] { let mut features = vec![];