Skip to content

Commit b511af6

Browse files
authored
feat(class): Static properties and methods #252 (#627)
* feat(class): Static properties and methods #252 * feat(class): Static properties and methods #252
1 parent b4d16f7 commit b511af6

File tree

14 files changed

+510
-34
lines changed

14 files changed

+510
-34
lines changed

allowed_bindings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ bind! {
8787
zend_declare_class_constant,
8888
zend_declare_property,
8989
zend_do_implement_interface,
90+
zend_read_static_property,
91+
zend_update_static_property,
9092
zend_enum_add_case,
9193
zend_enum_get_case,
9294
zend_enum_new,

crates/macros/src/class.rs

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,12 @@ pub fn parser(mut input: ItemStruct) -> Result<TokenStream> {
7676
#[darling(attributes(php), forward_attrs(doc), default)]
7777
struct PropAttributes {
7878
prop: Flag,
79+
#[darling(rename = "static")]
80+
static_: Flag,
7981
#[darling(flatten)]
8082
rename: PhpRename,
8183
flags: Option<Expr>,
84+
default: Option<Expr>,
8285
attrs: Vec<Attribute>,
8386
}
8487

@@ -114,10 +117,14 @@ impl Property<'_> {
114117
.rename
115118
.rename(self.ident.to_string(), RenameRule::Camel)
116119
}
120+
121+
pub fn is_static(&self) -> bool {
122+
self.attr.static_.is_present()
123+
}
117124
}
118125

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

