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
2 changes: 2 additions & 0 deletions allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ bind! {
zend_declare_class_constant,
zend_declare_property,
zend_do_implement_interface,
zend_read_static_property,
zend_update_static_property,
zend_enum_add_case,
zend_enum_get_case,
zend_enum_new,
Expand Down
56 changes: 51 additions & 5 deletions crates/macros/src/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ pub fn parser(mut input: ItemStruct) -> Result<TokenStream> {
#[darling(attributes(php), forward_attrs(doc), default)]
struct PropAttributes {
prop: Flag,
#[darling(rename = "static")]
static_: Flag,
#[darling(flatten)]
rename: PhpRename,
flags: Option<Expr>,
default: Option<Expr>,
attrs: Vec<Attribute>,
}

Expand Down Expand Up @@ -114,10 +117,14 @@ impl Property<'_> {
.rename
.rename(self.ident.to_string(), RenameRule::Camel)
}

pub fn is_static(&self) -> bool {
self.attr.static_.is_present()
}
}

/// Generates an implementation of `RegisteredClass` for struct `ident`.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
fn generate_registered_class_impl(
ident: &syn::Ident,
class_name: &str,
Expand All @@ -130,9 +137,14 @@ fn generate_registered_class_impl(
) -> TokenStream {
let modifier = modifier.option_tokens();

let fields = fields.iter().map(|prop| {
// Separate instance properties from static properties
let (instance_props, static_props): (Vec<_>, Vec<_>) =
fields.iter().partition(|prop| !prop.is_static());

// Generate instance properties (with Rust handlers)
let instance_fields = instance_props.iter().map(|prop| {
let name = prop.name();
let ident = prop.ident;
let field_ident = prop.ident;
let flags = prop
.attr
.flags
Expand All @@ -143,13 +155,41 @@ fn generate_registered_class_impl(

quote! {
(#name, ::ext_php_rs::internal::property::PropertyInfo {
prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#ident),
prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#field_ident),
flags: #flags,
docs: &[#(#docs,)*]
})
}
});

// Generate static properties (PHP-managed, no Rust handlers)
// We combine the base flags with Static flag using from_bits_retain which is
// const
let static_fields = static_props.iter().map(|prop| {
let name = prop.name();
let base_flags = prop
.attr
.flags
.as_ref()
.map(ToTokens::to_token_stream)
.unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public });
let docs = &prop.docs;

// Handle default value - if provided, wrap in Some(&value), otherwise None
let default_value = if let Some(expr) = &prop.attr.default {
quote! { ::std::option::Option::Some(&#expr as &'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)) }
} else {
quote! { ::std::option::Option::None }
};

// Use from_bits_retain to combine flags in a const context
quote! {
(#name, ::ext_php_rs::flags::PropertyFlags::from_bits_retain(
(#base_flags).bits() | ::ext_php_rs::flags::PropertyFlags::Static.bits()
), #default_value, &[#(#docs,)*] as &[&str])
}
});

let flags = match flags {
Some(flags) => flags.to_token_stream(),
None => quote! { ::ext_php_rs::flags::ClassFlags::empty() }.to_token_stream(),
Expand Down Expand Up @@ -204,10 +244,16 @@ fn generate_registered_class_impl(
> {
use ::std::iter::FromIterator;
::std::collections::HashMap::from_iter([
#(#fields,)*
#(#instance_fields,)*
])
}

#[must_use]
fn static_properties() -> &'static [(&'static str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &'static [&'static str])] {
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &[&str])] = &[#(#static_fields,)*];
STATIC_PROPS
}

#[inline]
fn method_builders() -> ::std::vec::Vec<
(::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)
Expand Down
104 changes: 101 additions & 3 deletions crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,16 @@ extern crate proc_macro;
/// publicly with the same name as the field. Property types must implement
/// `IntoZval` and `FromZval`.
///
/// You can rename the property with options:
/// You can customize properties with these options:
///
/// - `name` - Allows you to rename the property, e.g. `#[php(name =
/// - `name` - Allows you to rename the property, e.g. `#[php(prop, name =
/// "new_name")]`
/// - `change_case` - Allows you to rename the property using rename rules, e.g.
/// `#[php(change_case = PascalCase)]`
/// `#[php(prop, change_case = PascalCase)]`
/// - `static` - Makes the property static (shared across all instances), e.g.
/// `#[php(prop, static)]`
/// - `flags` - Sets property visibility flags, e.g. `#[php(prop, flags =
/// ext_php_rs::flags::PropertyFlags::Private)]`
///
/// ## Restrictions
///
Expand Down Expand Up @@ -204,6 +208,72 @@ extern crate proc_macro;
/// }
/// # fn main() {}
/// ````
///
/// ## Static Properties
///
/// Static properties are shared across all instances of a class. Use
/// `#[php(prop, static)]` to declare a static property. Unlike instance
/// properties, static properties are managed entirely by PHP and do not use
/// Rust property handlers.
///
/// You can specify a default value using the `default` attribute:
///
/// ```rust,no_run,ignore
/// # #![cfg_attr(windows, feature(abi_vectorcall))]
/// # extern crate ext_php_rs;
/// use ext_php_rs::prelude::*;
/// use ext_php_rs::class::RegisteredClass;
///
/// #[php_class]
/// pub struct Counter {
/// #[php(prop)]
/// pub instance_value: i32,
/// #[php(prop, static, default = 0)]
/// pub count: i32,
/// #[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)]
/// pub internal_state: String,
/// }
///
/// #[php_impl]
/// impl Counter {
/// pub fn __construct(value: i32) -> Self {
/// Self {
/// instance_value: value,
/// count: 0,
/// internal_state: String::new(),
/// }
/// }
///
/// /// Increment the static counter from Rust
/// pub fn increment() {
/// let ce = Self::get_metadata().ce();
/// let current: i64 = ce.get_static_property("count").unwrap_or(0);
/// ce.set_static_property("count", current + 1).unwrap();
/// }
///
/// /// Get the current count
/// pub fn get_count() -> i64 {
/// let ce = Self::get_metadata().ce();
/// ce.get_static_property("count").unwrap_or(0)
/// }
/// }
///
/// #[php_module]
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
/// module.class::<Counter>()
/// }
/// # fn main() {}
/// ```
///
/// From PHP, you can access static properties directly on the class:
///
/// ```php
/// // No need to initialize - count already has default value of 0
/// Counter::increment();
/// Counter::increment();
/// echo Counter::$count; // 2
/// echo Counter::getCount(); // 2
/// ```
// END DOCS FROM classes.md
#[proc_macro_attribute]
pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream {
Expand Down Expand Up @@ -812,6 +882,34 @@ fn php_module_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2
/// The `#[php(defaults)]` and `#[php(optional)]` attributes operate the same as
/// the equivalent function attribute parameters.
///
/// ### Static Methods
///
/// Methods that do not take a `&self` or `&mut self` parameter are
/// automatically exported as static methods. These can be called on the class
/// itself without creating an instance.
///
/// ```rust,ignore
/// #[php_impl]
/// impl MyClass {
/// // Static method - no self parameter
/// pub fn create_default() -> Self {
/// Self { /* ... */ }
/// }
///
/// // Instance method - takes &self
/// pub fn get_value(&self) -> i32 {
/// self.value
/// }
/// }
/// ```
///
/// From PHP:
///
/// ```php
/// $obj = MyClass::createDefault(); // Static call
/// $val = $obj->getValue(); // Instance call
/// ```
///
/// ### Constructors
///
/// By default, if a class does not have a constructor, it is not constructable
Expand Down
17 changes: 17 additions & 0 deletions crates/macros/tests/expand/class.expanded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,23 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass {
use ::std::iter::FromIterator;
::std::collections::HashMap::from_iter([])
}
#[must_use]
fn static_properties() -> &'static [(
&'static str,
::ext_php_rs::flags::PropertyFlags,
::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>,
&'static [&'static str],
)] {
static STATIC_PROPS: &[(
&str,
::ext_php_rs::flags::PropertyFlags,
::std::option::Option<
&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync),
>,
&[&str],
)] = &[];
STATIC_PROPS
}
#[inline]
fn method_builders() -> ::std::vec::Vec<
(
Expand Down
16 changes: 16 additions & 0 deletions docsrs_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2108,6 +2108,22 @@ unsafe extern "C" {
value: *mut zval,
);
}
unsafe extern "C" {
pub fn zend_update_static_property(
scope: *mut zend_class_entry,
name: *const ::std::os::raw::c_char,
name_length: usize,
value: *mut zval,
) -> zend_result;
}
unsafe extern "C" {
pub fn zend_read_static_property(
scope: *mut zend_class_entry,
name: *const ::std::os::raw::c_char,
name_length: usize,
silent: bool,
) -> *mut zval;
}
unsafe extern "C" {
pub fn object_properties_init(object: *mut zend_object, class_type: *mut zend_class_entry);
}
Expand Down
75 changes: 72 additions & 3 deletions guide/src/macros/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,16 @@ 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:
You can customize properties with these options:

- `name` - Allows you to rename the property, e.g.
`#[php(name = "new_name")]`
`#[php(prop, name = "new_name")]`
- `change_case` - Allows you to rename the property using rename rules, e.g.
`#[php(change_case = PascalCase)]`
`#[php(prop, change_case = PascalCase)]`
- `static` - Makes the property static (shared across all instances), e.g.
`#[php(prop, static)]`
- `flags` - Sets property visibility flags, e.g.
`#[php(prop, flags = ext_php_rs::flags::PropertyFlags::Private)]`

## Restrictions

Expand Down Expand Up @@ -174,3 +178,68 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
}
# fn main() {}
````

