Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

- Constructors that return `Self` can now be added to classes. [#83]
- `Default` is no longer required to be implemented on classes, however, a
constructor must be specified if you want to construct the class from PHP.
- Constructors can return `Self` or `Result<Self, E>`, where
`E: Into<PhpException>`.

[#83]: https://github.com/davidcole1340/ext-php-rs/pull/83

## Version 0.5.1

- `PhpException` no longer requires a lifetime [#80].
Expand Down
1 change: 1 addition & 0 deletions ext-php-rs-derive/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ pub struct Class {
pub parent: Option<String>,
pub interfaces: Vec<String>,
pub methods: Vec<crate::method::Method>,
pub constructor: Option<crate::method::Method>,
pub constants: Vec<crate::constant::Constant>,
pub properties: HashMap<String, Property>,
}
Expand Down
13 changes: 8 additions & 5 deletions ext-php-rs-derive/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pub fn parser(args: AttributeArgs, input: ItemFn) -> Result<(TokenStream, Functi
let args = build_args(inputs, &attr_args.defaults)?;
let optional = find_optional_parameter(args.iter(), attr_args.optional);
let arg_definitions = build_arg_definitions(&args);
let arg_parser = build_arg_parser(args.iter(), &optional)?;
let arg_parser = build_arg_parser(args.iter(), &optional, &quote! { return; })?;
let arg_accessors = build_arg_accessors(&args);

let return_type = get_return_type(output)?;
Expand Down Expand Up @@ -157,6 +157,7 @@ pub fn find_optional_parameter<'a>(
pub fn build_arg_parser<'a>(
args: impl Iterator<Item = &'a Arg>,
optional: &Option<String>,
ret: &TokenStream,
) -> Result<TokenStream> {
let mut rest_optional = false;

Expand Down Expand Up @@ -194,13 +195,15 @@ pub fn build_arg_parser<'a>(
.parse();

if parser.is_err() {
return;
#ret
}
})
}

fn build_arg_accessors(args: &[Arg]) -> Vec<TokenStream> {
args.iter().map(|arg| arg.get_accessor()).collect()
args.iter()
.map(|arg| arg.get_accessor(&quote! { return; }))
.collect()
}

pub fn get_return_type(output_type: &ReturnType) -> Result<Option<(String, bool)>> {
Expand Down Expand Up @@ -286,7 +289,7 @@ impl Arg {
}

/// Returns a [`TokenStream`] containing the line required to retrieve the value from the argument.
pub fn get_accessor(&self) -> TokenStream {
pub fn get_accessor(&self, ret: &TokenStream) -> TokenStream {
let name = &self.name;
let name_ident = self.get_name_ident();

Expand All @@ -310,7 +313,7 @@ impl Arg {
)
.throw()
.expect(concat!("Failed to throw exception: Invalid value given for argument `", #name, "`."));
return;
#ret
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion ext-php-rs-derive/src/impl_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pub enum ParsedAttribute {
prop_name: Option<String>,
ty: PropAttrTy,
},
Constructor,
}

#[derive(Default, Debug, FromMeta)]
Expand Down Expand Up @@ -151,7 +152,14 @@ pub fn parser(args: AttributeArgs, input: ItemImpl) -> Result<TokenStream> {
PropAttrTy::Setter => prop.add_setter(ident)?,
}
}
class.methods.push(parsed_method.method);
if parsed_method.constructor {
if class.constructor.is_some() {
bail!("You cannot have two constructors on the same class.");
}
class.constructor = Some(parsed_method.method);
} else {
class.methods.push(parsed_method.method);
}
parsed_method.tokens
}
item => item.to_token_stream(),
Expand Down Expand Up @@ -239,6 +247,7 @@ pub fn parse_attribute(attr: &Attribute) -> Result<ParsedAttribute> {
ty: PropAttrTy::Setter,
}
}
"constructor" => ParsedAttribute::Constructor,
attr => bail!("Invalid attribute `#[{}]`.", attr),
})
}
Expand Down
112 changes: 77 additions & 35 deletions ext-php-rs-derive/src/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,21 @@ pub struct ParsedMethod {
pub tokens: TokenStream,
pub method: Method,
pub property: Option<(String, PropAttrTy)>,
pub constructor: bool,
}

