diff --git a/CHANGELOG.md b/CHANGELOG.md index 4aad9e91b..c7ca31649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,22 +1,33 @@ # Changelog +## Unreleased + +### BREAKING CHANGES + +- *(macro)* [**breaking**] Functions and methods without an explicit return type now declare `void` as their PHP return type instead of having no return type (implicit `mixed`). This improves type safety but may cause errors if your function actually returns a value without declaring it. Magic methods `__destruct` and `__clone` are excluded as PHP forbids return types on them. See [migration guide](guide/src/migration-guides/v0.16.md). + +### Added + +- *(interface)* `#[php_impl_interface]` macro for implementing PHP interfaces via Rust traits [[#590](https://github.com/davidcole1340/ext-php-rs/issues/590)] + ## [0.15.4](https://github.com/extphprs/ext-php-rs/compare/ext-php-rs-v0.15.3...ext-php-rs-v0.15.4) - 2026-01-26 ### Added -- *(array)* Entry API (Issue #525) ([#611](https://github.com/extphprs/ext-php-rs/pull/611)) (by @kakserpom) [[#525](https://github.com/davidcole1340/ext-php-rs/issues/525)] [[#611](https://github.com/davidcole1340/ext-php-rs/issues/611)] -- *(class)* Readonly and final classes ([#639](https://github.com/extphprs/ext-php-rs/pull/639)) (by @kakserpom) [[#639](https://github.com/davidcole1340/ext-php-rs/issues/639)] -- *(core)* Add observer API ([#650](https://github.com/extphprs/ext-php-rs/pull/650)) (by @ptondereau) [[#650](https://github.com/davidcole1340/ext-php-rs/issues/650)] -- *(object)* Lazy ghost and Lazy Proxy ([#636](https://github.com/extphprs/ext-php-rs/pull/636)) (by @kakserpom) [[#636](https://github.com/davidcole1340/ext-php-rs/issues/636)] -- *(string)* Smartstring support ([#643](https://github.com/extphprs/ext-php-rs/pull/643)) (by @kakserpom) [[#643](https://github.com/davidcole1340/ext-php-rs/issues/643)] +- *(array)* Entry API (Issue #525) ([#611](https://github.com/extphprs/ext-php-rs/pull/611)) (by @kakserpom) [[#525](https://github.com/davidcole1340/ext-php-rs/issues/525)] [[#611](https://github.com/davidcole1340/ext-php-rs/issues/611)] +- *(class)* Readonly and final classes ([#639](https://github.com/extphprs/ext-php-rs/pull/639)) (by @kakserpom) [[#639](https://github.com/davidcole1340/ext-php-rs/issues/639)] +- *(core)* Add observer API ([#650](https://github.com/extphprs/ext-php-rs/pull/650)) (by @ptondereau) [[#650](https://github.com/davidcole1340/ext-php-rs/issues/650)] +- *(object)* Lazy ghost and Lazy Proxy ([#636](https://github.com/extphprs/ext-php-rs/pull/636)) (by @kakserpom) [[#636](https://github.com/davidcole1340/ext-php-rs/issues/636)] +- *(string)* Smartstring support ([#643](https://github.com/extphprs/ext-php-rs/pull/643)) (by @kakserpom) [[#643](https://github.com/davidcole1340/ext-php-rs/issues/643)] ### Fixed -- *(cargo-php)* Use runtime feature for cargo-php to avoid dynamic linking on musl ([#645](https://github.com/extphprs/ext-php-rs/pull/645)) (by @ptondereau) [[#645](https://github.com/davidcole1340/ext-php-rs/issues/645)] -- *(clippy)* V1.93.0 errors ([#648](https://github.com/extphprs/ext-php-rs/pull/648)) (by @ptondereau) [[#648](https://github.com/davidcole1340/ext-php-rs/issues/648)] -- *(deps)* Bump parking_lot required version to 0.12.3 (by @TobiasBengtsson) [[#640](https://github.com/davidcole1340/ext-php-rs/issues/640)] -- *(doc)* Update mdbook config ([#651](https://github.com/extphprs/ext-php-rs/pull/651)) (by @ptondereau) [[#651](https://github.com/davidcole1340/ext-php-rs/issues/651)] -- *(macro)* Refactor allowed and forbidden keywords to match PHP parser ([#647](https://github.com/extphprs/ext-php-rs/pull/647)) (by @ptondereau) [[#647](https://github.com/davidcole1340/ext-php-rs/issues/647)] -- *(windows)* Add fallback for 404 errors in windows build ([#649](https://github.com/extphprs/ext-php-rs/pull/649)) (by @ptondereau) [[#649](https://github.com/davidcole1340/ext-php-rs/issues/649)] -- Handle PHP mocks and subclasses of Rust-backed classes ([#653](https://github.com/extphprs/ext-php-rs/pull/653)) (by @ptondereau) [[#653](https://github.com/davidcole1340/ext-php-rs/issues/653)] +- *(cargo-php)* Use runtime feature for cargo-php to avoid dynamic linking on musl ([#645](https://github.com/extphprs/ext-php-rs/pull/645)) (by @ptondereau) [[#645](https://github.com/davidcole1340/ext-php-rs/issues/645)] +- *(clippy)* V1.93.0 errors ([#648](https://github.com/extphprs/ext-php-rs/pull/648)) (by @ptondereau) [[#648](https://github.com/davidcole1340/ext-php-rs/issues/648)] +- *(deps)* Bump parking_lot required version to 0.12.3 (by @TobiasBengtsson) [[#640](https://github.com/davidcole1340/ext-php-rs/issues/640)] +- *(doc)* Update mdbook config ([#651](https://github.com/extphprs/ext-php-rs/pull/651)) (by @ptondereau) [[#651](https://github.com/davidcole1340/ext-php-rs/issues/651)] +- *(macro)* Refactor allowed and forbidden keywords to match PHP parser ([#647](https://github.com/extphprs/ext-php-rs/pull/647)) (by @ptondereau) [[#647](https://github.com/davidcole1340/ext-php-rs/issues/647)] +- *(windows)* Add fallback for 404 errors in windows build ([#649](https://github.com/extphprs/ext-php-rs/pull/649)) (by @ptondereau) [[#649](https://github.com/davidcole1340/ext-php-rs/issues/649)] +- Handle PHP mocks and subclasses of Rust-backed classes ([#653](https://github.com/extphprs/ext-php-rs/pull/653)) (by @ptondereau) [[#653](https://github.com/davidcole1340/ext-php-rs/issues/653)] + ## [0.15.3](https://github.com/extphprs/ext-php-rs/compare/ext-php-rs-v0.15.2...ext-php-rs-v0.15.3) - 2025-12-28 ### Added diff --git a/Cargo.toml b/Cargo.toml index a9c50d1fc..1bf1e1c18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ cfg-if = "1.0" once_cell = "1.21" anyhow = { version = "1", optional = true } smartstring = { version = "1", optional = true } +inventory = "0.3" ext-php-rs-derive = { version = "=0.11.7", path = "./crates/macros" } [dev-dependencies] diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 8f72e0557..f59d06ff9 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -343,6 +343,24 @@ fn generate_registered_class_impl( ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_constants() } + #[inline] + fn interface_implementations() -> ::std::vec::Vec<::ext_php_rs::class::ClassEntryInfo> { + let my_type_id = ::std::any::TypeId::of::(); + ::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() + } + + #[inline] + fn interface_method_implementations() -> ::std::vec::Vec<( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + )> { + use ::ext_php_rs::internal::class::InterfaceMethodsProvider; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default().get_interface_methods() + } + #default_init_impl } } diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 9687b50e9..a2a547bce 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -317,46 +317,59 @@ impl<'a> Function<'a> { } } - fn build_returns(&self, call_type: Option<&CallType>) -> Option { - self.output.cloned().map(|mut output| { - output.drop_lifetimes(); - - // If returning &Self or &mut Self from a method, use the class type - // for return type information since we return `this` (ZendClassObject) - if returns_self_ref(self.output) - && let Some(CallType::Method { class, .. }) = call_type + fn build_returns(&self, call_type: Option<&CallType>) -> TokenStream { + let Some(output) = self.output.cloned() else { + // PHP magic methods __destruct and __clone cannot have return types + // (only applies to class methods, not standalone functions) + if matches!(call_type, Some(CallType::Method { .. })) + && (self.name == "__destruct" || self.name == "__clone") { - return quote! { - .returns( - <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::TYPE, - false, - <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::NULLABLE, - ) - }; + return quote! {}; } + // No return type means void in PHP + return quote! { + .returns(::ext_php_rs::flags::DataType::Void, false, false) + }; + }; - // If returning Self (new instance) from a method, replace Self with - // the actual class type since Self won't resolve in generated code - if returns_self(self.output) - && let Some(CallType::Method { class, .. }) = call_type - { - return quote! { - .returns( - <#class as ::ext_php_rs::convert::IntoZval>::TYPE, - false, - <#class as ::ext_php_rs::convert::IntoZval>::NULLABLE, - ) - }; - } + let mut output = output; + output.drop_lifetimes(); - quote! { + // If returning &Self or &mut Self from a method, use the class type + // for return type information since we return `this` (ZendClassObject) + if returns_self_ref(self.output) + && let Some(CallType::Method { class, .. }) = call_type + { + return quote! { .returns( - <#output as ::ext_php_rs::convert::IntoZval>::TYPE, + <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::TYPE, false, - <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE, + <&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::NULLABLE, ) - } - }) + }; + } + + // If returning Self (new instance) from a method, replace Self with + // the actual class type since Self won't resolve in generated code + if returns_self(self.output) + && let Some(CallType::Method { class, .. }) = call_type + { + return quote! { + .returns( + <#class as ::ext_php_rs::convert::IntoZval>::TYPE, + false, + <#class as ::ext_php_rs::convert::IntoZval>::NULLABLE, + ) + }; + } + + quote! { + .returns( + <#output as ::ext_php_rs::convert::IntoZval>::TYPE, + false, + <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE, + ) + } } fn build_result( diff --git a/crates/macros/src/impl_interface.rs b/crates/macros/src/impl_interface.rs new file mode 100644 index 000000000..4bae829cf --- /dev/null +++ b/crates/macros/src/impl_interface.rs @@ -0,0 +1,265 @@ +//! 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 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::*; + +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 +/// 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(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`)"); + }; + + // 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; + + // Generate method builders for each trait method + 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(RenameRule::Camel); + + // 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: &str, + struct_ty: &syn::Type, + method_ident: &syn::Ident, + inputs: &syn::punctuated::Punctuated, + output: &ReturnType, + is_static: bool, +) -> TokenStream { + // Collect non-self arguments + 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 arg_declarations: Vec<_> = args + .iter() + .enumerate() + .map(|(i, (name, ty))| { + let php_name = ident_to_php_name(name); + quote! { + let #name: #ty = match parse.arg(#i) { + Ok(v) => v, + Err(e) => { + let msg = format!("Invalid value for argument `{}`: {}", #php_name, e); + ::ext_php_rs::exception::PhpException::default(msg.into()) + .throw() + .expect("Failed to throw PHP exception."); + return; + } + }; + } + }) + .collect(); + + let arg_names: Vec<_> = args.iter().map(|(name, _)| name).collect(); + + // Generate .arg() calls for PHP reflection metadata + let arg_builders: Vec<_> = 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 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 if there's a return type + let returns_call = match output { + ReturnType::Default => quote! {}, + ReturnType::Type(_, ty) => { + quote! { + .returns( + <#ty as ::ext_php_rs::convert::IntoZval>::TYPE, + false, + <#ty as ::ext_php_rs::convert::IntoZval>::NULLABLE, + ) + } + } + }; + + let handler_body = if is_static { + quote! { + let parse = ex.parser(); + #(#arg_declarations)* + let result = <#struct_ty>::#method_ident(#(#arg_names),*); + 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."); + } + } + } 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; + } + }; + #(#arg_declarations)* + let result = this.#method_ident(#(#arg_names),*); + 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."); + } + } + }; + + 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 + }) + #(#arg_builders)* + #returns_call, + #flags + ) + } +} diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs index 756f40eec..660cfffa8 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::{ @@ -47,11 +47,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 full path to the supertrait's PHP interface struct (e.g., + /// `PhpInterfaceBar` or `other_module::PhpInterfaceBar`) + interface_struct_path: Path, +} + +impl ToTokens for SupertraitInterface { + fn to_tokens(&self, tokens: &mut TokenStream) { + let interface_struct_path = &self.interface_struct_path; + quote! { + ( + || <#interface_struct_path as ::ext_php_rs::class::RegisteredClass>::get_metadata().ce(), + <#interface_struct_path 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>, @@ -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; @@ -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] = &[ @@ -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(), @@ -239,6 +272,34 @@ 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`. +/// +/// This function preserves the full module path, so `trait Foo: other::Bar` +/// will generate `other::PhpInterfaceBar`. +fn parse_supertraits( + supertraits: &syn::punctuated::Punctuated, +) -> Vec { + supertraits + .iter() + .filter_map(|bound| { + if let TypeParamBound::Trait(trait_bound) = bound { + // Clone the path and modify the last segment to add PhpInterface prefix + let mut path = trait_bound.path.clone(); + let last_segment = path.segments.last_mut()?; + last_segment.ident = + format_ident!("{}{}", INTERNAL_INTERFACE_NAME_PREFIX, last_segment.ident); + Some(SupertraitInterface { + interface_struct_path: path, + }) + } 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 ad0d09947..b2793b69a 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; @@ -408,6 +409,172 @@ extern crate proc_macro; /// /// This is **optional** - if your extension only targets PHP 8.2+, you can use /// `#[php(readonly)]` directly without any build script setup. +/// +/// ## 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 { @@ -623,6 +790,299 @@ 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 } +/// } +/// +/// // Note: No need to add greet() here - it's automatically +/// // registered by #[php_impl_interface] below +/// } +/// +/// // Implement the interface for the class +/// // This automatically registers greet() as a PHP method +/// #[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(); // Output: Hello, World! +/// +/// // Can be used as type hint +/// function greet(Rust\Greetable $obj): void { +/// echo $obj->greet(); +/// } +/// +/// 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. +/// +/// ## Cross-Crate Support +/// +/// The `#[php_impl_interface]` macro supports cross-crate interface discovery +/// via the [`inventory`](https://crates.io/crates/inventory) crate. This means you can define +/// an interface in one crate and implement it in another crate, and the +/// implementation will be automatically discovered at link time. +/// +/// ### Example: Defining an Interface in a Library Crate +/// +/// First, create a library crate that defines the interface: +/// +/// ```toml +/// # my-interfaces/Cargo.toml +/// [package] +/// name = "my-interfaces" +/// version = "0.1.0" +/// +/// [dependencies] +/// ext-php-rs = "0.15" +/// ``` +/// +/// ```rust,no_run,ignore,ignore +/// // my-interfaces/src/lib.rs +/// use ext_php_rs::prelude::*; +/// +/// /// A serializable interface that can convert objects to JSON. +/// #[php_interface] +/// #[php(name = "MyInterfaces\\Serializable")] +/// pub trait Serializable { +/// fn to_json(&self) -> String; +/// } +/// +/// // Re-export the generated PHP interface struct for consumers +/// pub use PhpInterfaceSerializable; +/// ``` +/// +/// ### Example: Implementing the Interface in Another Crate +/// +/// Now create your extension crate that implements the interface: +/// +/// ```toml +/// # my-extension/Cargo.toml +/// [package] +/// name = "my-extension" +/// version = "0.1.0" +/// +/// [lib] +/// crate-type = ["cdylib"] +/// +/// [dependencies] +/// ext-php-rs = "0.15" +/// my-interfaces = { path = "../my-interfaces" } +/// ``` +/// +/// ```rust,no_run,ignore,ignore +/// // my-extension/src/lib.rs +/// use ext_php_rs::prelude::*; +/// use my_interfaces::Serializable; +/// +/// #[php_class] +/// #[php(name = "MyExtension\\User")] +/// pub struct User { +/// name: String, +/// email: String, +/// } +/// +/// #[php_impl] +/// impl User { +/// pub fn __construct(name: String, email: String) -> Self { +/// Self { name, email } +/// } +/// +/// // Note: No need to add to_json() here - it's automatically +/// // registered by #[php_impl_interface] below +/// } +/// +/// // Register the interface implementation +/// // This automatically registers to_json() as a PHP method +/// #[php_impl_interface] +/// impl Serializable for User { +/// fn to_json(&self) -> String { +/// format!(r#"{{"name":"{}","email":"{}"}}"#, self.name, self.email) +/// } +/// } +/// +/// #[php_module] +/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// // Register the interface from the library crate +/// .interface::() +/// .class::() +/// } +/// ``` +/// +/// ### Using in PHP +/// +/// ```php +/// toJson(); +/// } +/// +/// echo serialize_object($user); +/// // Output: {"name":"John","email":"john@example.com"} +/// ``` +/// +/// ### Important Notes +/// +/// 1. **Automatic method registration**: The `#[php_impl_interface]` macro +/// automatically registers all trait methods as PHP methods on the class. +/// You don't need to duplicate them in a `#[php_impl]` block. +/// +/// 2. **Interface registration**: The interface must be registered in the +/// `#[php_module]` function using `.interface::()`. +/// +/// 3. **Link-time discovery**: The `inventory` crate uses link-time +/// registration for interface discovery, so all implementations are +/// automatically discovered when the final binary is linked. // END DOCS FROM interface.md #[proc_macro_attribute] pub fn php_interface(args: TokenStream, input: TokenStream) -> TokenStream { @@ -1185,6 +1645,72 @@ 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. +/// +/// **Key feature**: The macro automatically registers the trait methods as PHP +/// methods on the class. You don't need to duplicate them in a separate +/// `#[php_impl]` block. +/// +/// ## 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) -> String; +/// } +/// +/// #[php_class] +/// struct MyClass; +/// +/// // The trait method my_method() is automatically registered as a PHP method +/// #[php_impl_interface] +/// impl MyInterface for MyClass { +/// fn my_method(&self) -> String { +/// "Hello from MyClass!".to_string() +/// } +/// } +/// +/// #[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`, and `$obj->myMethod()` will be callable. +/// +/// ## 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 /// @@ -1587,6 +2113,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 88ac1df6c..f2d946596 100644 --- a/crates/macros/tests/expand/class.expanded.rs +++ b/crates/macros/tests/expand/class.expanded.rs @@ -73,4 +73,27 @@ 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, + > { + let my_type_id = ::std::any::TypeId::of::(); + ::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() + } + #[inline] + fn interface_method_implementations() -> ::std::vec::Vec< + ( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + ), + > { + use ::ext_php_rs::internal::class::InterfaceMethodsProvider; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default() + .get_interface_methods() + } } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index f9312bf0a..b47a1ae22 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -49,4 +49,5 @@ # Migration Guides --- +[v0.16](./migration-guides/v0.16.md) [v0.14](./migration-guides/v0.14.md) diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 42daf0acc..34e419b89 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -375,3 +375,166 @@ The `ext-php-rs-build` crate provides several useful utilities: This is **optional** - if your extension only targets PHP 8.2+, you can use `#[php(readonly)]` directly without any build script setup. + +## 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 6ca88898f..a37e6b4ca 100644 --- a/guide/src/macros/interface.md +++ b/guide/src/macros/interface.md @@ -71,3 +71,290 @@ 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 } + } + + // Note: No need to add greet() here - it's automatically + // registered by #[php_impl_interface] below +} + +// Implement the interface for the class +// This automatically registers greet() as a PHP method +#[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(); // Output: Hello, World! + +// Can be used as type hint +function greet(Rust\Greetable $obj): void { + echo $obj->greet(); +} + +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. + +## Cross-Crate Support + +The `#[php_impl_interface]` macro supports cross-crate interface discovery via the +[`inventory`](https://crates.io/crates/inventory) crate. This means you can define +an interface in one crate and implement it in another crate, and the implementation +will be automatically discovered at link time. + +### Example: Defining an Interface in a Library Crate + +First, create a library crate that defines the interface: + +```toml +# my-interfaces/Cargo.toml +[package] +name = "my-interfaces" +version = "0.1.0" + +[dependencies] +ext-php-rs = "0.15" +``` + +```rust,no_run,ignore +// my-interfaces/src/lib.rs +use ext_php_rs::prelude::*; + +/// A serializable interface that can convert objects to JSON. +#[php_interface] +#[php(name = "MyInterfaces\\Serializable")] +pub trait Serializable { + fn to_json(&self) -> String; +} + +// Re-export the generated PHP interface struct for consumers +pub use PhpInterfaceSerializable; +``` + +### Example: Implementing the Interface in Another Crate + +Now create your extension crate that implements the interface: + +```toml +# my-extension/Cargo.toml +[package] +name = "my-extension" +version = "0.1.0" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ext-php-rs = "0.15" +my-interfaces = { path = "../my-interfaces" } +``` + +```rust,no_run,ignore +// my-extension/src/lib.rs +use ext_php_rs::prelude::*; +use my_interfaces::Serializable; + +#[php_class] +#[php(name = "MyExtension\\User")] +pub struct User { + name: String, + email: String, +} + +#[php_impl] +impl User { + pub fn __construct(name: String, email: String) -> Self { + Self { name, email } + } + + // Note: No need to add to_json() here - it's automatically + // registered by #[php_impl_interface] below +} + +// Register the interface implementation +// This automatically registers to_json() as a PHP method +#[php_impl_interface] +impl Serializable for User { + fn to_json(&self) -> String { + format!(r#"{{"name":"{}","email":"{}"}}"#, self.name, self.email) + } +} + +#[php_module] +pub fn module(module: ModuleBuilder) -> ModuleBuilder { + module + // Register the interface from the library crate + .interface::() + .class::() +} +``` + +### Using in PHP + +```php +toJson(); +} + +echo serialize_object($user); +// Output: {"name":"John","email":"john@example.com"} +``` + +### Important Notes + +1. **Automatic method registration**: The `#[php_impl_interface]` macro automatically + registers all trait methods as PHP methods on the class. You don't need to duplicate + them in a `#[php_impl]` block. + +2. **Interface registration**: The interface must be registered in the `#[php_module]` + function using `.interface::()`. + +3. **Link-time discovery**: The `inventory` crate uses link-time registration for + interface discovery, so all implementations are automatically discovered when the + final binary is linked. diff --git a/guide/src/migration-guides/v0.16.md b/guide/src/migration-guides/v0.16.md new file mode 100644 index 000000000..1842a308b --- /dev/null +++ b/guide/src/migration-guides/v0.16.md @@ -0,0 +1,42 @@ +# Migrating to `v0.16` + +## Void Return Type for Functions Without Explicit Return + +Functions and methods without an explicit Rust return type now declare `void` as their PHP return type. + +### Before (v0.15) + +```rust,ignore +#[php_function] +pub fn do_something() { + println!("Hello"); +} +``` + +Generated PHP signature: `function do_something()` (implicit `mixed` return) + +### After (v0.16) + +The same Rust code now generates: `function do_something(): void` + +### Migration + +If your function actually returns a value but didn't declare it, you must now add the return type: + +```rust,ignore +// Before: worked but was incorrect +#[php_function] +pub fn get_value() { + 42 // implicitly returned, but no declared type +} + +// After: must declare return type +#[php_function] +pub fn get_value() -> i32 { + 42 +} +``` + +### Exceptions + +The magic methods `__destruct` and `__clone` are excluded from this change, as PHP forbids return type declarations on them. diff --git a/src/builders/class.rs b/src/builders/class.rs index 4a166037a..975a6d32e 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -280,7 +280,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() { @@ -289,20 +291,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/module.rs b/src/builders/module.rs index 1bdcaf978..31cf06943 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -269,8 +269,8 @@ impl ModuleBuilder<'_> { } builder = builder.flags(ClassFlags::Interface); + // Note: interfaces should NOT have object_override because they cannot be instantiated builder - .object_override::() .registration(|ce| { T::get_metadata().set_ce(ce); }) @@ -290,12 +290,23 @@ impl ModuleBuilder<'_> { for (method, flags) in T::method_builders() { builder = builder.method(method, flags); } + // Methods from #[php_impl_interface] trait implementations. + // Uses the inventory crate for cross-crate method discovery. + for (method, flags) in T::interface_method_implementations() { + builder = builder.method(method, flags); + } 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. + // Uses the inventory crate for cross-crate interface discovery. + for interface in T::interface_implementations() { + builder = builder.implements(interface); + } for (name, value, docs) in T::constants() { builder = builder .dyn_constant(*name, *value, docs) @@ -378,14 +389,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 934082793..4c2b521b7 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 4803124d6..dec8ff2db 100644 --- a/src/class.rs +++ b/src/class.rs @@ -88,6 +88,32 @@ pub trait RegisteredClass: Sized + 'static { &[] } + /// Returns interfaces from `#[php_impl_interface]` trait implementations. + /// + /// This method is generated by the `#[php_class]` macro and uses the + /// `inventory` crate to collect interface registrations across crate + /// boundaries at link time. + /// + /// The default implementation returns an empty vector. The macro overrides + /// this to iterate over `InterfaceRegistration` entries matching this type. + #[must_use] + fn interface_implementations() -> Vec { + Vec::new() + } + + /// Returns methods from `#[php_impl_interface]` trait implementations. + /// + /// This method is generated by the `#[php_class]` macro and uses the + /// `inventory` crate to collect method registrations across crate + /// boundaries at link time. + /// + /// The default implementation returns an empty vector. The macro overrides + /// this to iterate over `MethodRegistration` entries matching this type. + #[must_use] + fn interface_method_implementations() -> Vec<(FunctionBuilder<'static>, MethodFlags)> { + Vec::new() + } + /// Returns a default instance of the class for immediate initialization. /// /// This is used when PHP creates an object without calling the constructor, diff --git a/src/closure.rs b/src/closure.rs index f184ff8f4..f58c9a6a0 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 d175c3f69..4e9a3e076 100644 --- a/src/embed/mod.rs +++ b/src/embed/mod.rs @@ -294,8 +294,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 5868a876d..8bcbd6970 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 e126a56f8..b5d6b9657 100644 --- a/src/internal/class.rs +++ b/src/internal/class.rs @@ -1,14 +1,42 @@ -use std::{collections::HashMap, marker::PhantomData}; +use std::{any::TypeId, collections::HashMap, marker::PhantomData}; use crate::{ builders::FunctionBuilder, - class::{ConstructorMeta, RegisteredClass}, + class::{ClassEntryInfo, ConstructorMeta, RegisteredClass}, convert::{IntoZval, IntoZvalDyn}, describe::DocComments, flags::MethodFlags, props::Property, }; +/// Registration entry for interface implementations. +/// Used by `#[php_impl_interface]` macro to register interfaces across crate boundaries. +pub struct InterfaceRegistration { + /// The `TypeId` of the class implementing the interface. + pub class_type_id: TypeId, + /// Function that returns the interface's `ClassEntryInfo`. + pub interface_getter: fn() -> ClassEntryInfo, +} + +inventory::collect!(InterfaceRegistration); + +/// Trait for getting interface method builders. +/// This trait uses autoref specialization to allow optional implementation. +/// Classes with `#[php_impl_interface]` will implement this directly (not on a reference). +pub trait InterfaceMethodsProvider { + fn get_interface_methods(self) -> Vec<(FunctionBuilder<'static>, MethodFlags)>; +} + +/// Default implementation for classes without interface implementations. +/// Uses autoref specialization - the reference implementation is chosen when +/// no direct implementation exists. +impl InterfaceMethodsProvider for &'_ PhpClassImplCollector { + #[inline] + fn get_interface_methods(self) -> Vec<(FunctionBuilder<'static>, MethodFlags)> { + Vec::new() + } +} + /// Collector used to collect methods for PHP classes. pub struct PhpClassImplCollector(PhantomData); diff --git a/src/lib.rs b/src/lib.rs index bc0ee1aba..41ab80ce8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,6 +39,10 @@ pub mod observer { } #[doc(hidden)] pub mod internal; + +// Re-export inventory for use by macros +#[doc(hidden)] +pub use inventory; pub mod props; pub mod rc; #[cfg(test)] @@ -66,8 +70,8 @@ pub mod prelude { #[cfg(feature = "observer")] pub use crate::zend::{FcallInfo, FcallObserver}; 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, }; } @@ -98,6 +102,6 @@ pub const PHP_85: bool = cfg!(php85); #[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 fbcc4c7f0..4d4cb4086 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 ccfee006f..357e75779 100644 --- a/src/types/zval.rs +++ b/src/types/zval.rs @@ -518,8 +518,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 1266bc4e9..283c02d00 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'); @@ -25,7 +25,7 @@ public function refToLikeThisClass( return sprintf('%s | %s', $this->nonStatic($data), $other->nonStatic($data)); } - public function setValue(?int $value = 0) { + public function setValue(int $value = 0): void { } } @@ -36,3 +36,133 @@ 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'); + +// ============================================================================ +// Test Feature 3: #[php_impl_interface] - Rust class implementing custom interface +// This demonstrates how a Rust struct can implement a PHP interface defined with +// #[php_interface] using the #[php_impl_interface] attribute +// ============================================================================ + +assert(class_exists('ExtPhpRs\Interface\Greeter'), 'Greeter class should exist'); + +$greeter = new ExtPhpRs\Interface\Greeter('World'); + +// Test that Greeter implements ParentInterface via #[php_impl_interface] +assert($greeter instanceof ExtPhpRs\Interface\ParentInterface, + 'Greeter should implement ParentInterface via #[php_impl_interface]'); + +// Test is_a() works for interface checking +assert(is_a($greeter, 'ExtPhpRs\Interface\ParentInterface'), + 'is_a() should recognize Greeter as ParentInterface'); + +// Test the class method +assert($greeter->getName() === 'World', 'Greeter should return correct name'); + +// Test type hinting with the interface +function greetWithInterface(ExtPhpRs\Interface\ParentInterface $obj): string { + // The parentMethod is defined on the interface + return $obj->parentMethod(); +} + +$result = greetWithInterface($greeter); +assert($result === 'Hello from World!', 'parentMethod should work via interface type hint'); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs index 620e5ac4a..7c6d13138 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,251 @@ 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: Cross-crate interface discovery is now supported via the `inventory` crate. +// You can use `#[php_impl_interface]` to implement interfaces defined in other crates. +// See the `php_interface` and `php_impl_interface` macros for more details. + +// 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; +} + +// ============================================================================ +// Test Feature 4: Using #[php_impl_interface] with inventory-based discovery +// This demonstrates cross-crate interface discovery via the inventory crate. +// ============================================================================ + +/// A simple greeter class that implements the `ParentInterface` via +/// `#[php_impl_interface]`. +#[php_class] +#[php(name = "ExtPhpRs\\Interface\\Greeter")] +pub struct Greeter { + name: String, +} + +#[php_impl] +impl Greeter { + pub fn __construct(name: String) -> Self { + Self { name } + } + + pub fn get_name(&self) -> &str { + &self.name + } +} + +#[php_impl_interface] +impl ParentInterface for Greeter { + fn parent_method(&self) -> String { + format!("Hello from {}!", self.name) + } +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { - builder.interface::() + builder + .interface::() + .interface::() + .interface::() + // Iterator examples for issue #308 + .class::() + .class::() + .class::() + // Greeter with #[php_impl_interface] + .class::() } #[cfg(test)]