diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 81a2a78c6..573501182 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -118,6 +118,7 @@ bind! { zend_register_double_constant, zend_register_ini_entries, zend_register_internal_enum, + zend_register_internal_interface, zend_ini_entry_def, zend_register_internal_class_ex, zend_register_long_constant, diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 103a238dd..4264d22c5 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -1,7 +1,7 @@ use darling::util::Flag; use darling::{FromAttributes, FromMeta, ToTokens}; use proc_macro2::TokenStream; -use quote::quote; +use quote::{TokenStreamExt, quote}; use syn::{Attribute, Expr, Fields, ItemStruct}; use crate::helpers::get_docs; @@ -28,8 +28,17 @@ pub struct StructAttributes { #[derive(FromMeta, Debug)] pub struct ClassEntryAttribute { - ce: syn::Expr, - stub: String, + pub ce: syn::Expr, + pub stub: String, +} + +impl ToTokens for ClassEntryAttribute { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ce = &self.ce; + let stub = &self.stub; + let token = quote! { (#ce, #stub) }; + tokens.append_all(token); + } } pub fn parser(mut input: ItemStruct) -> Result { @@ -151,10 +160,8 @@ fn generate_registered_class_impl( }; let extends = if let Some(extends) = extends { - let ce = &extends.ce; - let stub = &extends.stub; quote! { - Some((#ce, #stub)) + Some(#extends) } } else { quote! { None } diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 270a84618..bcc4865cc 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -131,6 +131,40 @@ impl<'a> Function<'a> { format_ident!("_internal_{}", &self.ident) } + pub fn abstract_function_builder(&self) -> TokenStream { + let name = &self.name; + let (required, not_required) = self.args.split_args(self.optional.as_ref()); + + // `entry` impl + let required_args = required + .iter() + .map(TypedArg::arg_builder) + .collect::>(); + let not_required_args = not_required + .iter() + .map(TypedArg::arg_builder) + .collect::>(); + + let returns = self.build_returns(); + let docs = if self.docs.is_empty() { + quote! {} + } else { + let docs = &self.docs; + quote! { + .docs(&[#(#docs),*]) + } + }; + + quote! { + ::ext_php_rs::builders::FunctionBuilder::new_abstract(#name) + #(.arg(#required_args))* + .not_required() + #(.arg(#not_required_args))* + #returns + #docs + } + } + /// Generates the function builder for the function. pub fn function_builder(&self, call_type: CallType) -> TokenStream { let name = &self.name; diff --git a/crates/macros/src/helpers.rs b/crates/macros/src/helpers.rs index 162c904fd..025af1991 100644 --- a/crates/macros/src/helpers.rs +++ b/crates/macros/src/helpers.rs @@ -24,3 +24,13 @@ pub fn get_docs(attrs: &[Attribute]) -> Result> { }) .collect::>>() } + +pub trait CleanPhpAttr { + fn clean_php(&mut self); +} + +impl CleanPhpAttr for Vec { + fn clean_php(&mut self) { + self.retain(|attr| !attr.path().is_ident("php")); + } +} diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 771eaa6b2..1d9687149 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -125,7 +125,7 @@ struct ParsedImpl<'a> { } #[derive(Debug, Eq, Hash, PartialEq)] -enum MethodModifier { +pub enum MethodModifier { Abstract, Static, } @@ -141,7 +141,7 @@ impl quote::ToTokens for MethodModifier { } #[derive(Debug)] -struct FnBuilder { +pub struct FnBuilder { /// Tokens which represent the `FunctionBuilder` for this function. pub builder: TokenStream, /// The visibility of this method. @@ -151,13 +151,13 @@ struct FnBuilder { } #[derive(Debug)] -struct Constant<'a> { +pub struct Constant<'a> { /// Name of the constant in PHP land. - name: String, + pub name: String, /// Identifier of the constant in Rust land. - ident: &'a syn::Ident, + pub ident: &'a syn::Ident, /// Documentation for the constant. - docs: Vec, + pub docs: Vec, } impl<'a> ParsedImpl<'a> { diff --git a/crates/macros/src/interface.rs b/crates/macros/src/interface.rs new file mode 100644 index 000000000..6e9a54a4f --- /dev/null +++ b/crates/macros/src/interface.rs @@ -0,0 +1,347 @@ +use std::collections::{HashMap, HashSet}; + +use crate::class::ClassEntryAttribute; +use crate::constant::PhpConstAttribute; +use crate::function::{Args, Function}; +use crate::helpers::{CleanPhpAttr, get_docs}; +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 crate::impl_::{FnBuilder, MethodModifier}; +use crate::parsing::{PhpRename, RenameRule, Visibility}; +use crate::prelude::*; + +const INTERNAL_INTERFACE_NAME_PREFIX: &str = "PhpInterface"; + +#[derive(FromAttributes, Debug, Default)] +#[darling(attributes(php), forward_attrs(doc), default)] +pub struct TraitAttributes { + #[darling(flatten)] + rename: PhpRename, + /// Rename methods to match the given rule. + change_method_case: Option, + /// Rename constants to match the given rule. + change_constant_case: Option, + #[darling(multiple)] + extends: Vec, + attrs: Vec, +} + +pub fn parser(mut input: ItemTrait) -> Result { + let interface_data: InterfaceData = input.parse()?; + let interface_tokens = quote! { #interface_data }; + + Ok(quote! { + #input + + #interface_tokens + }) +} + +trait Parse<'a, T> { + fn parse(&'a mut self) -> Result; +} + +struct InterfaceData<'a> { + ident: &'a Ident, + name: String, + path: Path, + extends: Vec, + constructor: Option>, + methods: Vec, + constants: Vec>, + docs: Vec, +} + +impl ToTokens for InterfaceData<'_> { + #[allow(clippy::too_many_lines)] + fn to_tokens(&self, tokens: &mut TokenStream) { + let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{}", self.ident); + let name = &self.name; + let implements = &self.extends; + let methods_sig = &self.methods; + let constants = &self.constants; + let docs = &self.docs; + + let _constructor = self + .constructor + .as_ref() + .map(|func| func.constructor_meta(&self.path, Some(&Visibility::Public))) + .option_tokens(); + + quote! { + pub struct #interface_name; + + impl ::ext_php_rs::class::RegisteredClass for #interface_name { + const CLASS_NAME: &'static str = #name; + + const BUILDER_MODIFIER: Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + + const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; + + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; + + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[ + #(#implements,)* + ]; + + const DOC_COMMENTS: &'static [&'static str] = &[ + #(#docs,)* + ]; + + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata<#interface_name> = + ::ext_php_rs::class::ClassMetadata::new(); + + &METADATA + } + + fn method_builders() -> Vec<( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + )> { + vec![#(#methods_sig),*] + } + + fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { + None + } + + fn constants() -> &'static [( + &'static str, + &'static dyn ext_php_rs::convert::IntoZvalDyn, + ext_php_rs::describe::DocComments, + )] { + &[#(#constants),*] + } + + fn get_properties<'a>() -> ::std::collections::HashMap<&'static str, ::ext_php_rs::internal::property::PropertyInfo<'a, Self>> { + panic!("Not supported for Interface"); + } + } + + impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a #interface_name { + #[inline] + fn from_zend_object( + obj: &'a ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&**obj) + } + } + impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> for &'a mut #interface_name { + #[inline] + fn from_zend_object_mut( + obj: &'a mut ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::<#interface_name>::from_zend_obj_mut(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&mut **obj) + } + } + impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + #[inline] + fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object(zval.object()?).ok() + } + } + impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> for &'a mut #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + #[inline] + fn from_zval_mut(zval: &'a mut ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object_mut(zval.object_mut()?) + .ok() + } + } + impl ::ext_php_rs::convert::IntoZendObject for #interface_name { + #[inline] + fn into_zend_object( + self, + ) -> ::ext_php_rs::error::Result<::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>> + { + Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) + } + } + impl ::ext_php_rs::convert::IntoZval for #interface_name { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object(Some( + <#interface_name as ::ext_php_rs::class::RegisteredClass>::CLASS_NAME, + )); + const NULLABLE: bool = false; + #[inline] + fn set_zval( + self, + zv: &mut ::ext_php_rs::types::Zval, + persistent: bool, + ) -> ::ext_php_rs::error::Result<()> { + use ::ext_php_rs::convert::IntoZendObject; + self.into_zend_object()?.set_zval(zv, persistent) + } + } + }.to_tokens(tokens); + } +} + +impl<'a> Parse<'a, InterfaceData<'a>> for ItemTrait { + fn parse(&'a mut self) -> Result> { + let attrs = TraitAttributes::from_attributes(&self.attrs)?; + let ident = &self.ident; + let name = attrs.rename.rename(ident.to_string(), RenameRule::Pascal); + let docs = get_docs(&attrs.attrs)?; + self.attrs.clean_php(); + let interface_name = format_ident!("{INTERNAL_INTERFACE_NAME_PREFIX}{ident}"); + let ts = quote! { #interface_name }; + let path: Path = syn::parse2(ts)?; + let mut data = InterfaceData { + ident, + name, + path, + extends: attrs.extends, + constructor: None, + methods: Vec::default(), + constants: Vec::default(), + docs, + }; + + for item in &mut self.items { + match item { + TraitItem::Fn(f) => match parse_trait_item_fn(f, attrs.change_method_case)? { + MethodKind::Method(builder) => data.methods.push(builder), + MethodKind::Constructor(builder) => { + if data.constructor.replace(builder).is_some() { + bail!("Only one constructor can be provided per class."); + } + } + }, + TraitItem::Const(c) => data + .constants + .push(parse_trait_item_const(c, attrs.change_constant_case)?), + _ => {} + } + } + + Ok(data) + } +} + +#[derive(FromAttributes, Default, Debug)] +#[darling(default, attributes(php), forward_attrs(doc))] +pub struct PhpFunctionInterfaceAttribute { + #[darling(flatten)] + rename: PhpRename, + defaults: HashMap, + optional: Option, + vis: Option, + attrs: Vec, + getter: Flag, + setter: Flag, + constructor: Flag, +} + +enum MethodKind<'a> { + Method(FnBuilder), + Constructor(Function<'a>), +} + +fn parse_trait_item_fn( + fn_item: &mut TraitItemFn, + change_case: Option, +) -> Result> { + if fn_item.default.is_some() { + bail!(fn_item => "Interface an not have default impl"); + } + + let php_attr = PhpFunctionInterfaceAttribute::from_attributes(&fn_item.attrs)?; + fn_item.attrs.clean_php(); + + let mut args = Args::parse_from_fnargs(fn_item.sig.inputs.iter(), php_attr.defaults)?; + + let docs = get_docs(&php_attr.attrs)?; + + let mut modifiers: HashSet = HashSet::new(); + modifiers.insert(MethodModifier::Abstract); + + if args.typed.first().is_some_and(|arg| arg.name == "self_") { + args.typed.pop(); + } else if args.receiver.is_none() { + modifiers.insert(MethodModifier::Static); + } + + let f = Function::new( + &fn_item.sig, + php_attr.rename.rename( + fn_item.sig.ident.to_string(), + change_case.unwrap_or(RenameRule::Camel), + ), + args, + php_attr.optional, + docs, + ); + + if php_attr.constructor.is_present() { + Ok(MethodKind::Constructor(f)) + } else { + let builder = FnBuilder { + builder: f.abstract_function_builder(), + vis: php_attr.vis.unwrap_or(Visibility::Public), + modifiers, + }; + + Ok(MethodKind::Method(builder)) + } +} + +#[derive(Debug)] +struct Constant<'a> { + name: String, + expr: &'a Expr, + docs: Vec, +} + +impl ToTokens for Constant<'_> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let name = &self.name; + let expr = &self.expr; + let docs = &self.docs; + quote! { + (#name, &#expr, &[#(#docs),*]) + } + .to_tokens(tokens); + } +} + +impl<'a> Constant<'a> { + fn new(name: String, expr: &'a Expr, docs: Vec) -> Self { + Self { name, expr, docs } + } +} + +fn parse_trait_item_const( + const_item: &mut TraitItemConst, + change_case: Option, +) -> Result> { + if const_item.default.is_none() { + bail!(const_item => "PHP Interface const can not be empty"); + } + + let attr = PhpConstAttribute::from_attributes(&const_item.attrs)?; + let name = attr.rename.rename( + const_item.ident.to_string(), + change_case.unwrap_or(RenameRule::ScreamingSnake), + ); + let docs = get_docs(&attr.attrs)?; + const_item.attrs.clean_php(); + + let (_, expr) = const_item.default.as_ref().unwrap(); + Ok(Constant::new(name, expr, docs)) +} diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index a60aeae92..c1bdefc38 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -7,6 +7,7 @@ mod fastcall; mod function; mod helpers; mod impl_; +mod interface; mod module; mod parsing; mod syn_ext; @@ -14,7 +15,9 @@ mod zval; use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; -use syn::{DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct}; +use syn::{ + DeriveInput, ItemConst, ItemEnum, ItemFn, ItemForeignMod, ItemImpl, ItemStruct, ItemTrait, +}; extern crate proc_macro; @@ -23,32 +26,34 @@ extern crate proc_macro; /// /// Structs can be exported to PHP as classes with the `#[php_class]` attribute /// macro. This attribute derives the `RegisteredClass` trait on your struct, as -/// well as registering the class to be registered with the `#[php_module]` macro. +/// well as registering the class to be registered with the `#[php_module]` +/// macro. /// /// ## Options /// /// There are additional macros that modify the class. These macros **must** be /// placed underneath the `#[php_class]` attribute. /// -/// - `name` - Changes the name of the class when exported to PHP. The Rust struct -/// name is kept the same. If no name is given, the name of the struct is used. -/// Useful for namespacing classes. +/// - `name` - Changes the name of the class when exported to PHP. The Rust +/// struct name is kept the same. If no name is given, the name of the struct +/// is used. Useful for namespacing classes. /// - `change_case` - Changes the case of the class name when exported to PHP. -/// - `#[php(extends(ce = ce_fn, stub = "ParentClass"))]` - Sets the parent class of the class. Can only be used once. -/// `ce_fn` must be a function with the signature `fn() -> &'static ClassEntry`. -/// - `#[php(implements(ce = ce_fn, stub = "InterfaceName"))]` - Implements the given interface on the class. Can be used -/// multiple times. `ce_fn` must be a valid function with the signature -/// `fn() -> &'static ClassEntry`. -/// -/// You may also use the `#[php(prop)]` attribute on a struct field to use the field as a -/// PHP property. By default, the field will be accessible from PHP publicly with -/// the same name as the field. Property types must implement `IntoZval` and -/// `FromZval`. +/// - `#[php(extends(ce = ce_fn, stub = "ParentClass"))]` - Sets the parent +/// class of the class. Can only be used once. `ce_fn` must be a function with +/// the signature `fn() -> &'static ClassEntry`. +/// - `#[php(implements(ce = ce_fn, stub = "InterfaceName"))]` - Implements the +/// given interface on the class. Can be used multiple times. `ce_fn` must be +/// a valid function with the signature `fn() -> &'static ClassEntry`. +/// +/// You may also use the `#[php(prop)]` attribute on a struct field to use the +/// field as a PHP property. By default, the field will be accessible from PHP +/// publicly with the same name as the field. Property types must implement +/// `IntoZval` and `FromZval`. /// /// You can rename the property with options: /// -/// - `name` - Allows you to rename the property, e.g. -/// `#[php(name = "new_name")]` +/// - `name` - Allows you to rename the property, e.g. `#[php(name = +/// "new_name")]` /// - `change_case` - Allows you to rename the property using rename rules, e.g. /// `#[php(change_case = PascalCase)]` /// @@ -56,17 +61,19 @@ extern crate proc_macro; /// /// ### No lifetime parameters /// -/// Rust lifetimes are used by the Rust compiler to reason about a program's memory safety. -/// They are a compile-time only concept; -/// there is no way to access Rust lifetimes at runtime from a dynamic language like PHP. +/// Rust lifetimes are used by the Rust compiler to reason about a program's +/// memory safety. They are a compile-time only concept; +/// there is no way to access Rust lifetimes at runtime from a dynamic language +/// like PHP. /// /// As soon as Rust data is exposed to PHP, -/// there is no guarantee which the Rust compiler can make on how long the data will live. -/// PHP is a reference-counted language and those references can be held -/// for an arbitrarily long time, which is untraceable by the Rust compiler. -/// The only possible way to express this correctly is to require that any `#[php_class]` -/// does not borrow data for any lifetime shorter than the `'static` lifetime, -/// i.e. the `#[php_class]` cannot have any lifetime parameters. +/// there is no guarantee which the Rust compiler can make on how long the data +/// will live. PHP is a reference-counted language and those references can be +/// held for an arbitrarily long time, which is untraceable by the Rust +/// compiler. The only possible way to express this correctly is to require that +/// any `#[php_class]` does not borrow data for any lifetime shorter than the +/// `'static` lifetime, i.e. the `#[php_class]` cannot have any lifetime +/// parameters. /// /// When you need to share ownership of data between PHP and Rust, /// instead of using borrowed references with lifetimes, consider using @@ -74,11 +81,12 @@ extern crate proc_macro; /// /// ### No generic parameters /// -/// A Rust struct `Foo` with a generic parameter `T` generates new compiled implementations -/// each time it is used with a different concrete type for `T`. +/// A Rust struct `Foo` with a generic parameter `T` generates new compiled +/// implementations each time it is used with a different concrete type for `T`. /// These new implementations are generated by the compiler at each usage site. /// This is incompatible with wrapping `Foo` in PHP, -/// where there needs to be a single compiled implementation of `Foo` which is integrated with the PHP interpreter. +/// where there needs to be a single compiled implementation of `Foo` which is +/// integrated with the PHP interpreter. /// /// ## Example /// @@ -104,8 +112,8 @@ extern crate proc_macro; /// # fn main() {} /// ``` /// -/// Create a custom exception `RedisException`, which extends `Exception`, and put -/// it in the `Redis\Exception` namespace: +/// Create a custom exception `RedisException`, which extends `Exception`, and +/// put it in the `Redis\Exception` namespace: /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -139,8 +147,8 @@ extern crate proc_macro; /// /// ## Implementing an Interface /// -/// To implement an interface, use `#[php(implements(ce = ce_fn, stub = "InterfaceName")]` where `ce_fn` is an function returning a `ClassEntry`. -/// The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): +/// To implement an interface, use `#[php(implements(ce = ce_fn, stub = +/// "InterfaceName")]` where `ce_fn` is an function returning a `ClassEntry`. The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): /// /// ````rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -214,23 +222,27 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { // BEGIN DOCS FROM enum.md /// # `#[php_enum]` Attribute /// -/// Enums can be exported to PHP as enums with the `#[php_enum]` attribute macro. -/// This attribute derives the `RegisteredClass` and `PhpEnum` traits on your enum. -/// To register the enum use the `enumeration::()` method on the `ModuleBuilder` -/// in the `#[php_module]` macro. +/// Enums can be exported to PHP as enums with the `#[php_enum]` attribute +/// macro. This attribute derives the `RegisteredClass` and `PhpEnum` traits on +/// your enum. To register the enum use the `enumeration::()` method +/// on the `ModuleBuilder` in the `#[php_module]` macro. /// /// ## Options /// /// The `#[php_enum]` attribute can be configured with the following options: -/// - `#[php(name = "EnumName")]` or `#[php(change_case = snake_case)]`: Sets the name of the enum in PHP. -/// The default is the `PascalCase` name of the enum. -/// - `#[php(allow_native_discriminants)]`: Allows the use of native Rust discriminants (e.g., `Hearts = 1`). +/// - `#[php(name = "EnumName")]` or `#[php(change_case = snake_case)]`: Sets +/// the name of the enum in PHP. The default is the `PascalCase` name of the +/// enum. +/// - `#[php(allow_native_discriminants)]`: Allows the use of native Rust +/// discriminants (e.g., `Hearts = 1`). /// /// The cases of the enum can be configured with the following options: -/// - `#[php(name = "CaseName")]` or `#[php(change_case = snake_case)]`: Sets the name of the enum case in PHP. -/// The default is the `PascalCase` name of the case. -/// - `#[php(value = "value")]` or `#[php(value = 123)]`: Sets the discriminant value for the enum case. -/// This can be a string or an integer. If not set, the case will be exported as a simple enum case without a discriminant. +/// - `#[php(name = "CaseName")]` or `#[php(change_case = snake_case)]`: Sets +/// the name of the enum case in PHP. The default is the `PascalCase` name of +/// the case. +/// - `#[php(value = "value")]` or `#[php(value = 123)]`: Sets the discriminant +/// value for the enum case. This can be a string or an integer. If not set, +/// the case will be exported as a simple enum case without a discriminant. /// /// ### Example /// @@ -257,10 +269,12 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// ``` /// /// ## Backed Enums -/// Enums can also be backed by either `i64` or `&'static str`. Those values can be set using the -/// `#[php(value = "value")]` or `#[php(value = 123)]` attributes on the enum variants. +/// Enums can also be backed by either `i64` or `&'static str`. Those values can +/// be set using the `#[php(value = "value")]` or `#[php(value = 123)]` +/// attributes on the enum variants. /// -/// All variants must have a value of the same type, either all `i64` or all `&'static str`. +/// All variants must have a value of the same type, either all `i64` or all +/// `&'static str`. /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -286,10 +300,12 @@ fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// ``` /// /// ### 'Native' Discriminators -/// Native rust discriminants are currently not supported and will not be exported to PHP. +/// Native rust discriminants are currently not supported and will not be +/// exported to PHP. /// -/// To avoid confusion a compiler error will be raised if you try to use a native discriminant. -/// You can ignore this error by adding the `#[php(allow_native_discriminants)]` attribute to your enum. +/// To avoid confusion a compiler error will be raised if you try to use a +/// native discriminant. You can ignore this error by adding the +/// `#[php(allow_native_discriminants)]` attribute to your enum. /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -326,6 +342,94 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { enum_::parser(input).unwrap_or_else(|e| e.to_compile_error()) } +// BEGIN DOCS FROM interface.md +/// # `#[php_interface]` Attribute +/// +/// You can export a `Trait` block to PHP. This exports all methods as well as +/// constants to PHP on the interface. Trait method SHOULD NOT contain default +/// implementations, as these are not supported in PHP interfaces. +/// +/// ## Options +/// +/// By default all constants are renamed to `UPPER_CASE` and all methods are +/// renamed to `camelCase`. This can be changed by passing the +/// `change_method_case` and `change_constant_case` as `#[php]` attributes on +/// the `impl` block. The options are: +/// +/// - `#[php(change_method_case = "snake_case")]` - Renames the method to snake +/// case. +/// - `#[php(change_constant_case = "snake_case")]` - Renames the constant to +/// snake case. +/// +/// See the [`name` and `change_case`](./php.md#name-and-change_case) section +/// for a list of all available cases. +/// +/// ## Methods +/// +/// See the [`php_impl`](./impl.md#) +/// +/// ## Constants +/// +/// See the [`php_impl`](./impl.md#) +/// +/// ## Example +/// +/// Define an example trait with methods and constant: +/// +/// ```rust,no_run,ignore +/// # #![cfg_attr(windows, feature(abi_vectorcall))] +/// # extern crate ext_php_rs; +/// use ext_php_rs::{prelude::*, types::ZendClassObject}; +/// +/// +/// #[php_interface] +/// #[php(name = "Rust\\TestInterface")] +/// trait Test { +/// const TEST: &'static str = "TEST"; +/// +/// fn co(); +/// +/// #[php(defaults(value = 0))] +/// fn set_value(&mut self, value: i32); +/// } +/// +/// #[php_module] +/// pub fn module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .interface::() +/// } +/// +/// # fn main() {} +/// ``` +/// +/// Using our newly created interface in PHP: +/// +/// ```php +/// TokenStream { + php_interface_internal(args.into(), input.into()).into() +} + +fn php_interface_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { + let input = parse_macro_input2!(input as ItemTrait); + + interface::parser(input).unwrap_or_else(|e| e.to_compile_error()) +} + // BEGIN DOCS FROM function.md /// # `#[php_function]` Attribute /// @@ -337,10 +441,10 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// /// ## Optional parameters /// -/// Optional parameters can be used by setting the Rust parameter type to a variant -/// of `Option`. The macro will then figure out which parameters are optional by -/// using the last consecutive arguments that are a variant of `Option` or have a -/// default value. +/// Optional parameters can be used by setting the Rust parameter type to a +/// variant of `Option`. The macro will then figure out which parameters are +/// optional by using the last consecutive arguments that are a variant of +/// `Option` or have a default value. /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -365,9 +469,9 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// # fn main() {} /// ``` /// -/// Default parameter values can also be set for optional parameters. This is done -/// through the `#[php(defaults)]` attribute option. When an optional parameter has a -/// default, it does not need to be a variant of `Option`: +/// Default parameter values can also be set for optional parameters. This is +/// done through the `#[php(defaults)]` attribute option. When an optional +/// parameter has a default, it does not need to be a variant of `Option`: /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -453,9 +557,9 @@ fn php_enum_internal(_args: TokenStream2, input: TokenStream2) -> TokenStream2 { /// /// ## Variadic Functions /// -/// Variadic functions can be implemented by specifying the last argument in the Rust -/// function to the type `&[&Zval]`. This is the equivalent of a PHP function using -/// the `...$args` syntax. +/// Variadic functions can be implemented by specifying the last argument in the +/// Rust function to the type `&[&Zval]`. This is the equivalent of a PHP +/// function using the `...$args` syntax. /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -500,16 +604,16 @@ fn php_function_internal(args: TokenStream2, input: TokenStream2) -> TokenStream // BEGIN DOCS FROM constant.md /// # `#[php_const]` Attribute /// -/// Exports a Rust constant as a global PHP constant. The constant can be any type -/// that implements `IntoConst`. +/// Exports a Rust constant as a global PHP constant. The constant can be any +/// type that implements `IntoConst`. /// -/// The `wrap_constant!()` macro can be used to simplify the registration of constants. -/// It sets the name and doc comments for the constant. +/// The `wrap_constant!()` macro can be used to simplify the registration of +/// constants. It sets the name and doc comments for the constant. /// /// You can rename the const with options: /// -/// - `name` - Allows you to rename the property, e.g. -/// `#[php(name = "new_name")]` +/// - `name` - Allows you to rename the property, e.g. `#[php(name = +/// "new_name")]` /// - `change_case` - Allows you to rename the property using rename rules, e.g. /// `#[php(change_case = PascalCase)]` /// @@ -568,25 +672,25 @@ fn php_const_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 { // BEGIN DOCS FROM module.md /// # `#[php_module]` Attribute /// -/// The module macro is used to annotate the `get_module` function, which is used by -/// the PHP interpreter to retrieve information about your extension, including the -/// name, version, functions and extra initialization functions. Regardless if you -/// use this macro, your extension requires a `extern "C" fn get_module()` so that -/// PHP can get this information. +/// The module macro is used to annotate the `get_module` function, which is +/// used by the PHP interpreter to retrieve information about your extension, +/// including the name, version, functions and extra initialization functions. +/// Regardless if you use this macro, your extension requires a `extern "C" fn +/// get_module()` so that PHP can get this information. /// /// The function is renamed to `get_module` if you have used another name. The -/// function is passed an instance of `ModuleBuilder` which allows you to register -/// the following (if required): +/// function is passed an instance of `ModuleBuilder` which allows you to +/// register the following (if required): /// /// - Functions, classes, and constants /// - Extension and request startup and shutdown functions. -/// - Read more about the PHP extension lifecycle -/// [here](https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html). +/// - Read more about the PHP extension lifecycle [here](https://www.phpinternalsbook.com/php7/extensions_design/php_lifecycle.html). /// - PHP extension information function -/// - Used by the `phpinfo()` function to get information about your extension. +/// - Used by the `phpinfo()` function to get information about your +/// extension. /// -/// Classes and constants are not registered with PHP in the `get_module` function. These are -/// registered inside the extension startup function. +/// Classes and constants are not registered with PHP in the `get_module` +/// function. These are registered inside the extension startup function. /// /// ## Usage /// @@ -651,28 +755,32 @@ fn php_module_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 // BEGIN DOCS FROM impl.md /// # `#[php_impl]` Attribute /// -/// You can export an entire `impl` block to PHP. This exports all methods as well -/// as constants to PHP on the class that it is implemented on. This requires the -/// `#[php_class]` macro to already be used on the underlying struct. Trait -/// implementations cannot be exported to PHP. Only one `impl` block can be exported -/// per class. +/// You can export an entire `impl` block to PHP. This exports all methods as +/// well as constants to PHP on the class that it is implemented on. This +/// requires the `#[php_class]` macro to already be used on the underlying +/// struct. Trait implementations cannot be exported to PHP. Only one `impl` +/// block can be exported per class. /// -/// If you do not want a function exported to PHP, you should place it in a separate -/// `impl` block. +/// If you do not want a function exported to PHP, you should place it in a +/// separate `impl` block. /// -/// If you want to use async Rust, use `#[php_async_impl]`, instead: see [here »](./async_impl.md) for more info. +/// If you want to use async Rust, use `#[php_async_impl]`, instead: see [here +/// »](./async_impl.md) for more info. /// /// ## Options /// -/// By default all constants are renamed to `UPPER_CASE` and all methods are renamed to -/// camelCase. This can be changed by passing the `change_method_case` and -/// `change_constant_case` as `#[php]` attributes on the `impl` block. The options are: +/// By default all constants are renamed to `UPPER_CASE` and all methods are +/// renamed to camelCase. This can be changed by passing the +/// `change_method_case` and `change_constant_case` as `#[php]` attributes on +/// the `impl` block. The options are: /// -/// - `#[php(change_method_case = "snake_case")]` - Renames the method to snake case. -/// - `#[php(change_constant_case = "snake_case")]` - Renames the constant to snake case. +/// - `#[php(change_method_case = "snake_case")]` - Renames the method to snake +/// case. +/// - `#[php(change_constant_case = "snake_case")]` - Renames the constant to +/// snake case. /// -/// See the [`name` and `change_case`](./php.md#name-and-change_case) section for a list of all -/// available cases. +/// See the [`name` and `change_case`](./php.md#name-and-change_case) section +/// for a list of all available cases. /// /// ## Methods /// @@ -680,74 +788,79 @@ fn php_module_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 /// [`php_function`] macro first. The primary difference between functions and /// methods is they are bounded by their class object. /// -/// Class methods can take a `&self` or `&mut self` parameter. They cannot take a -/// consuming `self` parameter. Static methods can omit this `self` parameter. +/// Class methods can take a `&self` or `&mut self` parameter. They cannot take +/// a consuming `self` parameter. Static methods can omit this `self` parameter. /// /// To access the underlying Zend object, you can take a reference to a -/// `ZendClassObject` in place of the self parameter, where the parameter must -/// be named `self_`. This can also be used to return a reference to `$this`. +/// `ZendClassObject` in place of the self parameter, where the parameter +/// must be named `self_`. This can also be used to return a reference to +/// `$this`. /// /// The rest of the options are passed as separate attributes: /// -/// - `#[php(defaults(i = 5, b = "hello"))]` - Sets the default value for parameter(s). -/// - `#[php(optional = i)]` - Sets the first optional parameter. Note that this also sets -/// the remaining parameters as optional, so all optional parameters must be a -/// variant of `Option`. -/// - `#[php(vis = "public")]`, `#[php(vis = "protected")]` and `#[php(vis = "private")]` - Sets the visibility of the -/// method. -/// - `#[php(name = "method_name")]` - Renames the PHP method to a different identifier, -/// without renaming the Rust method name. +/// - `#[php(defaults(i = 5, b = "hello"))]` - Sets the default value for +/// parameter(s). +/// - `#[php(optional = i)]` - Sets the first optional parameter. Note that this +/// also sets the remaining parameters as optional, so all optional parameters +/// must be a variant of `Option`. +/// - `#[php(vis = "public")]`, `#[php(vis = "protected")]` and `#[php(vis = +/// "private")]` - Sets the visibility of the method. +/// - `#[php(name = "method_name")]` - Renames the PHP method to a different +/// identifier, without renaming the Rust method name. /// -/// The `#[php(defaults)]` and `#[php(optional)]` attributes operate the same as the -/// equivalent function attribute parameters. +/// The `#[php(defaults)]` and `#[php(optional)]` attributes operate the same as +/// the equivalent function attribute parameters. /// /// ### Constructors /// -/// By default, if a class does not have a constructor, it is not constructable from -/// PHP. It can only be returned from a Rust function to PHP. +/// By default, if a class does not have a constructor, it is not constructable +/// from PHP. It can only be returned from a Rust function to PHP. /// /// Constructors are Rust methods which can take any amount of parameters and -/// returns either `Self` or `Result`, where `E: Into`. When -/// the error variant of `Result` is encountered, it is thrown as an exception and -/// the class is not constructed. +/// returns either `Self` or `Result`, where `E: Into`. +/// When the error variant of `Result` is encountered, it is thrown as an +/// exception and the class is not constructed. /// /// Constructors are designated by either naming the method `__construct` or by -/// annotating a method with the `#[php(constructor)]` attribute. Note that when using -/// the attribute, the function is not exported to PHP like a regular method. +/// annotating a method with the `#[php(constructor)]` attribute. Note that when +/// using the attribute, the function is not exported to PHP like a regular +/// method. /// /// Constructors cannot use the visibility or rename attributes listed above. /// /// ## Constants /// -/// Constants are defined as regular Rust `impl` constants. Any type that implements -/// `IntoZval` can be used as a constant. Constant visibility is not supported at -/// the moment, and therefore no attributes are valid on constants. +/// Constants are defined as regular Rust `impl` constants. Any type that +/// implements `IntoZval` can be used as a constant. Constant visibility is not +/// supported at the moment, and therefore no attributes are valid on constants. /// /// ## Property getters and setters /// /// You can add properties to classes which use Rust functions as getters and/or -/// setters. This is done with the `#[php(getter)]` and `#[php(setter)]` attributes. By -/// default, the `get_` or `set_` prefix is trimmed from the start of the function -/// name, and the remainder is used as the property name. +/// setters. This is done with the `#[php(getter)]` and `#[php(setter)]` +/// attributes. By default, the `get_` or `set_` prefix is trimmed from the +/// start of the function name, and the remainder is used as the property name. /// -/// If you want to use a different name for the property, you can pass a `name` or -/// `change_case` option to the `#[php]` attribute which will change the property name. +/// If you want to use a different name for the property, you can pass a `name` +/// or `change_case` option to the `#[php]` attribute which will change the +/// property name. /// -/// Properties do not necessarily have to have both a getter and a setter, if the -/// property is immutable the setter can be omitted, and vice versa for getters. +/// Properties do not necessarily have to have both a getter and a setter, if +/// the property is immutable the setter can be omitted, and vice versa for +/// getters. /// -/// The `#[php(getter)]` and `#[php(setter)]` attributes are mutually exclusive on methods. -/// Properties cannot have multiple getters or setters, and the property name cannot -/// conflict with field properties defined on the struct. +/// The `#[php(getter)]` and `#[php(setter)]` attributes are mutually exclusive +/// on methods. Properties cannot have multiple getters or setters, and the +/// property name cannot conflict with field properties defined on the struct. /// /// As the same as field properties, method property types must implement both /// `IntoZval` and `FromZval`. /// /// ## Example /// -/// Continuing on from our `Human` example in the structs section, we will define a -/// constructor, as well as getters for the properties. We will also define a -/// constant for the maximum age of a `Human`. +/// Continuing on from our `Human` example in the structs section, we will +/// define a constructor, as well as getters for the properties. We will also +/// define a constant for the maximum age of a `Human`. /// /// ```rust,no_run,ignore /// # #![cfg_attr(windows, feature(abi_vectorcall))] @@ -919,16 +1032,16 @@ fn php_extern_internal(_: TokenStream2, input: TokenStream2) -> TokenStream2 { // BEGIN DOCS FROM zval_convert.md /// # `ZvalConvert` Derive Macro /// -/// The `#[derive(ZvalConvert)]` macro derives the `FromZval` and `IntoZval` traits -/// on a struct or enum. +/// The `#[derive(ZvalConvert)]` macro derives the `FromZval` and `IntoZval` +/// traits on a struct or enum. /// /// ## Structs /// -/// When used on a struct, the `FromZendObject` and `IntoZendObject` traits are also -/// implemented, mapping fields to properties in both directions. All fields on the -/// struct must implement `FromZval` as well. Generics are allowed on structs that -/// use the derive macro, however, the implementation will add a `FromZval` bound to -/// all generics types. +/// When used on a struct, the `FromZendObject` and `IntoZendObject` traits are +/// also implemented, mapping fields to properties in both directions. All +/// fields on the struct must implement `FromZval` as well. Generics are allowed +/// on structs that use the derive macro, however, the implementation will add a +/// `FromZval` bound to all generics types. /// /// ### Examples /// @@ -1011,18 +1124,18 @@ fn php_extern_internal(_: TokenStream2, input: TokenStream2) -> TokenStream2 { /// ## Enums /// /// When used on an enum, the `FromZval` implementation will treat the enum as a -/// tagged union with a mixed datatype. This allows you to accept multiple types in -/// a parameter, for example, a string and an integer. +/// tagged union with a mixed datatype. This allows you to accept multiple types +/// in a parameter, for example, a string and an integer. /// -/// The enum variants must not have named fields, and each variant must have exactly -/// one field (the type to extract from the zval). Optionally, the enum may have one -/// default variant with no data contained, which will be used when the rest of the -/// variants could not be extracted from the zval. +/// The enum variants must not have named fields, and each variant must have +/// exactly one field (the type to extract from the zval). Optionally, the enum +/// may have one default variant with no data contained, which will be used when +/// the rest of the variants could not be extracted from the zval. /// /// The ordering of the variants in the enum is important, as the `FromZval` -/// implementation will attempt to parse the zval data in order. For example, if you -/// put a `String` variant before an integer variant, the integer would be converted -/// to a string and passed as the string variant. +/// implementation will attempt to parse the zval data in order. For example, if +/// you put a `String` variant before an integer variant, the integer would be +/// converted to a string and passed as the string variant. /// /// ### Examples /// @@ -1237,6 +1350,7 @@ mod tests { ("php_class", php_class_internal as AttributeFn), ("php_const", php_const_internal as AttributeFn), ("php_enum", php_enum_internal as AttributeFn), + ("php_interface", php_interface_internal as AttributeFn), ("php_extern", php_extern_internal as AttributeFn), ("php_function", php_function_internal as AttributeFn), ("php_impl", php_impl_internal as AttributeFn), diff --git a/crates/macros/tests/expand/class.expanded.rs b/crates/macros/tests/expand/class.expanded.rs new file mode 100644 index 000000000..f08ca30a6 --- /dev/null +++ b/crates/macros/tests/expand/class.expanded.rs @@ -0,0 +1,59 @@ +#[macro_use] +extern crate ext_php_rs_derive; +/// Doc comments for MyClass. +/// This is a basic class example. +pub struct MyClass {} +impl ::ext_php_rs::class::RegisteredClass for MyClass { + const CLASS_NAME: &'static str = "MyClass"; + const BUILDER_MODIFIER: ::std::option::Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = ::std::option::Option::None; + const EXTENDS: ::std::option::Option<::ext_php_rs::class::ClassEntryInfo> = None; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::empty(); + const DOC_COMMENTS: &'static [&'static str] = &[ + " Doc comments for MyClass.", + " This is a basic class example.", + ]; + #[inline] + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata = ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, + ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, + > { + use ::std::iter::FromIterator; + ::std::collections::HashMap::from_iter([]) + } + #[inline] + fn method_builders() -> ::std::vec::Vec< + ( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + ), + > { + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default() + .get_methods() + } + #[inline] + fn constructor() -> ::std::option::Option< + ::ext_php_rs::class::ConstructorMeta, + > { + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default() + .get_constructor() + } + #[inline] + fn constants() -> &'static [( + &'static str, + &'static dyn ::ext_php_rs::convert::IntoZvalDyn, + &'static [&'static str], + )] { + use ::ext_php_rs::internal::class::PhpClassImpl; + ::ext_php_rs::internal::class::PhpClassImplCollector::::default() + .get_constants() + } +} diff --git a/crates/macros/tests/expand/class.rs b/crates/macros/tests/expand/class.rs new file mode 100644 index 000000000..e8c8b5429 --- /dev/null +++ b/crates/macros/tests/expand/class.rs @@ -0,0 +1,7 @@ +#[macro_use] +extern crate ext_php_rs_derive; + +/// Doc comments for MyClass. +/// This is a basic class example. +#[php_class] +pub struct MyClass {} diff --git a/crates/macros/tests/expand/interface.expanded.rs b/crates/macros/tests/expand/interface.expanded.rs new file mode 100644 index 000000000..6a909135b --- /dev/null +++ b/crates/macros/tests/expand/interface.expanded.rs @@ -0,0 +1,321 @@ +#[macro_use] +extern crate ext_php_rs_derive; +/// Doc comments for MyInterface. +/// This is a basic interface example. +trait MyInterface { + /// Doc comments for MY_CONST. + const MY_CONST: i32 = 42; + /// Doc comments for my_method. + /// This method does something. + fn my_method(&self, arg: i32) -> String; +} +pub struct PhpInterfaceMyInterface; +impl ::ext_php_rs::class::RegisteredClass for PhpInterfaceMyInterface { + const CLASS_NAME: &'static str = "MyInterface"; + const BUILDER_MODIFIER: Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const DOC_COMMENTS: &'static [&'static str] = &[ + " Doc comments for MyInterface.", + " This is a basic interface example.", + ]; + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata = ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + fn method_builders() -> Vec< + ( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + ), + > { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + ( + ::ext_php_rs::builders::FunctionBuilder::new_abstract("myMethod") + .arg( + ::ext_php_rs::args::Arg::new( + "arg", + ::TYPE, + ), + ) + .not_required() + .returns( + ::TYPE, + false, + ::NULLABLE, + ) + .docs( + &[ + " Doc comments for my_method.", + " This method does something.", + ], + ), + ::ext_php_rs::flags::MethodFlags::Public + | ::ext_php_rs::flags::MethodFlags::Abstract, + ), + ]), + ) + } + fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { + None + } + fn constants() -> &'static [( + &'static str, + &'static dyn ext_php_rs::convert::IntoZvalDyn, + ext_php_rs::describe::DocComments, + )] { + &[("MY_CONST", &42, &[" Doc comments for MY_CONST."])] + } + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, + ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, + > { + { + ::core::panicking::panic_fmt(format_args!("Not supported for Interface")); + }; + } +} +impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a PhpInterfaceMyInterface { + #[inline] + fn from_zend_object( + obj: &'a ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::< + PhpInterfaceMyInterface, + >::from_zend_obj(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&**obj) + } +} +impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> +for &'a mut PhpInterfaceMyInterface { + #[inline] + fn from_zend_object_mut( + obj: &'a mut ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::< + PhpInterfaceMyInterface, + >::from_zend_obj_mut(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&mut **obj) + } +} +impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a PhpInterfaceMyInterface { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + #[inline] + fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object(zval.object()?) + .ok() + } +} +impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> for &'a mut PhpInterfaceMyInterface { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + #[inline] + fn from_zval_mut( + zval: &'a mut ::ext_php_rs::types::Zval, + ) -> ::std::option::Option { + ::from_zend_object_mut( + zval.object_mut()?, + ) + .ok() + } +} +impl ::ext_php_rs::convert::IntoZendObject for PhpInterfaceMyInterface { + #[inline] + fn into_zend_object( + self, + ) -> ::ext_php_rs::error::Result< + ::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>, + > { + Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) + } +} +impl ::ext_php_rs::convert::IntoZval for PhpInterfaceMyInterface { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + const NULLABLE: bool = false; + #[inline] + fn set_zval( + self, + zv: &mut ::ext_php_rs::types::Zval, + persistent: bool, + ) -> ::ext_php_rs::error::Result<()> { + use ::ext_php_rs::convert::IntoZendObject; + self.into_zend_object()?.set_zval(zv, persistent) + } +} +trait MyInterface2 { + const MY_CONST: i32 = 42; + const ANOTHER_CONST: &'static str = "Hello"; + fn my_method(&self, arg: i32) -> String; + fn anotherMethod(&self) -> i32; +} +pub struct PhpInterfaceMyInterface2; +impl ::ext_php_rs::class::RegisteredClass for PhpInterfaceMyInterface2 { + const CLASS_NAME: &'static str = "MyInterface2"; + const BUILDER_MODIFIER: Option< + fn(::ext_php_rs::builders::ClassBuilder) -> ::ext_php_rs::builders::ClassBuilder, + > = None; + const EXTENDS: Option<::ext_php_rs::class::ClassEntryInfo> = None; + const FLAGS: ::ext_php_rs::flags::ClassFlags = ::ext_php_rs::flags::ClassFlags::Interface; + const IMPLEMENTS: &'static [::ext_php_rs::class::ClassEntryInfo] = &[]; + const DOC_COMMENTS: &'static [&'static str] = &[]; + fn get_metadata() -> &'static ::ext_php_rs::class::ClassMetadata { + static METADATA: ::ext_php_rs::class::ClassMetadata = ::ext_php_rs::class::ClassMetadata::new(); + &METADATA + } + fn method_builders() -> Vec< + ( + ::ext_php_rs::builders::FunctionBuilder<'static>, + ::ext_php_rs::flags::MethodFlags, + ), + > { + <[_]>::into_vec( + ::alloc::boxed::box_new([ + ( + ::ext_php_rs::builders::FunctionBuilder::new_abstract("MY_METHOD") + .arg( + ::ext_php_rs::args::Arg::new( + "arg", + ::TYPE, + ), + ) + .not_required() + .returns( + ::TYPE, + false, + ::NULLABLE, + ), + ::ext_php_rs::flags::MethodFlags::Public + | ::ext_php_rs::flags::MethodFlags::Abstract, + ), + ( + ::ext_php_rs::builders::FunctionBuilder::new_abstract( + "AnotherMethod", + ) + .not_required() + .returns( + ::TYPE, + false, + ::NULLABLE, + ), + ::ext_php_rs::flags::MethodFlags::Public + | ::ext_php_rs::flags::MethodFlags::Abstract, + ), + ]), + ) + } + fn constructor() -> Option<::ext_php_rs::class::ConstructorMeta> { + None + } + fn constants() -> &'static [( + &'static str, + &'static dyn ext_php_rs::convert::IntoZvalDyn, + ext_php_rs::describe::DocComments, + )] { + &[("my_const", &42, &[]), ("AnotherConst", &"Hello", &[])] + } + fn get_properties<'a>() -> ::std::collections::HashMap< + &'static str, + ::ext_php_rs::internal::property::PropertyInfo<'a, Self>, + > { + { + ::core::panicking::panic_fmt(format_args!("Not supported for Interface")); + }; + } +} +impl<'a> ::ext_php_rs::convert::FromZendObject<'a> for &'a PhpInterfaceMyInterface2 { + #[inline] + fn from_zend_object( + obj: &'a ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::< + PhpInterfaceMyInterface2, + >::from_zend_obj(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&**obj) + } +} +impl<'a> ::ext_php_rs::convert::FromZendObjectMut<'a> +for &'a mut PhpInterfaceMyInterface2 { + #[inline] + fn from_zend_object_mut( + obj: &'a mut ::ext_php_rs::types::ZendObject, + ) -> ::ext_php_rs::error::Result { + let obj = ::ext_php_rs::types::ZendClassObject::< + PhpInterfaceMyInterface2, + >::from_zend_obj_mut(obj) + .ok_or(::ext_php_rs::error::Error::InvalidScope)?; + Ok(&mut **obj) + } +} +impl<'a> ::ext_php_rs::convert::FromZval<'a> for &'a PhpInterfaceMyInterface2 { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + #[inline] + fn from_zval(zval: &'a ::ext_php_rs::types::Zval) -> ::std::option::Option { + ::from_zend_object(zval.object()?) + .ok() + } +} +impl<'a> ::ext_php_rs::convert::FromZvalMut<'a> for &'a mut PhpInterfaceMyInterface2 { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + #[inline] + fn from_zval_mut( + zval: &'a mut ::ext_php_rs::types::Zval, + ) -> ::std::option::Option { + ::from_zend_object_mut( + zval.object_mut()?, + ) + .ok() + } +} +impl ::ext_php_rs::convert::IntoZendObject for PhpInterfaceMyInterface2 { + #[inline] + fn into_zend_object( + self, + ) -> ::ext_php_rs::error::Result< + ::ext_php_rs::boxed::ZBox<::ext_php_rs::types::ZendObject>, + > { + Ok(::ext_php_rs::types::ZendClassObject::new(self).into()) + } +} +impl ::ext_php_rs::convert::IntoZval for PhpInterfaceMyInterface2 { + const TYPE: ::ext_php_rs::flags::DataType = ::ext_php_rs::flags::DataType::Object( + Some( + ::CLASS_NAME, + ), + ); + const NULLABLE: bool = false; + #[inline] + fn set_zval( + self, + zv: &mut ::ext_php_rs::types::Zval, + persistent: bool, + ) -> ::ext_php_rs::error::Result<()> { + use ::ext_php_rs::convert::IntoZendObject; + self.into_zend_object()?.set_zval(zv, persistent) + } +} diff --git a/crates/macros/tests/expand/interface.rs b/crates/macros/tests/expand/interface.rs new file mode 100644 index 000000000..55febeacb --- /dev/null +++ b/crates/macros/tests/expand/interface.rs @@ -0,0 +1,25 @@ +#[macro_use] +extern crate ext_php_rs_derive; + +/// Doc comments for MyInterface. +/// This is a basic interface example. +#[php_interface] +trait MyInterface { + /// Doc comments for MY_CONST. + const MY_CONST: i32 = 42; + /// Doc comments for my_method. + /// This method does something. + fn my_method(&self, arg: i32) -> String; +} + +#[php_interface] +#[php(change_method_case = "UPPER_CASE")] +#[php(change_constant_case = "snake_case")] +trait MyInterface2 { + const MY_CONST: i32 = 42; + #[php(change_case = "PascalCase")] + const ANOTHER_CONST: &'static str = "Hello"; + fn my_method(&self, arg: i32) -> String; + #[php(change_case = "PascalCase")] + fn anotherMethod(&self) -> i32; +} diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index 36f49ef17..73a514b4c 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -2004,6 +2004,11 @@ unsafe extern "C" { parent_ce: *mut zend_class_entry, ) -> *mut zend_class_entry; } +unsafe extern "C" { + pub fn zend_register_internal_interface( + orig_class_entry: *mut zend_class_entry, + ) -> *mut zend_class_entry; +} unsafe extern "C" { pub fn zend_is_callable( callable: *mut zval, diff --git a/flake.lock b/flake.lock index fd49aa685..594401cd2 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1759036355, - "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "lastModified": 1762977756, + "narHash": "sha256-4PqRErxfe+2toFJFgcRKZ0UI9NSIOJa+7RXVtBhy4KE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "rev": "c5ae371f1a6a7fd27823bc500d9390b38c05fa55", "type": "github" }, "original": { @@ -29,11 +29,11 @@ ] }, "locked": { - "lastModified": 1759199574, - "narHash": "sha256-w24RYly3VSVKp98rVfCI1nFYfQ0VoWmShtKPCbXgK6A=", + "lastModified": 1763087910, + "narHash": "sha256-eB9Z1mWd1U6N61+F8qwDggX0ihM55s4E0CluwNukJRU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "381776b12d0d125edd7c1930c2041a1471e586c0", + "rev": "cf4a68749733d45c0420726596367acd708eb2e8", "type": "github" }, "original": { diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index d5e18495c..b3fa7fc6b 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -27,6 +27,7 @@ - [Macros](./macros/index.md) - [Module](./macros/module.md) - [Function](./macros/function.md) + - [Interfaces](./macros/interface.md) - [Classes](./macros/classes.md) - [`impl`s](./macros/impl.md) - [Constants](./macros/constant.md) diff --git a/guide/src/macros/index.md b/guide/src/macros/index.md index 9fdcd56a9..c45127581 100644 --- a/guide/src/macros/index.md +++ b/guide/src/macros/index.md @@ -14,6 +14,7 @@ used from PHP without fiddling around with zvals. - [`php_const`] - Used to export a Rust constant to PHP as a global constant. - [`php_extern`] - Attribute used to annotate `extern` blocks which are deemed as PHP functions. +- [`php_interface`] - Attribute used to export Rust Trait as PHP interface - [`php`] - Used to modify the default behavior of the above macros. This is a generic attribute that can be used on most of the above macros. @@ -23,4 +24,5 @@ used from PHP without fiddling around with zvals. [`php_impl`]: ./impl.md [`php_const`]: ./constant.md [`php_extern`]: ./extern.md +[`php_interface`]: ./interface.md [`php`]: ./php.md diff --git a/guide/src/macros/interface.md b/guide/src/macros/interface.md new file mode 100644 index 000000000..6ca88898f --- /dev/null +++ b/guide/src/macros/interface.md @@ -0,0 +1,73 @@ +# `#[php_interface]` Attribute + +You can export a `Trait` block to PHP. This exports all methods as well as +constants to PHP on the interface. Trait method SHOULD NOT contain default +implementations, as these are not supported in PHP interfaces. + +## Options + +By default all constants are renamed to `UPPER_CASE` and all methods are renamed to +`camelCase`. This can be changed by passing the `change_method_case` and +`change_constant_case` as `#[php]` attributes on the `impl` block. The options are: + +- `#[php(change_method_case = "snake_case")]` - Renames the method to snake case. +- `#[php(change_constant_case = "snake_case")]` - Renames the constant to snake case. + +See the [`name` and `change_case`](./php.md#name-and-change_case) section for a list of all +available cases. + +## Methods + +See the [`php_impl`](./impl.md#) + +## Constants + +See the [`php_impl`](./impl.md#) + +## Example + +Define an example trait with methods and constant: + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +use ext_php_rs::{prelude::*, types::ZendClassObject}; + + +#[php_interface] +#[php(name = "Rust\\TestInterface")] +trait Test { + const TEST: &'static str = "TEST"; + + fn co(); + + #[php(defaults(value = 0))] + fn set_value(&mut self, value: i32); +} + +#[php_module] +pub fn module(module: ModuleBuilder) -> ModuleBuilder { + module + .interface::() +} + +# fn main() {} +``` + +Using our newly created interface in PHP: + +```php + u32 { + self.ce.ce_flags + } + /// Sets the class builder to extend another class. /// /// # Parameters @@ -238,16 +244,26 @@ impl ClassBuilder { "Class name in builder does not match class name in `impl RegisteredClass`." ); self.object_override = Some(create_object::); + let is_interface = T::FLAGS.contains(ClassFlags::Interface); let (func, visibility) = if let Some(ConstructorMeta { build_fn, flags, .. }) = T::constructor() { - let func = FunctionBuilder::new("__construct", constructor::); + let func = if is_interface { + FunctionBuilder::new_abstract("__construct") + } else { + FunctionBuilder::new("__construct", constructor::) + }; + (build_fn(func), flags.unwrap_or(MethodFlags::Public)) } else { ( - FunctionBuilder::new("__construct", constructor::), + if is_interface { + FunctionBuilder::new_abstract("__construct") + } else { + FunctionBuilder::new("__construct", constructor::) + }, MethodFlags::Public, ) }; @@ -306,16 +322,24 @@ impl ClassBuilder { let func = Box::into_raw(methods.into_boxed_slice()) as *const FunctionEntry; self.ce.info.internal.builtin_functions = func; - let class = unsafe { - zend_register_internal_class_ex( - &raw mut self.ce, - match self.extends { - Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(), - None => std::ptr::null_mut(), - }, - ) - .as_mut() - .ok_or(Error::InvalidPointer)? + let class = if self.ce.flags().contains(ClassFlags::Interface) { + unsafe { + zend_register_internal_interface(&raw mut self.ce) + .as_mut() + .ok_or(Error::InvalidPointer)? + } + } else { + unsafe { + zend_register_internal_class_ex( + &raw mut self.ce, + match self.extends { + Some((ptr, _)) => ptr::from_ref(ptr()).cast_mut(), + None => std::ptr::null_mut(), + }, + ) + .as_mut() + .ok_or(Error::InvalidPointer)? + } }; // disable serialization if the class has an associated object @@ -467,6 +491,14 @@ mod tests { assert!(class.register.is_some()); } + #[test] + fn test_registration_interface() { + let class = ClassBuilder::new("Foo") + .flags(ClassFlags::Interface) + .registration(|_| {}); + assert!(class.register.is_some()); + } + #[test] fn test_docs() { let class = ClassBuilder::new("Foo").docs(&["Doc 1"]); diff --git a/src/builders/module.rs b/src/builders/module.rs index bdb258f79..3f8e8f8fb 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -8,6 +8,7 @@ use crate::{ describe::DocComments, error::Result, ffi::{ZEND_MODULE_API_NO, ext_php_rs_php_build_id}, + flags::ClassFlags, zend::{FunctionEntry, ModuleEntry}, }; #[cfg(feature = "enum")] @@ -48,6 +49,7 @@ pub struct ModuleBuilder<'a> { pub(crate) functions: Vec>, pub(crate) constants: Vec<(String, Box, DocComments)>, pub(crate) classes: Vec ClassBuilder>, + pub(crate) interfaces: Vec ClassBuilder>, #[cfg(feature = "enum")] pub(crate) enums: Vec EnumBuilder>, startup_func: Option, @@ -192,6 +194,41 @@ impl ModuleBuilder<'_> { self } + /// Adds a interface to the extension. + /// + /// # Panics + /// + /// * Panics if a constant could not be registered. + pub fn interface(mut self) -> Self { + self.interfaces.push(|| { + let mut builder = ClassBuilder::new(T::CLASS_NAME); + for (method, flags) in T::method_builders() { + builder = builder.method(method, flags); + } + for interface in T::IMPLEMENTS { + builder = builder.implements(*interface); + } + for (name, value, docs) in T::constants() { + builder = builder + .dyn_constant(*name, *value, docs) + .expect("Failed to register constant"); + } + + if let Some(modifier) = T::BUILDER_MODIFIER { + builder = modifier(builder); + } + + builder = builder.flags(ClassFlags::Interface); + builder + .object_override::() + .registration(|ce| { + T::get_metadata().set_ce(ce); + }) + .docs(T::DOC_COMMENTS) + }); + self + } + /// Adds a class to the extension. /// /// # Panics @@ -262,6 +299,7 @@ impl ModuleBuilder<'_> { pub struct ModuleStartup { constants: Vec<(String, Box)>, classes: Vec ClassBuilder>, + interfaces: Vec ClassBuilder>, #[cfg(feature = "enum")] enums: Vec EnumBuilder>, } @@ -286,6 +324,10 @@ impl ModuleStartup { c.register().expect("Failed to build class"); }); + self.interfaces.into_iter().map(|c| c()).for_each(|c| { + c.register().expect("Failed to build interface"); + }); + #[cfg(feature = "enum")] self.enums .into_iter() @@ -328,6 +370,7 @@ impl TryFrom> for (ModuleEntry, ModuleStartup) { .map(|(n, v, _)| (n, v)) .collect(), classes: builder.classes, + interfaces: builder.interfaces, #[cfg(feature = "enum")] enums: builder.enums, }; @@ -383,6 +426,7 @@ mod tests { assert!(builder.functions.is_empty()); assert!(builder.constants.is_empty()); assert!(builder.classes.is_empty()); + assert!(builder.interfaces.is_empty()); assert!(builder.startup_func.is_none()); assert!(builder.shutdown_func.is_none()); assert!(builder.request_startup_func.is_none()); diff --git a/src/describe/mod.rs b/src/describe/mod.rs index 82238f0f5..bb361d313 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -193,6 +193,8 @@ pub struct Class { pub methods: Vec, /// Constants of the class. pub constants: Vec, + /// Class flags + pub flags: u32, } #[cfg(feature = "closure")] @@ -225,15 +227,18 @@ impl Class { }), r#static: false, visibility: Visibility::Public, + r#abstract: false, }] .into(), constants: StdVec::new().into(), + flags: 0, } } } impl From for Class { fn from(val: ClassBuilder) -> Self { + let flags = val.get_flags(); Self { name: val.name.into(), docs: DocBlock( @@ -269,6 +274,7 @@ impl From for Class { .map(Constant::from) .collect::>() .into(), + flags, } } } @@ -416,6 +422,8 @@ pub struct Method { pub r#static: bool, /// Visibility of the method. pub visibility: Visibility, + /// Not describe method body, if is abstract. + pub r#abstract: bool, } impl From<(FunctionBuilder<'_>, MethodFlags)> for Method { @@ -448,6 +456,7 @@ impl From<(FunctionBuilder<'_>, MethodFlags)> for Method { ty: flags.into(), r#static: flags.contains(MethodFlags::Static), visibility: flags.into(), + r#abstract: flags.contains(MethodFlags::Abstract), } } } @@ -685,6 +694,7 @@ mod tests { retval: Option::None, r#static: false, visibility: Visibility::Protected, + r#abstract: false } ); } diff --git a/src/describe/stub.rs b/src/describe/stub.rs index ca3404ee4..b204145e0 100644 --- a/src/describe/stub.rs +++ b/src/describe/stub.rs @@ -16,7 +16,7 @@ use super::{ #[cfg(feature = "enum")] use crate::describe::{Enum, EnumCase}; -use crate::flags::DataType; +use crate::flags::{ClassFlags, DataType}; /// Implemented on types which can be converted into PHP stubs. pub trait ToStub { @@ -226,13 +226,20 @@ impl ToStub for Class { self.docs.fmt_stub(buf)?; let (_, name) = split_namespace(self.name.as_ref()); - write!(buf, "class {name} ")?; + let flags = ClassFlags::from_bits(self.flags).unwrap_or(ClassFlags::empty()); + let is_interface = flags.contains(ClassFlags::Interface); + + if is_interface { + write!(buf, "interface {name} ")?; + } else { + write!(buf, "class {name} ")?; + } if let Option::Some(extends) = &self.extends { write!(buf, "extends {extends} ")?; } - if !self.implements.is_empty() { + if !self.implements.is_empty() && !is_interface { write!( buf, "implements {} ", @@ -244,6 +251,18 @@ impl ToStub for Class { )?; } + if !self.implements.is_empty() && is_interface { + write!( + buf, + "extends {} ", + self.implements + .iter() + .map(RString::as_str) + .collect::>() + .join(", ") + )?; + } + writeln!(buf, "{{")?; buf.push_str( @@ -360,7 +379,11 @@ impl ToStub for Method { retval.ty.fmt_stub(buf)?; } - writeln!(buf, " {{}}") + if self.r#abstract { + writeln!(buf, ";") + } else { + writeln!(buf, " {{}}") + } } } diff --git a/src/lib.rs b/src/lib.rs index 0ee026fed..3da2710b0 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_module, - wrap_constant, wrap_function, zend_fastcall, + ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, 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_module, - wrap_constant, wrap_function, zend_fastcall, + ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_interface, + php_module, wrap_constant, wrap_function, zend_fastcall, }; diff --git a/tests/src/integration/interface/interface.php b/tests/src/integration/interface/interface.php new file mode 100644 index 000000000..1266bc4e9 --- /dev/null +++ b/tests/src/integration/interface/interface.php @@ -0,0 +1,38 @@ +nonStatic($data), $other->nonStatic($data)); + } + + public function setValue(?int $value = 0) { + + } +} +$f = new Test(); + +assert(is_a($f, Throwable::class)); +assert($f->nonStatic('Rust') === 'Rust - TEST'); +assert($f->refToLikeThisClass('TEST', $f) === 'TEST - TEST | TEST - TEST'); +assert(ExtPhpRs\Interface\EmptyObjectInterface::STRING_CONST === 'STRING_CONST'); +assert(ExtPhpRs\Interface\EmptyObjectInterface::USIZE_CONST === 200); diff --git a/tests/src/integration/interface/mod.rs b/tests/src/integration/interface/mod.rs new file mode 100644 index 000000000..620e5ac4a --- /dev/null +++ b/tests/src/integration/interface/mod.rs @@ -0,0 +1,39 @@ +use ext_php_rs::php_interface; +use ext_php_rs::prelude::ModuleBuilder; +use ext_php_rs::types::ZendClassObject; +use ext_php_rs::zend::ce; + +#[php_interface] +#[php(extends(ce = ce::throwable, stub = "\\Throwable"))] +#[php(name = "ExtPhpRs\\Interface\\EmptyObjectInterface")] +#[allow(dead_code)] +pub trait EmptyObjectTrait { + const STRING_CONST: &'static str = "STRING_CONST"; + + const USIZE_CONST: u64 = 200; + + fn void(); + + fn non_static(&self, data: String) -> String; + + fn ref_to_like_this_class( + &self, + data: String, + other: &ZendClassObject, + ) -> String; + + #[php(defaults(value = 0))] + fn set_value(&mut self, value: i32); +} + +pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { + builder.interface::() +} + +#[cfg(test)] +mod tests { + #[test] + fn interface_work() { + assert!(crate::integration::test::run_php("interface/interface.php")); + } +} diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index ab0980b8b..60acb2958 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -9,6 +9,7 @@ pub mod defaults; pub mod enum_; pub mod exception; pub mod globals; +pub mod interface; pub mod iterator; pub mod magic_method; pub mod nullable; diff --git a/tests/src/lib.rs b/tests/src/lib.rs index ced895ded..3b7b2141d 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -31,6 +31,7 @@ pub fn build_module(module: ModuleBuilder) -> ModuleBuilder { module = integration::object::build_module(module); module = integration::string::build_module(module); module = integration::variadic_args::build_module(module); + module = integration::interface::build_module(module); module } diff --git a/tools/update_lib_docs.sh b/tools/update_lib_docs.sh index b8e794dac..9c23ac61d 100755 --- a/tools/update_lib_docs.sh +++ b/tools/update_lib_docs.sh @@ -51,6 +51,7 @@ update_docs "impl" update_docs "module" update_docs "zval_convert" update_docs "enum" +update_docs "interface" # Format to remove trailing whitespace rustup run nightly rustfmt crates/macros/src/lib.rs