impl ParsedMethod {
pub fn new(
tokens: TokenStream,
method: Method,
property: Option<(String, PropAttrTy)>,
constructor: bool,
) -> Self {
Self {
tokens,
method,
property,
constructor,
}
}
}
Expand All @@ -64,6 +67,7 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
let mut visibility = Visibility::Public;
let mut as_prop = None;
let mut identifier = None;
let mut is_constructor = false;

for attr in input.attrs.iter() {
match parse_attribute(attr)? {
Expand All @@ -89,6 +93,7 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
});
as_prop = Some((prop_name, ty))
}
ParsedAttribute::Constructor => is_constructor = true,
}
}

Expand All @@ -102,6 +107,20 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
..
} = &sig;

let name = identifier.unwrap_or_else(|| rename_rule.rename(ident.to_string()));
if name == "__construct" {
is_constructor = true;
}

if is_constructor && (!matches!(visibility, Visibility::Public) || as_prop.is_some()) {
bail!("`#[constructor]` attribute cannot be combined with the visibility or getter/setter attributes.");
}

let bail = if is_constructor {
quote! { return ConstructorResult::ArgError; }
} else {
quote! { return; }
};
let internal_ident = Ident::new(&format!("_internal_php_{}", ident), Span::call_site());
let args = build_args(inputs, &defaults)?;
let optional = function::find_optional_parameter(
Expand All @@ -112,34 +131,55 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
optional,
);
let (arg_definitions, is_static) = build_arg_definitions(&args);
let arg_parser = build_arg_parser(args.iter(), &optional)?;
let arg_accessors = build_arg_accessors(&args);
let arg_parser = build_arg_parser(args.iter(), &optional, &bail)?;
let arg_accessors = build_arg_accessors(&args, &bail);
let this = if is_static {
quote! { Self:: }
} else {
quote! { this. }
};

let func = quote! {
#input
let func = if is_constructor {
quote! {
#input

#[doc(hidden)]
pub fn #internal_ident(
ex: &mut ::ext_php_rs::php::execution_data::ExecutionData
) -> ::ext_php_rs::php::types::object::ConstructorResult<Self> {
use ::ext_php_rs::php::types::zval::IntoZval;
use ::ext_php_rs::php::types::object::ConstructorResult;

#(#arg_definitions)*
#arg_parser

Self::#ident(#(#arg_accessors,)*).into()
}
}
} else {
quote! {
#input

#[doc(hidden)]
pub extern "C" fn #internal_ident(ex: &mut ::ext_php_rs::php::execution_data::ExecutionData, retval: &mut ::ext_php_rs::php::types::zval::Zval) {
use ::ext_php_rs::php::types::zval::IntoZval;
#[doc(hidden)]
pub extern "C" fn #internal_ident(
ex: &mut ::ext_php_rs::php::execution_data::ExecutionData,
retval: &mut ::ext_php_rs::php::types::zval::Zval
) {
use ::ext_php_rs::php::types::zval::IntoZval;

#(#arg_definitions)*
#arg_parser
#(#arg_definitions)*
#arg_parser

let result = #this #ident(#(#arg_accessors, )*);
let result = #this #ident(#(#arg_accessors, )*);

if let Err(e) = result.set_zval(retval, false) {
let e: ::ext_php_rs::php::exceptions::PhpException = e.into();
e.throw().expect("Failed to throw exception");
if let Err(e) = result.set_zval(retval, false) {
let e: ::ext_php_rs::php::exceptions::PhpException = e.into();
e.throw().expect("Failed to throw exception");
}
}
}
};

let name = identifier.unwrap_or_else(|| rename_rule.rename(ident.to_string()));
let method = Method {
name,
ident: internal_ident.to_string(),
Expand All @@ -151,7 +191,7 @@ pub fn parser(input: &mut ImplItemMethod, rename_rule: RenameRule) -> Result<Par
visibility,
};

Ok(ParsedMethod::new(func, method, as_prop))
Ok(ParsedMethod::new(func, method, as_prop, is_constructor))
}