133-
let fields = fields.iter().map(|prop| {
140+
// Separate instance properties from static properties
141+
let (instance_props, static_props): (Vec<_>, Vec<_>) =
142+
fields.iter().partition(|prop| !prop.is_static());
143+
144+
// Generate instance properties (with Rust handlers)
145+
let instance_fields = instance_props.iter().map(|prop| {
134146
let name = prop.name();
135-
let ident = prop.ident;
147+
let field_ident = prop.ident;
136148
let flags = prop
137149
.attr
138150
.flags
@@ -143,13 +155,41 @@ fn generate_registered_class_impl(
143155

144156
quote! {
145157
(#name, ::ext_php_rs::internal::property::PropertyInfo {
146-
prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#ident),
158+
prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#field_ident),
147159
flags: #flags,
148160
docs: &[#(#docs,)*]
149161
})
150162
}
151163
});
152164

165+
// Generate static properties (PHP-managed, no Rust handlers)
166+
// We combine the base flags with Static flag using from_bits_retain which is
167+
// const
168+
let static_fields = static_props.iter().map(|prop| {
169+
let name = prop.name();
170+
let base_flags = prop
171+
.attr
172+
.flags
173+
.as_ref()
174+
.map(ToTokens::to_token_stream)
175+
.unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public });
176+
let docs = &prop.docs;
177+
178+
// Handle default value - if provided, wrap in Some(&value), otherwise None
179+
let default_value = if let Some(expr) = &prop.attr.default {
180+
quote! { ::std::option::Option::Some(&#expr as &'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)) }
181+
} else {
182+
quote! { ::std::option::Option::None }
183+
};
184+
185+
// Use from_bits_retain to combine flags in a const context
186+
quote! {
187+
(#name, ::ext_php_rs::flags::PropertyFlags::from_bits_retain(
188+
(#base_flags).bits() | ::ext_php_rs::flags::PropertyFlags::Static.bits()
189+
), #default_value, &[#(#docs,)*] as &[&str])
190+
}
191+
});
192+
153193
let flags = match flags {
154194
Some(flags) => flags.to_token_stream(),
155195
None => quote! { ::ext_php_rs::flags::ClassFlags::empty() }.to_token_stream(),
@@ -204,10 +244,16 @@ fn generate_registered_class_impl(
204244
> {
205245
use ::std::iter::FromIterator;
206246
::std::collections::HashMap::from_iter([
207-
#(#fields,)*
247+
#(#instance_fields,)*
208248
])
209249
}
210250

251+
#[must_use]
252+
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])] {
253+
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, ::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>, &[&str])] = &[#(#static_fields,)*];
254+
STATIC_PROPS
255+
}
256+
211257
#[inline]
212258
fn method_builders() -> ::std::vec::Vec<
213259
(::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)

crates/macros/src/lib.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,16 @@ extern crate proc_macro;
5151
/// publicly with the same name as the field. Property types must implement
5252
/// `IntoZval` and `FromZval`.
5353
///
54-
/// You can rename the property with options:
54+
/// You can customize properties with these options:
5555
///
56-
/// - `name` - Allows you to rename the property, e.g. `#[php(name =
56+
/// - `name` - Allows you to rename the property, e.g. `#[php(prop, name =
5757
/// "new_name")]`
5858
/// - `change_case` - Allows you to rename the property using rename rules, e.g.
59-
/// `#[php(change_case = PascalCase)]`
59+
/// `#[php(prop, change_case = PascalCase)]`
60+
/// - `static` - Makes the property static (shared across all instances), e.g.
61+
/// `#[php(prop, static)]`
62+
/// - `flags` - Sets property visibility flags, e.g. `#[php(prop, flags =
63+
/// ext_php_rs::flags::PropertyFlags::Private)]`
6064
///
6165
/// ## Restrictions
6266
///
@@ -204,6 +208,72 @@ extern crate proc_macro;
204208
/// }
205209
/// # fn main() {}
206210
/// ````
211+
///
212+
/// ## Static Properties
213+
///
214+
/// Static properties are shared across all instances of a class. Use
215+
/// `#[php(prop, static)]` to declare a static property. Unlike instance
216+
/// properties, static properties are managed entirely by PHP and do not use
217+
/// Rust property handlers.
218+
///
219+
/// You can specify a default value using the `default` attribute:
220+
///
221+
/// ```rust,no_run,ignore
222+
/// # #![cfg_attr(windows, feature(abi_vectorcall))]
223+
/// # extern crate ext_php_rs;
224+
/// use ext_php_rs::prelude::*;
225+
/// use ext_php_rs::class::RegisteredClass;
226+
///
227+
/// #[php_class]
228+
/// pub struct Counter {
229+
/// #[php(prop)]
230+
/// pub instance_value: i32,
231+
/// #[php(prop, static, default = 0)]
232+
/// pub count: i32,
233+
/// #[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)]
234+
/// pub internal_state: String,
235+
/// }
236+
///
237+
/// #[php_impl]
238+
/// impl Counter {
239+
/// pub fn __construct(value: i32) -> Self {
240+
/// Self {
241+
/// instance_value: value,
242+
/// count: 0,
243+
/// internal_state: String::new(),
244+
/// }
245+
/// }
246+
///
247+
/// /// Increment the static counter from Rust
248+
/// pub fn increment() {
249+
/// let ce = Self::get_metadata().ce();
250+
/// let current: i64 = ce.get_static_property("count").unwrap_or(0);
251+
/// ce.set_static_property("count", current + 1).unwrap();
252+
/// }
253+
///
254+
/// /// Get the current count
255+
/// pub fn get_count() -> i64 {
256+
/// let ce = Self::get_metadata().ce();
257+
/// ce.get_static_property("count").unwrap_or(0)
258+
/// }
259+
/// }
260+
///
261+
/// #[php_module]
262+
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
263+
/// module.class::<Counter>()
264+
/// }
265+
/// # fn main() {}
266+
/// ```
267+
///
268+
/// From PHP, you can access static properties directly on the class:
269+
///
270+
/// ```php
271+
/// // No need to initialize - count already has default value of 0
272+
/// Counter::increment();
273+
/// Counter::increment();
274+
/// echo Counter::$count; // 2
275+
/// echo Counter::getCount(); // 2
276+
/// ```
207277
// END DOCS FROM classes.md
208278
#[proc_macro_attribute]
209279
pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream {
@@ -812,6 +882,34 @@ fn php_module_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2
812882
/// The `#[php(defaults)]` and `#[php(optional)]` attributes operate the same as
813883
/// the equivalent function attribute parameters.
814884
///
885+
/// ### Static Methods
886+
///
887+
/// Methods that do not take a `&self` or `&mut self` parameter are
888+
/// automatically exported as static methods. These can be called on the class
889+
/// itself without creating an instance.
890+
///
891+
/// ```rust,ignore
892+
/// #[php_impl]
893+
/// impl MyClass {
894+
/// // Static method - no self parameter
895+
/// pub fn create_default() -> Self {
896+
/// Self { /* ... */ }
897+
/// }
898+
///
899+
/// // Instance method - takes &self
900+
/// pub fn get_value(&self) -> i32 {
901+
/// self.value
902+
/// }
903+
/// }
904+
/// ```
905+
///
906+
/// From PHP:
907+
///
908+
/// ```php
909+
/// $obj = MyClass::createDefault(); // Static call
910+
/// $val = $obj->getValue(); // Instance call
911+
/// ```
912+
///
815913
/// ### Constructors
816914
///
817915
/// By default, if a class does not have a constructor, it is not constructable

crates/macros/tests/expand/class.expanded.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass {
2727
use ::std::iter::FromIterator;
2828
::std::collections::HashMap::from_iter([])
2929
}
30+
#[must_use]
31+
fn static_properties() -> &'static [(
32+
&'static str,
33+
::ext_php_rs::flags::PropertyFlags,
34+
::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>,
35+
&'static [&'static str],
36+
)] {
37+
static STATIC_PROPS: &[(
38+
&str,
39+
::ext_php_rs::flags::PropertyFlags,
40+
::std::option::Option<
41+
&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync),
42+
>,
43+
&[&str],
44+
)] = &[];
45+
STATIC_PROPS
46+
}
3047
#[inline]
3148
fn method_builders() -> ::std::vec::Vec<
3249
(

docsrs_bindings.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2108,6 +2108,22 @@ unsafe extern "C" {
21082108
value: *mut zval,
21092109
);
21102110
}
2111+
unsafe extern "C" {
2112+
pub fn zend_update_static_property(
2113+
scope: *mut zend_class_entry,
2114+
name: *const ::std::os::raw::c_char,
2115+
name_length: usize,
2116+
value: *mut zval,
2117+
) -> zend_result;
2118+
}
2119+
unsafe extern "C" {
2120+
pub fn zend_read_static_property(
2121+
scope: *mut zend_class_entry,
2122+
name: *const ::std::os::raw::c_char,
2123+
name_length: usize,
2124+
silent: bool,
2125+
) -> *mut zval;
2126+
}
21112127
unsafe extern "C" {
21122128
pub fn object_properties_init(object: *mut zend_object, class_type: *mut zend_class_entry);
21132129
}

