diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 9868600f09..0b84ca512a 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -1,76 +1,37 @@ -use darling::ast::NestedMeta; -use darling::{FromMeta, ToTokens}; -use proc_macro2::{Ident, TokenStream}; +use darling::util::Flag; +use darling::{FromAttributes, FromMeta, ToTokens}; +use proc_macro2::TokenStream; use quote::quote; -use syn::parse::ParseStream; -use syn::{Attribute, Expr, Fields, ItemStruct, LitStr, Meta, Token}; +use syn::{Attribute, Expr, Fields, ItemStruct}; use crate::helpers::get_docs; +use crate::parsing::PhpRename; use crate::prelude::*; -#[derive(Debug, Default, FromMeta)] -#[darling(default)] -pub struct StructArgs { +#[derive(FromAttributes, Debug, Default)] +#[darling(attributes(php), forward_attrs(doc), default)] +pub struct StructAttributes { /// The name of the PHP class. Defaults to the same name as the struct. - name: Option, + #[darling(flatten)] + rename: PhpRename, /// A modifier function which should accept one argument, a `ClassBuilder`, /// and return the same object. Allows the user to modify the class before /// it is built. modifier: Option, /// An expression of `ClassFlags` to be applied to the class. flags: Option, -} - -/// Sub-attributes which are parsed by this macro. Must be placed underneath the -/// main `#[php_class]` attribute. -#[derive(Debug, Default)] -struct ClassAttrs { extends: Option, + #[darling(multiple)] implements: Vec, - docs: Vec, + attrs: Vec, } -impl ClassAttrs { - fn parse(&mut self, attrs: &mut Vec) -> Result<()> { - let mut unparsed = vec![]; - unparsed.append(attrs); - for attr in unparsed { - let path = attr.path(); - - if path.is_ident("extends") { - if self.extends.is_some() { - bail!(attr => "Only one `#[extends]` attribute is valid per struct."); - } - let extends: syn::Expr = match attr.parse_args() { - Ok(extends) => extends, - Err(_) => bail!(attr => "Invalid arguments passed to extends attribute."), - }; - self.extends = Some(extends); - } else if path.is_ident("implements") { - let implements: syn::Expr = match attr.parse_args() { - Ok(extends) => extends, - Err(_) => bail!(attr => "Invalid arguments passed to implements attribute."), - }; - self.implements.push(implements); - } else { - attrs.push(attr); - } - } - self.docs = get_docs(attrs); - Ok(()) - } -} - -pub fn parser(args: TokenStream, mut input: ItemStruct) -> Result { +pub fn parser(mut input: ItemStruct) -> Result { + let attr = StructAttributes::from_attributes(&input.attrs)?; let ident = &input.ident; - let meta = NestedMeta::parse_meta_list(args)?; - let args = match StructArgs::from_list(&meta) { - Ok(args) => args, - Err(e) => bail!("Failed to parse struct arguments: {:?}", e), - }; - - let mut class_attrs = ClassAttrs::default(); - class_attrs.parse(&mut input.attrs)?; + let name = attr.rename.rename(ident.to_string()); + let docs = get_docs(&attr.attrs)?; + input.attrs.retain(|attr| !attr.path().is_ident("php")); let fields = match &mut input.fields { Fields::Named(fields) => parse_fields(fields.named.iter_mut())?, @@ -79,13 +40,13 @@ pub fn parser(args: TokenStream, mut input: ItemStruct) -> Result { let class_impl = generate_registered_class_impl( ident, - args.name.as_deref(), - args.modifier.as_ref(), - class_attrs.extends.as_ref(), - &class_attrs.implements, + &name, + attr.modifier.as_ref(), + attr.extends.as_ref(), + &attr.implements, &fields, - args.flags.as_ref(), - &class_attrs.docs, + attr.flags.as_ref(), + &docs, ); Ok(quote! { @@ -96,6 +57,16 @@ pub fn parser(args: TokenStream, mut input: ItemStruct) -> Result { }) } +#[derive(FromAttributes, Debug, Default)] +#[darling(attributes(php), forward_attrs(doc), default)] +struct PropAttributes { + prop: Flag, + #[darling(flatten)] + rename: PhpRename, + flags: Option, + attrs: Vec, +} + fn parse_fields<'a>(fields: impl Iterator) -> Result>> { #[derive(Debug, Default, FromMeta)] #[darling(default)] @@ -105,35 +76,16 @@ fn parse_fields<'a>(fields: impl Iterator) -> Result< let mut result = vec![]; for field in fields { - let mut docs = vec![]; - let mut property = None; - let mut unparsed = vec![]; - unparsed.append(&mut field.attrs); - - for attr in unparsed { - if let Some(parsed) = parse_attribute(&attr)? { - match parsed { - ParsedAttribute::Property(prop) => { - let ident = field - .ident - .as_ref() - .ok_or_else(|| err!(attr => "Only named fields can be properties."))?; - - property = Some((ident, prop)); - } - ParsedAttribute::Comment(doc) => docs.push(doc), - } - } else { - field.attrs.push(attr); - } - } - - if let Some((ident, prop)) = property { - result.push(Property { - ident, - attr: prop, - docs, - }); + let attr = PropAttributes::from_attributes(&field.attrs)?; + if attr.prop.is_present() { + let ident = field + .ident + .as_ref() + .ok_or_else(|| err!("Only named fields can be properties."))?; + let docs = get_docs(&attr.attrs)?; + field.attrs.retain(|attr| !attr.path().is_ident("php")); + + result.push(Property { ident, attr, docs }); } } @@ -141,98 +93,23 @@ fn parse_fields<'a>(fields: impl Iterator) -> Result< } #[derive(Debug)] -pub struct Property<'a> { +struct Property<'a> { pub ident: &'a syn::Ident, - pub attr: PropertyAttr, + pub attr: PropAttributes, pub docs: Vec, } impl Property<'_> { pub fn name(&self) -> String { - self.attr - .rename - .to_owned() - .unwrap_or_else(|| self.ident.to_string()) + self.attr.rename.rename(self.ident.to_string()) } } -#[derive(Debug, Default)] -pub struct PropertyAttr { - pub rename: Option, - pub flags: Option, -} - -impl syn::parse::Parse for PropertyAttr { - fn parse(input: syn::parse::ParseStream) -> syn::Result { - let mut this = Self::default(); - while !input.is_empty() { - let field = input.parse::()?.to_string(); - input.parse::()?; - - match field.as_str() { - "rename" => { - this.rename.replace(input.parse::()?.value()); - } - "flags" => { - this.flags.replace(input.parse::()?); - } - _ => return Err(input.error("invalid attribute field")), - } - - let _ = input.parse::(); - } - - Ok(this) - } -} - -#[derive(Debug)] -pub enum ParsedAttribute { - Property(PropertyAttr), - Comment(String), -} - -pub fn parse_attribute(attr: &Attribute) -> Result> { - let name = attr.path().to_token_stream().to_string(); - - Ok(match name.as_ref() { - "doc" => { - struct DocComment(pub String); - - impl syn::parse::Parse for DocComment { - fn parse(input: ParseStream) -> syn::Result { - input.parse::()?; - let comment: LitStr = input.parse()?; - Ok(Self(comment.value())) - } - } - - let comment: DocComment = syn::parse2(attr.to_token_stream()) - .map_err(|e| err!(attr => "Failed to parse doc comment {}", e))?; - Some(ParsedAttribute::Comment(comment.0)) - } - "prop" | "property" => { - let attr = match attr.meta { - Meta::Path(_) => PropertyAttr::default(), - Meta::List(_) => attr - .parse_args() - .map_err(|e| err!(attr => "Unable to parse `#[{}]` attribute: {}", name, e))?, - _ => { - bail!(attr => "Invalid attribute format for `#[{}]`", name); - } - }; - - Some(ParsedAttribute::Property(attr)) - } - _ => None, - }) -} - /// Generates an implementation of `RegisteredClass` for struct `ident`. #[allow(clippy::too_many_arguments)] fn generate_registered_class_impl( ident: &syn::Ident, - class_name: Option<&str>, + class_name: &str, modifier: Option<&syn::Ident>, extends: Option<&syn::Expr>, implements: &[syn::Expr], @@ -240,11 +117,6 @@ fn generate_registered_class_impl( flags: Option<&syn::Expr>, docs: &[String], ) -> TokenStream { - let ident_str = ident.to_string(); - let class_name = match class_name { - Some(class_name) => class_name, - None => &ident_str, - }; let modifier = modifier.option_tokens(); let extends = extends.option_tokens(); @@ -255,7 +127,7 @@ fn generate_registered_class_impl( .attr .flags .as_ref() - .map(|flags| flags.to_token_stream()) + .map(ToTokens::to_token_stream) .unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public }); let docs = &prop.docs; diff --git a/crates/macros/src/constant.rs b/crates/macros/src/constant.rs index 14c0383e67..d5f6efc23b 100644 --- a/crates/macros/src/constant.rs +++ b/crates/macros/src/constant.rs @@ -1,31 +1,52 @@ +use darling::FromAttributes; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::ItemConst; use crate::helpers::get_docs; +use crate::parsing::PhpRename; use crate::prelude::*; const INTERNAL_CONST_DOC_PREFIX: &str = "_internal_const_docs_"; +const INTERNAL_CONST_NAME_PREFIX: &str = "_internal_const_name_"; -pub fn parser(item: ItemConst) -> TokenStream { - let docs = get_docs(&item.attrs); +#[derive(FromAttributes, Default, Debug)] +#[darling(default, attributes(php), forward_attrs(doc))] +pub(crate) struct PhpConstAttribute { + #[darling(flatten)] + pub(crate) rename: PhpRename, + // TODO: Implement const Visibility + // pub(crate) vis: Option, + pub(crate) attrs: Vec, +} + +pub fn parser(mut item: ItemConst) -> Result { + let attr = PhpConstAttribute::from_attributes(&item.attrs)?; + + let name = attr.rename.rename(item.ident.to_string()); + let name_ident = format_ident!("{INTERNAL_CONST_NAME_PREFIX}{}", item.ident); + + let docs = get_docs(&attr.attrs)?; let docs_ident = format_ident!("{INTERNAL_CONST_DOC_PREFIX}{}", item.ident); + item.attrs.retain(|attr| !attr.path().is_ident("php")); - quote! { + Ok(quote! { #item #[allow(non_upper_case_globals)] const #docs_ident: &[&str] = &[#(#docs),*]; - } + #[allow(non_upper_case_globals)] + const #name_ident: &str = #name; + }) } -pub fn wrap(input: syn::Path) -> Result { - let Some(const_name) = input.get_ident().map(|i| i.to_string()) else { +pub fn wrap(input: &syn::Path) -> Result { + let Some(const_name) = input.get_ident().map(ToString::to_string) else { bail!(input => "Pass a PHP const into `wrap_constant!()`."); }; let doc_const = format_ident!("{INTERNAL_CONST_DOC_PREFIX}{const_name}"); + let const_name = format_ident!("{INTERNAL_CONST_NAME_PREFIX}{const_name}"); Ok(quote! { (#const_name, #input, #doc_const) - }) } diff --git a/crates/macros/src/function.rs b/crates/macros/src/function.rs index 4bf9f3adbd..5148559259 100644 --- a/crates/macros/src/function.rs +++ b/crates/macros/src/function.rs @@ -1,7 +1,6 @@ use std::collections::HashMap; -use darling::ast::NestedMeta; -use darling::{FromMeta, ToTokens}; +use darling::{FromAttributes, ToTokens}; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; use syn::spanned::Spanned as _; @@ -9,10 +8,11 @@ use syn::PatType; use syn::{FnArg, GenericArgument, ItemFn, Lit, PathArguments, Type, TypePath}; use crate::helpers::get_docs; +use crate::parsing::{PhpRename, Visibility}; use crate::prelude::*; use crate::syn_ext::DropLifetimes; -pub fn wrap(input: syn::Path) -> Result { +pub fn wrap(input: &syn::Path) -> Result { let Some(func_name) = input.get_ident() else { bail!(input => "Pass a PHP function name into `wrap_function!()`."); }; @@ -23,33 +23,36 @@ pub fn wrap(input: syn::Path) -> Result { }}) } -#[derive(Default, Debug, FromMeta)] -#[darling(default)] -pub struct FnArgs { - /// The name of the function. - name: Option, - /// The first optional argument of the function signature. - optional: Option, - /// Default values for optional arguments. +#[derive(FromAttributes, Default, Debug)] +#[darling(default, attributes(php), forward_attrs(doc))] +struct PhpFunctionAttribute { + #[darling(flatten)] + rename: PhpRename, defaults: HashMap, + optional: Option, + vis: Option, + attrs: Vec, } -pub fn parser(opts: TokenStream, input: ItemFn) -> Result { - let meta = NestedMeta::parse_meta_list(opts)?; - let opts = match FnArgs::from_list(&meta) { - Ok(opts) => opts, - Err(e) => bail!("Failed to parse attribute options: {:?}", e), - }; +pub fn parser(mut input: ItemFn) -> Result { + let php_attr = PhpFunctionAttribute::from_attributes(&input.attrs)?; + input.attrs.retain(|attr| !attr.path().is_ident("php")); - let args = Args::parse_from_fnargs(input.sig.inputs.iter(), opts.defaults)?; + let args = Args::parse_from_fnargs(input.sig.inputs.iter(), php_attr.defaults)?; if let Some(ReceiverArg { span, .. }) = args.receiver { bail!(span => "Receiver arguments are invalid on PHP functions. See `#[php_impl]`."); } - let docs = get_docs(&input.attrs); + let docs = get_docs(&php_attr.attrs)?; - let func = Function::new(&input.sig, opts.name, args, opts.optional, docs)?; - let function_impl = func.php_function_impl()?; + let func = Function::new( + &input.sig, + php_attr.rename.rename(input.sig.ident.to_string()), + args, + php_attr.optional, + docs, + ); + let function_impl = func.php_function_impl(); Ok(quote! { #input @@ -104,14 +107,14 @@ impl<'a> Function<'a> { /// * `optional` - The ident of the first optional argument. pub fn new( sig: &'a syn::Signature, - name: Option, + name: String, args: Args<'a>, optional: Option, docs: Vec, - ) -> Result { - Ok(Self { + ) -> Self { + Self { ident: &sig.ident, - name: name.unwrap_or_else(|| sig.ident.to_string()), + name, args, output: match &sig.output { syn::ReturnType::Default => None, @@ -119,7 +122,7 @@ impl<'a> Function<'a> { }, optional, docs, - }) + } } /// Generates an internal identifier for the function. @@ -128,7 +131,7 @@ impl<'a> Function<'a> { } /// Generates the function builder for the function. - pub fn function_builder(&self, call_type: CallType) -> Result { + pub fn function_builder(&self, call_type: CallType) -> TokenStream { let ident = self.ident; let name = &self.name; let (required, not_required) = self.args.split_args(self.optional.as_ref()); @@ -141,7 +144,7 @@ impl<'a> Function<'a> { .typed .iter() .map(TypedArg::arg_declaration) - .collect::>>()?; + .collect::>(); let arg_accessors = self.args.typed.iter().map(|arg| { arg.accessor(|e| { quote! { @@ -155,11 +158,11 @@ impl<'a> Function<'a> { let required_args = required .iter() .map(TypedArg::arg_builder) - .collect::>>()?; + .collect::>(); let not_required_args = not_required .iter() .map(TypedArg::arg_builder) - .collect::>>()?; + .collect::>(); let returns = self.output.as_ref().map(|output| { quote! { .returns( @@ -226,16 +229,16 @@ impl<'a> Function<'a> { } }; - let docs = if !self.docs.is_empty() { + let docs = if self.docs.is_empty() { + quote! {} + } else { let docs = &self.docs; quote! { .docs(&[#(#docs),*]) } - } else { - quote! {} }; - Ok(quote! { + quote! { ::ext_php_rs::builders::FunctionBuilder::new(#name, { ::ext_php_rs::zend_fastcall! { extern fn handler( @@ -262,15 +265,15 @@ impl<'a> Function<'a> { #(.arg(#not_required_args))* #returns #docs - }) + } } /// Generates a struct and impl for the `PhpFunction` trait. - pub fn php_function_impl(&self) -> Result { + pub fn php_function_impl(&self) -> TokenStream { let internal_ident = self.internal_ident(); - let builder = self.function_builder(CallType::Function)?; + let builder = self.function_builder(CallType::Function); - Ok(quote! { + quote! { #[doc(hidden)] #[allow(non_camel_case_types)] struct #internal_ident; @@ -284,22 +287,22 @@ impl<'a> Function<'a> { entry }; } - }) + } } /// 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) -> Result { + pub fn constructor_meta(&self, class: &syn::Path) -> TokenStream { let ident = self.ident; let (required, not_required) = self.args.split_args(self.optional.as_ref()); let required_args = required .iter() .map(TypedArg::arg_builder) - .collect::>>()?; + .collect::>(); let not_required_args = not_required .iter() .map(TypedArg::arg_builder) - .collect::>>()?; + .collect::>(); let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect(); let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect(); @@ -308,7 +311,7 @@ impl<'a> Function<'a> { .typed .iter() .map(TypedArg::arg_declaration) - .collect::>>()?; + .collect::>(); let arg_accessors = self.args.typed.iter().map(|arg| { arg.accessor( |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); }, @@ -320,7 +323,7 @@ impl<'a> Function<'a> { } }); - Ok(quote! { + quote! { ::ext_php_rs::class::ConstructorMeta { constructor: { fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> { @@ -349,7 +352,7 @@ impl<'a> Function<'a> { inner } } - }) + } } } @@ -398,9 +401,8 @@ impl<'a> Args<'a> { }); } FnArg::Typed(PatType { pat, ty, .. }) => { - let ident = match &**pat { - syn::Pat::Ident(syn::PatIdent { ident, .. }) => ident, - _ => bail!(pat => "Unsupported argument."), + let syn::Pat::Ident(syn::PatIdent { ident, .. }) = &**pat else { + bail!(pat => "Unsupported argument."); }; // If the variable is `&[&Zval]` treat it as the variadic argument. @@ -512,9 +514,7 @@ impl TypedArg<'_> { // Variadic arguments are passed as slices, so we need to extract the // inner type. if self.variadic { - let reference = if let Type::Reference(r) = &ty { - r - } else { + let Type::Reference(reference) = &ty else { return ty; }; @@ -528,17 +528,17 @@ impl TypedArg<'_> { /// Returns a token stream containing an argument declaration, where the /// name of the variable holding the arg is the name of the argument. - fn arg_declaration(&self) -> Result { + fn arg_declaration(&self) -> TokenStream { let name = self.name; - let val = self.arg_builder()?; - Ok(quote! { + let val = self.arg_builder(); + quote! { let mut #name = #val; - }) + } } /// Returns a token stream containing the `Arg` definition to be passed to /// `ext-php-rs`. - fn arg_builder(&self) -> Result { + fn arg_builder(&self) -> TokenStream { let name = self.name.to_string(); let ty = self.clean_ty(); let null = if self.nullable { @@ -558,13 +558,13 @@ impl TypedArg<'_> { None }; let variadic = self.variadic.then(|| quote! { .is_variadic() }); - Ok(quote! { + quote! { ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE) #null #default #as_ref #variadic - }) + } } /// Get the accessor used to access the value of the argument. @@ -615,8 +615,7 @@ pub fn type_is_nullable(ty: &Type, has_default: bool) -> Result { .segments .iter() .next_back() - .map(|seg| seg.ident == "Option") - .unwrap_or(false) + .is_some_and(|seg| seg.ident == "Option") } syn::Type::Reference(_) => false, /* Reference cannot be nullable unless */ // wrapped in `Option` (in that case it'd be a Path). diff --git a/crates/macros/src/helpers.rs b/crates/macros/src/helpers.rs index dfc4b8affb..162c904fd5 100644 --- a/crates/macros/src/helpers.rs +++ b/crates/macros/src/helpers.rs @@ -1,16 +1,26 @@ -use crate::class::{parse_attribute, ParsedAttribute}; -use syn::Attribute; +use crate::prelude::*; +use syn::{Attribute, Expr, Lit, Meta}; /// Takes a list of attributes and returns a list of doc comments retrieved from /// the attributes. -pub fn get_docs(attrs: &[Attribute]) -> Vec { - let mut docs = vec![]; +pub fn get_docs(attrs: &[Attribute]) -> Result> { + attrs + .iter() + .filter(|attr| attr.path().is_ident("doc")) + .map(|attr| { + let Meta::NameValue(meta) = &attr.meta else { + bail!(attr.meta => "Invalid format for `#[doc]` attribute."); + }; - for attr in attrs { - if let Ok(Some(ParsedAttribute::Comment(doc))) = parse_attribute(attr) { - docs.push(doc); - } - } + let Expr::Lit(value) = &meta.value else { + bail!(attr.meta => "Invalid format for `#[doc]` attribute."); + }; - docs + let Lit::Str(doc) = &value.lit else { + bail!(value.lit => "Invalid format for `#[doc]` attribute."); + }; + + Ok(doc.value()) + }) + .collect::>>() } diff --git a/crates/macros/src/impl_.rs b/crates/macros/src/impl_.rs index b0209ca33c..b43f7ea1ad 100644 --- a/crates/macros/src/impl_.rs +++ b/crates/macros/src/impl_.rs @@ -1,73 +1,16 @@ -use darling::ast::NestedMeta; -use darling::FromMeta; +use darling::util::Flag; +use darling::FromAttributes; use proc_macro2::TokenStream; use quote::quote; use std::collections::HashMap; use syn::{Ident, ItemImpl, Lit}; +use crate::constant::PhpConstAttribute; use crate::function::{Args, CallType, Function, MethodReceiver}; use crate::helpers::get_docs; +use crate::parsing::{PhpRename, RenameRule, Visibility}; use crate::prelude::*; -#[derive(Debug, Copy, Clone, FromMeta, Default)] -pub enum RenameRule { - /// Methods won't be renamed. - #[darling(rename = "none")] - None, - /// Methods will be converted to camelCase. - #[darling(rename = "camelCase")] - #[default] - Camel, - /// Methods will be converted to snake_case. - #[darling(rename = "snake_case")] - Snake, -} - -impl RenameRule { - /// Change case of an identifier. - /// - /// Magic methods are handled specially to make sure they're always cased - /// correctly. - pub fn rename(&self, name: impl AsRef) -> String { - let name = name.as_ref(); - match self { - RenameRule::None => name.to_string(), - rule => match name { - "__construct" => "__construct".to_string(), - "__destruct" => "__destruct".to_string(), - "__call" => "__call".to_string(), - "__call_static" => "__callStatic".to_string(), - "__get" => "__get".to_string(), - "__set" => "__set".to_string(), - "__isset" => "__isset".to_string(), - "__unset" => "__unset".to_string(), - "__sleep" => "__sleep".to_string(), - "__wakeup" => "__wakeup".to_string(), - "__serialize" => "__serialize".to_string(), - "__unserialize" => "__unserialize".to_string(), - "__to_string" => "__toString".to_string(), - "__invoke" => "__invoke".to_string(), - "__set_state" => "__set_state".to_string(), - "__clone" => "__clone".to_string(), - "__debug_info" => "__debugInfo".to_string(), - field => match rule { - Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field), - Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field), - Self::None => unreachable!(), - }, - }, - } - } -} - -/// Method visibilities. -#[derive(Debug)] -enum MethodVis { - Public, - Private, - Protected, -} - /// Method types. #[derive(Debug)] enum MethodTy { @@ -83,26 +26,18 @@ enum MethodTy { Abstract, } -#[derive(Default, Debug, FromMeta)] -#[darling(default)] -pub struct AttrArgs { +#[derive(FromAttributes, Debug, Default)] +#[darling(attributes(php), default)] +pub struct PhpImpl { + /// Rename methods to match the given rule. rename_methods: Option, + /// Rename constants to match the given rule. + rename_constants: Option, } -/// Attribute arguments for `impl` blocks. -#[derive(Debug, Default, FromMeta)] -#[darling(default)] -pub struct ImplArgs { - /// How the methods are renamed. - rename_methods: RenameRule, -} - -pub fn parser(args: TokenStream, mut input: ItemImpl) -> Result { - let meta = NestedMeta::parse_meta_list(args)?; - let args = match ImplArgs::from_list(&meta) { - Ok(args) => args, - Err(e) => bail!(input => "Failed to parse impl attribute arguments: {:?}", e), - }; +pub fn parser(mut input: ItemImpl) -> Result { + let args = PhpImpl::from_attributes(&input.attrs)?; + input.attrs.retain(|attr| !attr.path().is_ident("php")); let path = match &*input.self_ty { syn::Type::Path(ty) => &ty.path, _ => { @@ -110,10 +45,15 @@ pub fn parser(args: TokenStream, mut input: ItemImpl) -> Result { } }; - let mut parsed = ParsedImpl::new(path, args.rename_methods); + let mut parsed = ParsedImpl::new( + path, + args.rename_methods.unwrap_or(RenameRule::Camel), + args.rename_constants + .unwrap_or(RenameRule::ScreamingSnakeCase), + ); parsed.parse(input.items.iter_mut())?; - let php_class_impl = parsed.generate_php_class_impl()?; + let php_class_impl = parsed.generate_php_class_impl(); Ok(quote::quote! { #input #php_class_impl @@ -130,86 +70,55 @@ struct MethodArgs { /// Default values for optional arguments. defaults: HashMap, /// Visibility of the method (public, protected, private). - vis: MethodVis, + vis: Visibility, /// Method type. ty: MethodTy, } +#[derive(FromAttributes, Default, Debug)] +#[darling(default, attributes(php), forward_attrs(doc))] +pub struct PhpFunctionImplAttribute { + #[darling(flatten)] + rename: PhpRename, + defaults: HashMap, + optional: Option, + vis: Option, + attrs: Vec, + getter: Flag, + setter: Flag, + constructor: Flag, + abstract_method: Flag, +} + impl MethodArgs { - fn new(name: String) -> Self { - let ty = if name == "__construct" { + fn new(name: String, attr: PhpFunctionImplAttribute) -> Self { + let ty = if name == "__construct" || attr.constructor.is_present() { MethodTy::Constructor + } else if attr.getter.is_present() { + MethodTy::Getter + } else if attr.setter.is_present() { + MethodTy::Setter + } else if attr.abstract_method.is_present() { + MethodTy::Abstract } else { MethodTy::Normal }; + Self { name, - optional: Default::default(), - defaults: Default::default(), - vis: MethodVis::Public, + optional: attr.optional, + defaults: attr.defaults, + vis: attr.vis.unwrap_or(Visibility::Public), ty, } } - - fn parse(&mut self, attrs: &mut Vec) -> Result<()> { - let mut unparsed = vec![]; - unparsed.append(attrs); - for attr in unparsed { - let path = &attr.path(); - - if path.is_ident("optional") { - // x - if self.optional.is_some() { - bail!(attr => "Only one `#[optional]` attribute is valid per method."); - } - let optional = attr.parse_args().map_err( - |e| err!(e.span() => "Invalid arguments passed to `#[optional]` attribute. {}", e), - )?; - self.optional = Some(optional); - } else if path.is_ident("defaults") { - let defaults = HashMap::from_meta(&attr.meta).map_err( - |e| err!(e.span() => "Invalid arguments passed to `#[defaults]` attribute. {}", e), - )?; - self.defaults = defaults; - } else if path.is_ident("public") { - // x - self.vis = MethodVis::Public; - } else if path.is_ident("protected") { - // x - self.vis = MethodVis::Protected; - } else if path.is_ident("private") { - // x - self.vis = MethodVis::Private; - } else if path.is_ident("rename") { - let lit: syn::Lit = attr.parse_args().map_err(|e| err!(attr => "Invalid arguments passed to the `#[rename]` attribute. {}", e))?; - match lit { - Lit::Str(name) => self.name = name.value(), - _ => bail!(attr => "Only strings are valid method names."), - }; - } else if path.is_ident("getter") { - // x - self.ty = MethodTy::Getter; - } else if path.is_ident("setter") { - // x - self.ty = MethodTy::Setter; - } else if path.is_ident("constructor") { - // x - self.ty = MethodTy::Constructor; - } else if path.is_ident("abstract_method") { - // x - self.ty = MethodTy::Abstract; - } else { - attrs.push(attr); - } - } - Ok(()) - } } #[derive(Debug)] struct ParsedImpl<'a> { path: &'a syn::Path, - rename: RenameRule, + rename_methods: RenameRule, + rename_constants: RenameRule, functions: Vec, constructor: Option>, constants: Vec>, @@ -217,10 +126,10 @@ struct ParsedImpl<'a> { #[derive(Debug)] struct FnBuilder { - /// Tokens which represent the FunctionBuilder for this function. + /// Tokens which represent the `FunctionBuilder` for this function. pub builder: TokenStream, /// The visibility of this method. - pub vis: MethodVis, + pub vis: Visibility, /// Whether this method is abstract. pub r#abstract: bool, } @@ -242,13 +151,14 @@ impl<'a> ParsedImpl<'a> { /// /// * `path` - Path of the type the `impl` block is for. /// * `rename` - Rename rule for methods. - fn new(path: &'a syn::Path, rename: RenameRule) -> Self { + fn new(path: &'a syn::Path, rename_methods: RenameRule, rename_constants: RenameRule) -> Self { Self { path, - rename, - functions: Default::default(), - constructor: Default::default(), - constants: Default::default(), + rename_methods, + rename_constants, + functions: Vec::default(), + constructor: Option::default(), + constants: Vec::default(), } } @@ -257,37 +167,28 @@ impl<'a> ParsedImpl<'a> { for items in items { match items { syn::ImplItem::Const(c) => { - let mut name = None; - let mut unparsed = vec![]; - unparsed.append(&mut c.attrs); - for attr in unparsed { - if attr.path().is_ident("rename") { - let lit: syn::Lit = attr.parse_args().map_err(|e| err!(attr => "Invalid arguments passed to the `#[rename]` attribute. {}", e))?; - match lit { - Lit::Str(str) => name = Some(str.value()), - _ => bail!(attr => "Only strings are valid constant names."), - }; - } else { - c.attrs.push(attr); - } - } - let docs = get_docs(&c.attrs); + let attr = PhpConstAttribute::from_attributes(&c.attrs)?; + let name = self.rename_constants.rename(c.ident.to_string()); + let name = attr.rename.rename(name); + let docs = get_docs(&attr.attrs)?; + c.attrs.retain(|attr| !attr.path().is_ident("php")); self.constants.push(Constant { - name: name.unwrap_or_else(|| c.ident.to_string()), + name, ident: &c.ident, docs, }); } syn::ImplItem::Fn(method) => { - let name = self.rename.rename(method.sig.ident.to_string()); - let docs = get_docs(&method.attrs); - let mut opts = MethodArgs::new(name); - opts.parse(&mut method.attrs)?; + let attr = PhpFunctionImplAttribute::from_attributes(&method.attrs)?; + let name = self.rename_methods.rename(method.sig.ident.to_string()); + let name = attr.rename.rename(name); + let docs = get_docs(&attr.attrs)?; + method.attrs.retain(|attr| !attr.path().is_ident("php")); + let opts = MethodArgs::new(name, attr); let args = Args::parse_from_fnargs(method.sig.inputs.iter(), opts.defaults)?; - let mut func = - Function::new(&method.sig, Some(opts.name), args, opts.optional, docs)?; + let mut func = Function::new(&method.sig, opts.name, args, opts.optional, docs); if matches!(opts.ty, MethodTy::Constructor) { if self.constructor.replace(func).is_some() { @@ -303,8 +204,7 @@ impl<'a> ParsedImpl<'a> { .args .typed .first() - .map(|arg| arg.name == "self_") - .unwrap_or_default() + .is_some_and(|arg| arg.name == "self_") { // `self_: &[mut] ZendClassObject` // Need to remove arg from argument list @@ -315,7 +215,7 @@ impl<'a> ParsedImpl<'a> { MethodReceiver::Static }, }; - let builder = func.function_builder(call_type)?; + let builder = func.function_builder(call_type); self.functions.push(FnBuilder { builder, vis: opts.vis, @@ -331,14 +231,14 @@ impl<'a> ParsedImpl<'a> { /// Generates an `impl PhpClassImpl for PhpClassImplCollector` /// block. - fn generate_php_class_impl(&self) -> Result { + fn generate_php_class_impl(&self) -> TokenStream { let path = &self.path; let functions = &self.functions; - let constructor = match &self.constructor { - Some(func) => Some(func.constructor_meta(self.path)?), - None => None, - } - .option_tokens(); + let constructor = self + .constructor + .as_ref() + .map(|func| func.constructor_meta(self.path)) + .option_tokens(); let constants = self.constants.iter().map(|c| { let name = &c.name; let ident = c.ident; @@ -348,7 +248,7 @@ impl<'a> ParsedImpl<'a> { } }); - Ok(quote! { + quote! { impl ::ext_php_rs::internal::class::PhpClassImpl<#path> for ::ext_php_rs::internal::class::PhpClassImplCollector<#path> { @@ -370,7 +270,7 @@ impl<'a> ParsedImpl<'a> { &[#(#constants),*] } } - }) + } } } @@ -380,9 +280,9 @@ impl quote::ToTokens for FnBuilder { // TODO(cole_d): allow more flags via attributes let mut flags = vec![]; flags.push(match self.vis { - MethodVis::Public => quote! { ::ext_php_rs::flags::MethodFlags::Public }, - MethodVis::Protected => quote! { ::ext_php_rs::flags::MethodFlags::Protected }, - MethodVis::Private => quote! { ::ext_php_rs::flags::MethodFlags::Private }, + 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 }, }); if self.r#abstract { flags.push(quote! { ::ext_php_rs::flags::MethodFlags::Abstract }); diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 588a487dca..521a8bc90f 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -7,6 +7,7 @@ mod function; mod helpers; mod impl_; mod module; +mod parsing; mod syn_ext; mod zval; @@ -21,50 +22,56 @@ 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 /// /// The attribute takes some options to modify the output of the class: /// -/// - `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. /// -/// There are also additional macros that modify the class. These macros **must** be -/// placed underneath the `#[php_class]` attribute. +/// There are also additional macros that modify the class. These macros +/// **must** be placed underneath the `#[php_class]` attribute. /// -/// - `#[extends(ce)]` - Sets the parent class of the class. Can only be used once. -/// `ce` must be a function with the signature `fn() -> &'static ClassEntry`. -/// - `#[implements(ce)]` - Implements the given interface on the class. Can be used -/// multiple times. `ce` must be a valid function with the signature -/// `fn() -> &'static ClassEntry`. +/// - `#[php(extends = ce)]` - Sets the parent class of the class. Can only be +/// used once. `ce` must be a function with the signature `fn() -> &'static +/// ClassEntry`. +/// - `#[php(implements = ce)]` - Implements the given interface on the class. +/// Can be used multiple times. `ce` must be a valid function with the +/// signature `fn() -> &'static ClassEntry`. /// -/// You may also use the `#[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 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: /// -/// - `rename` - Allows you to rename the property, e.g. -/// `#[prop(rename = "new_name")]` +/// - `name` - Allows you to rename the property, e.g. `#[php(name = +/// "new_name")]` +/// - `rename` - Allows you to rename the property using rename rules, e.g. +/// `#[php(rename = PascalCase)]` /// /// ## Restrictions /// /// ### 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 @@ -72,11 +79,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 /// @@ -91,7 +99,7 @@ extern crate proc_macro; /// pub struct Human { /// name: String, /// age: i32, -/// #[prop] +/// #[php(prop)] /// address: String, /// } /// @@ -102,8 +110,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))] @@ -115,7 +123,7 @@ extern crate proc_macro; /// }; /// /// #[php_class(name = "Redis\\Exception\\RedisException")] -/// #[extends(ce::exception)] +/// #[php(extends = ce::exception)] /// #[derive(Default)] /// pub struct RedisException; /// @@ -136,8 +144,8 @@ extern crate proc_macro; /// /// ## Implementing an Interface /// -/// To implement an interface, use `#[implements(ce)]` where `ce` 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)]` where `ce` 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))] @@ -150,7 +158,7 @@ extern crate proc_macro; /// }; /// /// #[php_class] -/// #[implements(ce::arrayaccess)] +/// #[php(implements = ce::arrayaccess)] /// #[derive(Default)] /// pub struct EvenNumbersArray; /// @@ -193,10 +201,10 @@ extern crate proc_macro; /// # fn main() {} /// ```` #[proc_macro_attribute] -pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { +pub fn php_class(_args: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); - class::parser(args.into(), input) + class::parser(input) .unwrap_or_else(|e| e.to_compile_error()) .into() } @@ -211,10 +219,10 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { /// /// ## 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))] @@ -239,16 +247,17 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { /// # fn main() {} /// ``` /// -/// Default parameter values can also be set for optional parameters. This is done -/// through the `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))] /// # extern crate ext_php_rs; /// use ext_php_rs::prelude::*; /// -/// #[php_function(defaults(offset = 0))] +/// #[php_function] +/// #[php(defaults(offset = 0))] /// pub fn rusty_strpos(haystack: &str, needle: &str, offset: i64) -> Option { /// let haystack: String = haystack.chars().skip(offset as usize).collect(); /// haystack.find(needle) @@ -301,7 +310,8 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { /// /// /// `age` will be deemed required and nullable rather than optional, /// /// while description will be optional. -/// #[php_function(optional = "description")] +/// #[php_function] +/// #[php(optional = "description")] /// pub fn greet(name: String, age: Option, description: Option) -> String { /// let mut greeting = format!("Hello, {}!", name); /// @@ -325,9 +335,9 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { /// /// ## 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))] @@ -354,21 +364,28 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream { /// translated into an exception and thrown. See the section on /// [exceptions](../exceptions.md) for more details. #[proc_macro_attribute] -pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream { +pub fn php_function(_args: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemFn); - function::parser(args.into(), input) + function::parser(input) .unwrap_or_else(|e| e.to_compile_error()) .into() } /// # `#[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. +/// +/// You can rename the const with options: /// -/// The `wrap_constant!()` macro can be used to simplify the registration of constants. -/// It sets the name and doc comments for the constant. +/// - `name` - Allows you to rename the property, e.g. `#[php(name = +/// "new_name")]` +/// - `rename` - Allows you to rename the property using rename rules, e.g. +/// `#[php(rename = PascalCase)]` /// /// ## Examples /// @@ -381,12 +398,17 @@ pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream { /// const TEST_CONSTANT: i32 = 100; /// /// #[php_const] +/// #[php(name = "I_AM_RENAMED")] +/// const TEST_CONSTANT_THE_SECOND: i32 = 42; +/// +/// #[php_const] /// const ANOTHER_STRING_CONST: &'static str = "Hello world!"; /// /// #[php_module] /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { /// module /// .constant(wrap_constant!(TEST_CONSTANT)) +/// .constant(wrap_constant!(TEST_CONSTANT_THE_SECOND)) /// .constant(("MANUAL_CONSTANT", ANOTHER_STRING_CONST, &[])) /// } /// # fn main() {} @@ -398,36 +420,39 @@ pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream { /// TokenStream { let input = parse_macro_input!(input as ItemConst); - constant::parser(input).into() + constant::parser(input) + .unwrap_or_else(|e| e.to_compile_error()) + .into() } /// # `#[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 /// @@ -484,16 +509,17 @@ pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream { /// # `#[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. /// /// ## Methods /// @@ -501,16 +527,17 @@ pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream { /// [`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`. /// -/// By default, all methods are renamed in PHP to the camel-case variant of the Rust -/// method name. This can be changed on the `#[php_impl]` attribute, by passing one -/// of the following as the `rename_methods` option: +/// By default, all methods are renamed in PHP to the camel-case variant of the +/// Rust method name. This can be changed on the `#[php_impl]` attribute, by +/// passing one of the following as the `rename_methods` option: /// /// - `"none"` - does not rename the methods. /// - `"camelCase"` - renames all methods to camel case (default). @@ -521,65 +548,68 @@ pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream { /// /// The rest of the options are passed as separate attributes: /// -/// - `#[defaults(i = 5, b = "hello")]` - Sets the default value for parameter(s). -/// - `#[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`. -/// - `#[public]`, `#[protected]` and `#[private]` - Sets the visibility of the -/// method. -/// - `#[rename("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(public)]`, `#[php(protected)]` and `#[php(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 `#[defaults]` and `#[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 `#[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 `#[getter]` and `#[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 `rename` -/// option to the attribute which will change the property name. +/// If you want to use a different name for the property, you can pass a +/// `rename` option to the 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 `#[getter]` and `#[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))] @@ -591,7 +621,7 @@ pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream { /// pub struct Human { /// name: String, /// age: i32, -/// #[prop] +/// #[php(prop)] /// address: String, /// } /// @@ -608,17 +638,17 @@ pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream { /// } /// } /// -/// #[getter] +/// #[php(getter)] /// pub fn get_name(&self) -> String { /// self.name.to_string() /// } /// -/// #[setter] +/// #[php(setter)] /// pub fn set_name(&mut self, name: String) { /// self.name = name; /// } /// -/// #[getter] +/// #[php(getter)] /// pub fn get_age(&self) -> i32 { /// self.age /// } @@ -656,10 +686,10 @@ pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream { /// /// [`php_async_impl`]: ./async_impl.md #[proc_macro_attribute] -pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream { +pub fn php_impl(_args: TokenStream, input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemImpl); - impl_::parser(args.into(), input) + impl_::parser(input) .unwrap_or_else(|e| e.to_compile_error()) .into() } @@ -738,16 +768,16 @@ pub fn php_extern(_: TokenStream, input: TokenStream) -> TokenStream { /// # `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 /// @@ -830,18 +860,18 @@ pub fn php_extern(_: TokenStream, input: TokenStream) -> TokenStream { /// ## 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 /// @@ -939,7 +969,7 @@ pub fn zend_fastcall(input: TokenStream) -> TokenStream { pub fn wrap_function(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as syn::Path); - match function::wrap(input) { + match function::wrap(&input) { Ok(parsed) => parsed, Err(e) => e.to_compile_error(), } @@ -951,7 +981,7 @@ pub fn wrap_function(input: TokenStream) -> TokenStream { pub fn wrap_constant(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as syn::Path); - match constant::wrap(input) { + match constant::wrap(&input) { Ok(parsed) => parsed, Err(e) => e.to_compile_error(), } @@ -987,9 +1017,10 @@ pub(crate) mod prelude { impl OptionTokens for Option { fn option_tokens(&self) -> proc_macro2::TokenStream { - match self { - Some(token) => quote::quote! { ::std::option::Option::Some(#token) }, - None => quote::quote! { ::std::option::Option::None }, + if let Some(token) = self { + quote::quote! { ::std::option::Option::Some(#token) } + } else { + quote::quote! { ::std::option::Option::None } } } } diff --git a/crates/macros/src/parsing.rs b/crates/macros/src/parsing.rs new file mode 100644 index 0000000000..d72cdbdd9e --- /dev/null +++ b/crates/macros/src/parsing.rs @@ -0,0 +1,176 @@ +use darling::FromMeta; + +#[derive(Debug, FromMeta)] +pub enum Visibility { + #[darling(rename = "public")] + Public, + #[darling(rename = "private")] + Private, + #[darling(rename = "protected")] + Protected, +} + +#[derive(FromMeta, Debug, Default)] +#[darling(default)] +pub struct PhpRename { + name: Option, + rename: Option, +} + +impl PhpRename { + pub fn rename(&self, name: impl AsRef) -> String { + self.name.as_ref().map_or_else( + || { + let name = name.as_ref(); + self.rename + .as_ref() + .map_or_else(|| name.to_string(), |r| r.rename(name)) + }, + ToString::to_string, + ) + } +} + +#[derive(Debug, Copy, Clone, FromMeta, Default)] +pub enum RenameRule { + /// Methods won't be renamed. + #[darling(rename = "none")] + None, + /// Methods will be converted to `camelCase`. + #[darling(rename = "camelCase")] + #[default] + Camel, + /// Methods will be converted to `snake_case`. + #[darling(rename = "snake_case")] + Snake, + /// Methods will be converted to `PascalCase`. + #[darling(rename = "PascalCase")] + Pascal, + /// Renames to `UPPER_SNAKE_CASE`. + #[darling(rename = "UPPER_CASE")] + ScreamingSnakeCase, +} + +impl RenameRule { + /// Change case of an identifier. + /// + /// Magic methods are handled specially to make sure they're always cased + /// correctly. + pub fn rename(self, name: impl AsRef) -> String { + let name = name.as_ref(); + match self { + RenameRule::None => name.to_string(), + rule => match name { + "__construct" => "__construct".to_string(), + "__destruct" => "__destruct".to_string(), + "__call" => "__call".to_string(), + "__call_static" => "__callStatic".to_string(), + "__get" => "__get".to_string(), + "__set" => "__set".to_string(), + "__isset" => "__isset".to_string(), + "__unset" => "__unset".to_string(), + "__sleep" => "__sleep".to_string(), + "__wakeup" => "__wakeup".to_string(), + "__serialize" => "__serialize".to_string(), + "__unserialize" => "__unserialize".to_string(), + "__to_string" => "__toString".to_string(), + "__invoke" => "__invoke".to_string(), + "__set_state" => "__set_state".to_string(), + "__clone" => "__clone".to_string(), + "__debug_info" => "__debugInfo".to_string(), + field => match rule { + Self::Camel => ident_case::RenameRule::CamelCase.apply_to_field(field), + Self::Pascal => ident_case::RenameRule::PascalCase.apply_to_field(field), + Self::Snake => ident_case::RenameRule::SnakeCase.apply_to_field(field), + Self::ScreamingSnakeCase => { + ident_case::RenameRule::ScreamingSnakeCase.apply_to_field(field) + } + Self::None => unreachable!(), + }, + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::{PhpRename, RenameRule}; + + #[test] + fn test_php_rename() { + let rename = PhpRename { + name: Some("test".to_string()), + rename: None, + }; + assert_eq!(rename.rename("test"), "test"); + assert_eq!(rename.rename("Test"), "test"); + assert_eq!(rename.rename("TEST"), "test"); + + let rename = PhpRename { + name: None, + rename: Some(RenameRule::ScreamingSnakeCase), + }; + assert_eq!(rename.rename("test"), "TEST"); + assert_eq!(rename.rename("Test"), "TEST"); + assert_eq!(rename.rename("TEST"), "TEST"); + + let rename = PhpRename { + name: Some("test".to_string()), + rename: Some(RenameRule::ScreamingSnakeCase), + }; + assert_eq!(rename.rename("test"), "test"); + assert_eq!(rename.rename("Test"), "test"); + assert_eq!(rename.rename("TEST"), "test"); + + let rename = PhpRename { + name: None, + rename: None, + }; + assert_eq!(rename.rename("test"), "test"); + assert_eq!(rename.rename("Test"), "Test"); + assert_eq!(rename.rename("TEST"), "TEST"); + } + + #[test] + fn test_rename_magic() { + for &(magic, expected) in &[ + ("__construct", "__construct"), + ("__destruct", "__destruct"), + ("__call", "__call"), + ("__call_static", "__callStatic"), + ("__get", "__get"), + ("__set", "__set"), + ("__isset", "__isset"), + ("__unset", "__unset"), + ("__sleep", "__sleep"), + ("__wakeup", "__wakeup"), + ("__serialize", "__serialize"), + ("__unserialize", "__unserialize"), + ("__to_string", "__toString"), + ("__invoke", "__invoke"), + ("__set_state", "__set_state"), + ("__clone", "__clone"), + ("__debug_info", "__debugInfo"), + ] { + assert_eq!(magic, RenameRule::None.rename(magic)); + assert_eq!(expected, RenameRule::Camel.rename(magic)); + assert_eq!(expected, RenameRule::Pascal.rename(magic)); + assert_eq!(expected, RenameRule::Snake.rename(magic)); + assert_eq!(expected, RenameRule::ScreamingSnakeCase.rename(magic)); + } + } + + #[test] + fn test_rename_php_methods() { + let &(original, camel, snake, pascal, screaming_snake) = + &("get_name", "getName", "get_name", "GetName", "GET_NAME"); + assert_eq!(original, RenameRule::None.rename(original)); + assert_eq!(camel, RenameRule::Camel.rename(original)); + assert_eq!(pascal, RenameRule::Pascal.rename(original)); + assert_eq!(snake, RenameRule::Snake.rename(original)); + assert_eq!( + screaming_snake, + RenameRule::ScreamingSnakeCase.rename(original) + ); + } +} diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 411c4865ba..6e513a3982 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -1,18 +1,19 @@ +#![allow(missing_docs)] #![cfg_attr(windows, feature(abi_vectorcall))] use ext_php_rs::{constant::IntoConst, prelude::*, types::ZendClassObject}; #[derive(Debug)] #[php_class] pub struct TestClass { - #[prop] + #[php(prop)] a: i32, - #[prop] + #[php(prop)] b: i32, } #[php_impl] impl TestClass { - #[rename("NEW_CONSTANT_NAME")] + #[php(name = "NEW_CONSTANT_NAME")] pub const SOME_CONSTANT: i32 = 5; pub const SOME_OTHER_STR: &'static str = "Hello, world!"; @@ -23,8 +24,7 @@ impl TestClass { } } - #[optional(test)] - #[defaults(a = 5, test = 100)] + #[php(defaults(a = 5, test = 100))] pub fn test_camel_case(&self, a: i32, test: i32) { println!("a: {} test: {}", a, test); } diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 333504056c..d5e18495c2 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -32,6 +32,7 @@ - [Constants](./macros/constant.md) - [PHP Functions](./macros/extern.md) - [`ZvalConvert`](./macros/zval_convert.md) + - [`Attributes`](./macros/php.md) - [Exceptions](./exceptions.md) - [INI Settings](./ini-settings.md) diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 5588a6f7a8..f959b1faef 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -15,21 +15,23 @@ The attribute takes some options to modify the output of the class: There are also additional macros that modify the class. These macros **must** be placed underneath the `#[php_class]` attribute. -- `#[extends(ce)]` - Sets the parent class of the class. Can only be used once. +- `#[php(extends = ce)]` - Sets the parent class of the class. Can only be used once. `ce` must be a function with the signature `fn() -> &'static ClassEntry`. -- `#[implements(ce)]` - Implements the given interface on the class. Can be used +- `#[php(implements = ce)]` - Implements the given interface on the class. Can be used multiple times. `ce` must be a valid function with the signature `fn() -> &'static ClassEntry`. -You may also use the `#[prop]` attribute on a struct field to use the field as a +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: -- `rename` - Allows you to rename the property, e.g. - `#[prop(rename = "new_name")]` +- `name` - Allows you to rename the property, e.g. + `#[php(name = "new_name")]` +- `rename` - Allows you to rename the property using rename rules, e.g. + `#[php(rename = PascalCase)]` ## Restrictions @@ -72,7 +74,7 @@ use ext_php_rs::prelude::*; pub struct Human { name: String, age: i32, - #[prop] + #[php(prop)] address: String, } @@ -96,7 +98,7 @@ use ext_php_rs::{ }; #[php_class(name = "Redis\\Exception\\RedisException")] -#[extends(ce::exception)] +#[php(extends = ce::exception)] #[derive(Default)] pub struct RedisException; @@ -117,7 +119,7 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { ## Implementing an Interface -To implement an interface, use `#[implements(ce)]` where `ce` is an function returning a `ClassEntry`. +To implement an interface, use `#[php(implements = ce)]` where `ce` is an function returning a `ClassEntry`. The following example implements [`ArrayAccess`](https://www.php.net/manual/en/class.arrayaccess.php): ````rust,no_run @@ -131,7 +133,7 @@ use ext_php_rs::{ }; #[php_class] -#[implements(ce::arrayaccess)] +#[php(implements = ce::arrayaccess)] #[derive(Default)] pub struct EvenNumbersArray; diff --git a/guide/src/macros/constant.md b/guide/src/macros/constant.md index a7198b853c..99dee3f0e9 100644 --- a/guide/src/macros/constant.md +++ b/guide/src/macros/constant.md @@ -6,6 +6,13 @@ 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. +You can rename the const with options: + +- `name` - Allows you to rename the property, e.g. + `#[php(name = "new_name")]` +- `rename` - Allows you to rename the property using rename rules, e.g. + `#[php(rename = PascalCase)]` + ## Examples ```rust,no_run @@ -16,6 +23,10 @@ use ext_php_rs::prelude::*; #[php_const] const TEST_CONSTANT: i32 = 100; +#[php_const] +#[php(name = "I_AM_RENAMED")] +const TEST_CONSTANT_THE_SECOND: i32 = 42; + #[php_const] const ANOTHER_STRING_CONST: &'static str = "Hello world!"; @@ -23,6 +34,7 @@ const ANOTHER_STRING_CONST: &'static str = "Hello world!"; pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module .constant(wrap_constant!(TEST_CONSTANT)) + .constant(wrap_constant!(TEST_CONSTANT_THE_SECOND)) .constant(("MANUAL_CONSTANT", ANOTHER_STRING_CONST, &[])) } # fn main() {} @@ -34,5 +46,6 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { ModuleBuilder { ``` Default parameter values can also be set for optional parameters. This is done -through the `defaults` attribute option. When an optional parameter has a +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 @@ -45,7 +45,8 @@ default, it does not need to be a variant of `Option`: # extern crate ext_php_rs; use ext_php_rs::prelude::*; -#[php_function(defaults(offset = 0))] +#[php_function] +#[php(defaults(offset = 0))] pub fn rusty_strpos(haystack: &str, needle: &str, offset: i64) -> Option { let haystack: String = haystack.chars().skip(offset as usize).collect(); haystack.find(needle) @@ -98,7 +99,8 @@ use ext_php_rs::prelude::*; /// `age` will be deemed required and nullable rather than optional, /// while description will be optional. -#[php_function(optional = "description")] +#[php_function] +#[php(optional = "description")] pub fn greet(name: String, age: Option, description: Option) -> String { let mut greeting = format!("Hello, {}!", name); diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index c76c4205a1..3c437c7c1b 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -37,16 +37,16 @@ For example, to disable renaming, change the `#[php_impl]` attribute to The rest of the options are passed as separate attributes: -- `#[defaults(i = 5, b = "hello")]` - Sets the default value for parameter(s). -- `#[optional(i)]` - Sets the first optional parameter. Note that this also sets +- `#[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`. -- `#[public]`, `#[protected]` and `#[private]` - Sets the visibility of the +- `#[php(public)]`, `#[php(protected)]` and `#[php(private)]` - Sets the visibility of the method. -- `#[rename("method_name")]` - Renames the PHP method to a different identifier, +- `#[php(name = "method_name")]` - Renames the PHP method to a different identifier, without renaming the Rust method name. -The `#[defaults]` and `#[optional]` attributes operate the same as the +The `#[php(defaults)]` and `#[php(optional)]` attributes operate the same as the equivalent function attribute parameters. ### Constructors @@ -60,7 +60,7 @@ 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 `#[constructor]` attribute. Note that when using +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. @@ -74,7 +74,7 @@ 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 `#[getter]` and `#[setter]` attributes. By +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. @@ -84,7 +84,7 @@ option to the 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. -The `#[getter]` and `#[setter]` attributes are mutually exclusive on methods. +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. @@ -107,7 +107,7 @@ use ext_php_rs::{prelude::*, types::ZendClassObject}; pub struct Human { name: String, age: i32, - #[prop] + #[php(prop)] address: String, } @@ -124,17 +124,17 @@ impl Human { } } - #[getter] + #[php(getter)] pub fn get_name(&self) -> String { self.name.to_string() } - #[setter] + #[php(setter)] pub fn set_name(&mut self, name: String) { self.name = name; } - #[getter] + #[php(getter)] pub fn get_age(&self) -> i32 { self.age } diff --git a/guide/src/macros/index.md b/guide/src/macros/index.md index d06f8cea14..9fdcd56a93 100644 --- a/guide/src/macros/index.md +++ b/guide/src/macros/index.md @@ -14,6 +14,8 @@ 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`] - 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. [`php_module`]: ./module.md [`php_function`]: ./function.md @@ -21,3 +23,4 @@ used from PHP without fiddling around with zvals. [`php_impl`]: ./impl.md [`php_const`]: ./constant.md [`php_extern`]: ./extern.md +[`php`]: ./php.md diff --git a/guide/src/macros/php.md b/guide/src/macros/php.md new file mode 100644 index 0000000000..09b8e45347 --- /dev/null +++ b/guide/src/macros/php.md @@ -0,0 +1,53 @@ +# `#[php]` Attributes + +There are a number of attributes that can be used to annotate elements in your +extension. + +Multiple `#[php]` attributes will be combined. For example, the following will +be identical: + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +# use ext_php_rs::prelude::*; +#[php_function] +#[php(name = "hi_world")] +#[php(defaults(a = 1, b = 2))] +fn hello_world(a: i32, b: i32) -> i32 { + a + b +} +# fn main() {} +``` + +```rust,no_run +# #![cfg_attr(windows, feature(abi_vectorcall))] +# extern crate ext_php_rs; +# use ext_php_rs::prelude::*; +#[php_function] +#[php(name = "hi_world", defaults(a = 1, b = 2))] +fn hello_world(a: i32, b: i32) -> i32 { + a + b +} +# fn main() {} +``` + +Which attributes are available depends on the element you are annotating: + +| Attribute | `const` | `fn` | `struct` | `struct` Field | `impl` | `impl` `const` | `impl` `fn` | +| ---------------- | ------- | ---- | -------- | -------------- | ------ | -------------- | ----------- | +| name | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| rename | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| rename_methods | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| rename_constants | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| flags | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | +| prop | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | +| extends | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| implements | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| modifier | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | +| defaults | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | +| optional | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | +| vis | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | +| getter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| setter | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| constructor | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | +| abstract_method | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | diff --git a/guide/src/migration-guides/v0.14.md b/guide/src/migration-guides/v0.14.md index 1ff61e1e01..ea3831f44b 100644 --- a/guide/src/migration-guides/v0.14.md +++ b/guide/src/migration-guides/v0.14.md @@ -44,6 +44,13 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { } ``` +**Supported `#[php]` attributes:** +- `#[php(name = "NEW_NAME")]` - Renames the function +- `#[php(rename = case)]` - Changes the case of the function name +- `#[php(vis = "public")]` - Changes the visibility of the function +- `#[php(defaults(a = 5, test = 100))]` - Sets default values for function arguments +- `#[php(variadic)]` - Marks the function as variadic. The last argument must be a `&[&Zval]` + ### Classes Mostly unchanged in terms of the class and impl definitions, however you now @@ -55,15 +62,15 @@ use ext_php_rs::prelude::*; #[php_class] #[derive(Debug)] pub struct TestClass { - #[prop] + #[php(prop)] a: i32, - #[prop] + #[php(prop)] b: i32, } #[php_impl] impl TestClass { - #[rename("NEW_CONSTANT_NAME")] + #[php(name = "NEW_CONSTANT_NAME")] pub const SOME_CONSTANT: i32 = 5; pub const SOME_OTHER_STR: &'static str = "Hello, world!"; @@ -71,8 +78,7 @@ impl TestClass { Self { a: a + 10, b: b + 10 } } - #[optional(test)] - #[defaults(a = 5, test = 100)] + #[php(defaults(a = 5, test = 100))] pub fn test_camel_case(&self, a: i32, test: i32) { println!("a: {} test: {}", a, test); } @@ -95,6 +101,20 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { } ``` +**Supported `#[php]` attributes (`struct`):** +- `#[php(name = "NEW_NAME")]` - Renames the class +- `#[php(rename = case)]` - Changes the case of the class name +- `#[php(vis = "public")]` - Changes the visibility of the class +- `#[php(extends = "ParentClass")]` - Extends a parent class +- `#[php(implements = "Interface")]` - Implements an interface +- `#[php(prop)]` - Marks a field as a property + +**Supported `#[php]` attributes (`impl`):** +- `#[php(rename_consts = case)]` - Changes the case of the constant names. Can be overridden by attributes on the constants. +- `#[php(rename_methods = case)]` - Changes the case of the method names. Can be overridden by attributes on the methods. + +For elements in the `#[php_impl]` block see the respective function and constant attributes. + ### Constants Mostly unchanged in terms of constant definition, however you now need to @@ -106,14 +126,24 @@ use ext_php_rs::prelude::*; #[php_const] const SOME_CONSTANT: i32 = 100; +#[php_const] +#[php(name = "HELLO_WORLD")] +const SOME_OTHER_CONSTANT: &'static str = "Hello, world!"; + #[php_module] pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module .constant(wrap_constant!(SOME_CONSTANT)) // SOME_CONSTANT = 100 + .constant(wrap_constant!(SOME_OTHER_CONSTANT)) // HELLO_WORLD = "Hello, world!" .constant(("CONST_NAME", SOME_CONSTANT, &[])) // CONST_NAME = 100 } ``` +**Supported `#[php]` attributes:** +- `#[php(name = "NEW_NAME")]` - Renames the constant +- `#[php(rename = case)]` - Changes the case of the constant name +- `#[php(vis = "public")]` - Changes the visibility of the constant + ### Extern No changes. @@ -149,3 +179,18 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { module } ``` + +### `#[php]` Attributes + +Attributes like `#[rename]` or `#[prop]` have been moved to the `#[php]` attribute. + +The `#[php]` attribute on an item are combined with each other. This means that +the following variants are equivalent: +```rs +#[php(rename = case)] +#[php(vis = "public")] +``` + +```rs +#[php(rename = case, vis = "public")] +``` diff --git a/src/exception.rs b/src/exception.rs index 5465ecc10b..1f2b201308 100644 --- a/src/exception.rs +++ b/src/exception.rs @@ -187,13 +187,13 @@ pub fn throw_with_code(ex: &ClassEntry, code: i32, message: &str) -> Result<()> /// use crate::ext_php_rs::convert::IntoZval; /// /// #[php_class] -/// #[extends(ext_php_rs::zend::ce::exception)] +/// #[php(extends = ext_php_rs::zend::ce::exception)] /// pub struct JsException { -/// #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)] +/// #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)] /// message: String, -/// #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)] +/// #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)] /// code: i32, -/// #[prop(flags = ext_php_rs::flags::PropertyFlags::Public)] +/// #[php(prop, flags = ext_php_rs::flags::PropertyFlags::Public)] /// file: String, /// } /// diff --git a/tests/src/lib.rs b/tests/src/lib.rs index 3839062381..c9cc15c81b 100644 --- a/tests/src/lib.rs +++ b/tests/src/lib.rs @@ -185,28 +185,28 @@ pub fn test_variadic_add_required(number: u32, numbers: &[&Zval]) -> u32 { pub struct TestClass { string: String, number: i32, - #[prop] + #[php(prop)] boolean: bool, } #[php_impl] impl TestClass { - #[getter] + #[php(getter)] pub fn get_string(&self) -> String { self.string.to_string() } - #[setter] + #[php(setter)] pub fn set_string(&mut self, string: String) { self.string = string; } - #[getter] + #[php(getter)] pub fn get_number(&self) -> i32 { self.number } - #[setter] + #[php(setter)] pub fn set_number(&mut self, number: i32) { self.number = number; }