fn build_args(
Expand Down Expand Up @@ -216,20 +256,22 @@ fn build_arg_definitions(args: &[Arg]) -> (Vec<TokenStream>, bool) {
fn build_arg_parser<'a>(
args: impl Iterator<Item = &'a Arg>,
optional: &Option<String>,
ret: &TokenStream,
) -> Result<TokenStream> {
function::build_arg_parser(
args.filter_map(|arg| match arg {
Arg::Typed(arg) => Some(arg),
_ => None,
}),
optional,
ret,
)
}

fn build_arg_accessors(args: &[Arg]) -> Vec<TokenStream> {
fn build_arg_accessors(args: &[Arg], ret: &TokenStream) -> Vec<TokenStream> {
args.iter()
.filter_map(|arg| match arg {
Arg::Typed(arg) => Some(arg.get_accessor()),
Arg::Typed(arg) => Some(arg.get_accessor(ret)),
_ => None,
})
.collect()
Expand All @@ -241,27 +283,27 @@ impl Method {
Ident::new(&self.ident, Span::call_site())
}

pub fn get_arg_definitions(&self) -> impl Iterator<Item = TokenStream> + '_ {
self.args.iter().filter_map(move |arg| match arg {
Arg::Typed(arg) => {
let def = arg.get_arg_definition();
let prelude = self.optional.as_ref().and_then(|opt| {
if opt.eq(&arg.name) {
Some(quote! { .not_required() })
} else {
None
}
});
Some(quote! { #prelude.arg(#def) })
}
_ => None,
})
}

pub fn get_builder(&self, class_path: &Ident) -> TokenStream {
let name = &self.name;
let name_ident = self.get_name_ident();
let args = self
.args
.iter()
.filter_map(|arg| match arg {
Arg::Typed(arg) => {
let def = arg.get_arg_definition();
let prelude = self.optional.as_ref().and_then(|opt| {
if opt.eq(&arg.name) {
Some(quote! { .not_required() })
} else {
None
}
});
Some(quote! { #prelude.arg(#def) })
}
_ => None,
})
.collect::<Vec<_>>();
let args = self.get_arg_definitions();
let output = self.output.as_ref().map(|(ty, nullable)| {
let ty: Type = syn::parse_str(ty).unwrap();

Expand Down
22 changes: 22 additions & 0 deletions ext-php-rs-derive/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,34 @@ pub fn generate_registered_class_impl(class: &Class) -> Result<TokenStream> {
.properties
.iter()
.map(|(name, prop)| prop.as_prop_tuple(name));
let constructor = if let Some(constructor) = &class.constructor {
let func = Ident::new(&constructor.ident, Span::call_site());
let args = constructor.get_arg_definitions();
quote! {
Some(::ext_php_rs::php::types::object::ConstructorMeta {
constructor: Self::#func,
build_fn: {
use ext_php_rs::php::function::FunctionBuilder;
fn build_fn(func: FunctionBuilder) -> FunctionBuilder {
func
#(#args)*
}
build_fn
}
})
}
} else {
quote! { None }
};

Ok(quote! {
static #meta: ::ext_php_rs::php::types::object::ClassMetadata<#self_ty> = ::ext_php_rs::php::types::object::ClassMetadata::new();

impl ::ext_php_rs::php::types::object::RegisteredClass for #self_ty {
const CLASS_NAME: &'static str = #class_name;
const CONSTRUCTOR: ::std::option::Option<
::ext_php_rs::php::types::object::ConstructorMeta<Self>
> = #constructor;

fn get_metadata() -> &'static ::ext_php_rs::php::types::object::ClassMetadata<Self> {
&#meta
Expand Down
18 changes: 5 additions & 13 deletions guide/src/macros/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,6 @@ 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.

The implementation of `RegisteredClass` requires the implementation of `Default`
on the struct. This is because the struct is initialized before the constructor
is called, therefore it must have default values for all properties.

Note that Rust struct properties **are not** PHP properties, so if you want the
user to be able to access these, you must provide getters and/or setters.
Properties are supported internally, however, they are not usable through the
automatic macros. Support for properties is planned.

## Options

The attribute takes some options to modify the output of the class:
Expand All @@ -33,21 +24,22 @@ placed underneath the `#[php_class]` attribute.

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 publically with
the same name as the field. You can rename the property with options:
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")]`

## Example

This example creates a PHP class `Human`, adding a PHP property `address` with
an empty string as the default value.
This example creates a PHP class `Human`, adding a PHP property `address`.

```rust
# extern crate ext_php_rs;
# use ext_php_rs::prelude::*;
#[php_class]
#[derive(Default)]
pub struct Human {
name: String,
age: i32,
Expand Down
Loading