guide/src/macros/classes.md

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ PHP property. By default, the field will be accessible from PHP publicly with
2424
the same name as the field. Property types must implement `IntoZval` and
2525
`FromZval`.
2626

27-
You can rename the property with options:
27+
You can customize properties with these options:
2828

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

3438
## Restrictions
3539

@@ -174,3 +178,68 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
174178
}
175179
# fn main() {}
176180
````
181+
182+
## Static Properties
183+
184+
Static properties are shared across all instances of a class. Use `#[php(prop, static)]`
185+
to declare a static property. Unlike instance properties, static properties are managed
186+
entirely by PHP and do not use Rust property handlers.
187+
188+
You can specify a default value using the `default` attribute:
189+
190+
```rust,no_run
191+
# #![cfg_attr(windows, feature(abi_vectorcall))]
192+
# extern crate ext_php_rs;
193+
use ext_php_rs::prelude::*;
194+
use ext_php_rs::class::RegisteredClass;
195+
196+
#[php_class]
197+
pub struct Counter {
198+
#[php(prop)]
199+
pub instance_value: i32,
200+
#[php(prop, static, default = 0)]
201+
pub count: i32,
202+
#[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)]
203+
pub internal_state: String,
204+
}
205+
206+
#[php_impl]
207+
impl Counter {
208+
pub fn __construct(value: i32) -> Self {
209+
Self {
210+
instance_value: value,
211+
count: 0,
212+
internal_state: String::new(),
213+
}
214+
}
215+
216+
/// Increment the static counter from Rust
217+
pub fn increment() {
218+
let ce = Self::get_metadata().ce();
219+
let current: i64 = ce.get_static_property("count").unwrap_or(0);
220+
ce.set_static_property("count", current + 1).unwrap();
221+
}
222+
223+
/// Get the current count
224+
pub fn get_count() -> i64 {
225+
let ce = Self::get_metadata().ce();
226+
ce.get_static_property("count").unwrap_or(0)
227+
}
228+
}
229+
230+
#[php_module]
231+
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
232+
module.class::<Counter>()
233+
}
234+
# fn main() {}
235+
```
236+
237+
From PHP, you can access static properties directly on the class:
238+
239+
```php
240+
// No need to initialize - count already has default value of 0
241+
Counter::increment();
242+
Counter::increment();
243+
echo Counter::$count; // 2
244+
echo Counter::getCount(); // 2
245+
```

0 commit comments

Comments
 (0)