diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index b808e2b22..d24f61fe6 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -308,7 +308,11 @@ impl<'a> Function<'a> { /// Returns a constructor metadata object for this function. This doesn't /// check if the function is a constructor, however. - pub fn constructor_meta(&self, class: &syn::Path) -> TokenStream { + pub fn constructor_meta( + &self, + class: &syn::Path, + visibility: Option<&Visibility>, + ) -> TokenStream { let ident = self.ident; let (required, not_required) = self.args.split_args(self.optional.as_ref()); let required_args = required @@ -339,6 +343,7 @@ impl<'a> Function<'a> { } }); let docs = &self.docs; + let flags = visibility.option_tokens(); quote! { ::ext_php_rs::class::ConstructorMeta { @@ -368,7 +373,8 @@ impl<'a> Function<'a> { #variadic } inner - } + }, + flags: #flags } } } diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index 039122621..353448eb5 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -120,7 +120,7 @@ struct ParsedImpl<'a> { change_method_case: RenameRule, change_constant_case: RenameRule, functions: Vec, - constructor: Option>, + constructor: Option<(Function<'a>, Option)>, constants: Vec>, } @@ -212,7 +212,7 @@ impl<'a> ParsedImpl<'a> { let mut modifiers: HashSet = HashSet::new(); if matches!(opts.ty, MethodTy::Constructor) { - if self.constructor.replace(func).is_some() { + if self.constructor.replace((func, opts.vis.into())).is_some() { bail!(method => "Only one constructor can be provided per class."); } } else { @@ -264,7 +264,7 @@ impl<'a> ParsedImpl<'a> { let constructor = self .constructor .as_ref() - .map(|func| func.constructor_meta(self.path)) + .map(|(func, vis)| func.constructor_meta(self.path, vis.as_ref())) .option_tokens(); let constants = self.constants.iter().map(|c| { let name = &c.name; @@ -306,11 +306,8 @@ impl quote::ToTokens for FnBuilder { let builder = &self.builder; // TODO(cole_d): allow more flags via attributes let mut flags = vec![]; - flags.push(match self.vis { - Visibility::Public => quote! { ::ext_php_rs::flags::MethodFlags::Public }, - Visibility::Protected => quote! { ::ext_php_rs::flags::MethodFlags::Protected }, - Visibility::Private => quote! { ::ext_php_rs::flags::MethodFlags::Private }, - }); + let vis = &self.vis; + flags.push(quote! { #vis }); for flag in &self.modifiers { flags.push(quote! { #flag }); } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 8b7cdbee0..553bb3de8 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -712,8 +712,8 @@ fn php_module_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 /// - `#[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(public)]`, `#[php(protected)]` and `#[php(private)]` - Sets the -/// visibility of the method. +/// - `#[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. /// diff --git a/crates/macros/src/parsing.rs b/crates/macros/src/parsing.rs index 957740348..d73349e8f 100644 --- a/crates/macros/src/parsing.rs +++ b/crates/macros/src/parsing.rs @@ -1,5 +1,6 @@ use convert_case::{Case, Casing}; use darling::FromMeta; +use quote::{quote, ToTokens}; const MAGIC_METHOD: [&str; 17] = [ "__construct", @@ -31,6 +32,17 @@ pub enum Visibility { Protected, } +impl ToTokens for Visibility { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + Visibility::Public => quote! { ::ext_php_rs::flags::MethodFlags::Public }, + Visibility::Protected => quote! { ::ext_php_rs::flags::MethodFlags::Protected }, + Visibility::Private => quote! { ::ext_php_rs::flags::MethodFlags::Private }, + } + .to_tokens(tokens); + } +} + pub trait Rename { fn rename(&self, rule: RenameRule) -> String; } diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index bd1a3f433..878e9c484 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -42,7 +42,7 @@ The rest of the options are passed as separate attributes: - `#[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(public)]`, `#[php(protected)]` and `#[php(private)]` - Sets the visibility of the +- `#[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. diff --git a/src/builders/class.rs b/src/builders/class.rs index 453d9efa7..daa006177 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -238,16 +238,21 @@ impl ClassBuilder { "Class name in builder does not match class name in `impl RegisteredClass`." ); self.object_override = Some(create_object::); - self.method( - { - let mut func = FunctionBuilder::new("__construct", constructor::); - if let Some(ConstructorMeta { build_fn, .. }) = T::constructor() { - func = build_fn(func); - } - func - }, - MethodFlags::Public, - ) + + let (func, visibility) = if let Some(ConstructorMeta { + build_fn, flags, .. + }) = T::constructor() + { + let func = FunctionBuilder::new("__construct", constructor::); + (build_fn(func), flags.unwrap_or(MethodFlags::Public)) + } else { + ( + FunctionBuilder::new("__construct", constructor::), + MethodFlags::Public, + ) + }; + + self.method(func, visibility) } /// Function to register the class with PHP. This function is called after diff --git a/src/class.rs b/src/class.rs index 42673ac8e..ad6846c9f 100644 --- a/src/class.rs +++ b/src/class.rs @@ -82,6 +82,8 @@ pub struct ConstructorMeta { /// Function called to build the constructor function. Usually adds /// arguments. pub build_fn: fn(FunctionBuilder) -> FunctionBuilder, + /// Add constructor modification + pub flags: Option, } /// Result returned from a constructor of a class. diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index a4d2b9126..d6c6ee6c1 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -43,3 +43,11 @@ assert_exception_thrown(fn() => $arrayAccess['foo']); assert($arrayAccess[0] === true); assert($arrayAccess[1] === false); + +$classReflection = new ReflectionClass(TestClassMethodVisibility::class); +assert($classReflection->getMethod('__construct')->isPrivate()); +assert($classReflection->getMethod('private')->isPrivate()); +assert($classReflection->getMethod('protected')->isProtected()); + +$classReflection = new ReflectionClass(TestClassProtectedConstruct::class); +assert($classReflection->getMethod('__construct')->isProtected()); diff --git a/tests/src/integration/class/mod.rs b/tests/src/integration/class/mod.rs index c9774be61..fe1aae7ae 100644 --- a/tests/src/integration/class/mod.rs +++ b/tests/src/integration/class/mod.rs @@ -144,12 +144,45 @@ impl TestClassExtendsImpl { } } +#[php_class] +struct TestClassMethodVisibility; + +#[php_impl] +impl TestClassMethodVisibility { + #[php(vis = "private")] + fn __construct() -> Self { + Self + } + + #[php(vis = "private")] + fn private() -> u32 { + 3 + } + + #[php(vis = "protected")] + fn protected() -> u32 { + 3 + } +} +#[php_class] +struct TestClassProtectedConstruct; + +#[php_impl] +impl TestClassProtectedConstruct { + #[php(vis = "protected")] + fn __construct() -> Self { + Self + } +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { builder .class::() .class::() .class::() .class::() + .class::() + .class::() .function(wrap_function!(test_class)) .function(wrap_function!(throw_exception)) }