## Static Properties

Static properties are shared across all instances of a class. Use `#[php(prop, static)]`
to declare a static property. Unlike instance properties, static properties are managed
entirely by PHP and do not use Rust property handlers.

You can specify a default value using the `default` attribute:

```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::class::RegisteredClass;
#[php_class]
pub struct Counter {
#[php(prop)]
pub instance_value: i32,
#[php(prop, static, default = 0)]
pub count: i32,
#[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)]
pub internal_state: String,
}
#[php_impl]
impl Counter {
pub fn __construct(value: i32) -> Self {
Self {
instance_value: value,
count: 0,
internal_state: String::new(),
}
}
/// Increment the static counter from Rust
pub fn increment() {
let ce = Self::get_metadata().ce();
let current: i64 = ce.get_static_property("count").unwrap_or(0);
ce.set_static_property("count", current + 1).unwrap();
}
/// Get the current count
pub fn get_count() -> i64 {
let ce = Self::get_metadata().ce();
ce.get_static_property("count").unwrap_or(0)
}
}
#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.class::<Counter>()
}
# fn main() {}
```

From PHP, you can access static properties directly on the class:

```php
// No need to initialize - count already has default value of 0
Counter::increment();
Counter::increment();
echo Counter::$count; // 2
echo Counter::getCount(); // 2
```
Loading