Skip to content

Commit b8c720d

Browse files
committed
feat(class): Static properties and methods #252
1 parent 8d9072e commit b8c720d

File tree

12 files changed

+298
-45
lines changed

12 files changed

+298
-45
lines changed

crates/macros/src/class.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ struct PropAttributes {
8181
#[darling(flatten)]
8282
rename: PhpRename,
8383
flags: Option<Expr>,
84+
default: Option<Expr>,
8485
attrs: Vec<Attribute>,
8586
}
8687

@@ -123,7 +124,7 @@ impl Property<'_> {
123124
}
124125

125126
/// Generates an implementation of `RegisteredClass` for struct `ident`.
126-
#[allow(clippy::too_many_arguments)]
127+
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
127128
fn generate_registered_class_impl(
128129
ident: &syn::Ident,
129130
class_name: &str,
@@ -162,7 +163,8 @@ fn generate_registered_class_impl(
162163
});
163164

164165
// Generate static properties (PHP-managed, no Rust handlers)
165-
// We combine the base flags with Static flag using from_bits_retain which is const
166+
// We combine the base flags with Static flag using from_bits_retain which is
167+
// const
166168
let static_fields = static_props.iter().map(|prop| {
167169
let name = prop.name();
168170
let base_flags = prop
@@ -173,11 +175,18 @@ fn generate_registered_class_impl(
173175
.unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public });
174176
let docs = &prop.docs;
175177

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+
176185
// Use from_bits_retain to combine flags in a const context
177186
quote! {
178187
(#name, ::ext_php_rs::flags::PropertyFlags::from_bits_retain(
179188
(#base_flags).bits() | ::ext_php_rs::flags::PropertyFlags::Static.bits()
180-
), &[#(#docs,)*] as &[&str])
189+
), #default_value, &[#(#docs,)*] as &[&str])
181190
}
182191
});
183192

@@ -240,8 +249,8 @@ fn generate_registered_class_impl(
240249
}
241250

242251
#[must_use]
243-
fn static_properties() -> &'static [(&'static str, ::ext_php_rs::flags::PropertyFlags, &'static [&'static str])] {
244-
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, &[&str])] = &[#(#static_fields,)*];
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,)*];
245254
STATIC_PROPS
246255
}
247256

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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,17 @@ impl ::ext_php_rs::class::RegisteredClass for MyClass {
3131
fn static_properties() -> &'static [(
3232
&'static str,
3333
::ext_php_rs::flags::PropertyFlags,
34+
::std::option::Option<&'static (dyn ::ext_php_rs::convert::IntoZvalDyn + Sync)>,
3435
&'static [&'static str],
3536
)] {
36-
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, &[&str])] = &[];
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+
)] = &[];
3745
STATIC_PROPS
3846
}
3947
#[inline]

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+
```

guide/src/macros/impl.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,34 @@ The rest of the options are passed as separate attributes:
5050
The `#[php(defaults)]` and `#[php(optional)]` attributes operate the same as the
5151
equivalent function attribute parameters.
5252

53+
### Static Methods
54+
55+
Methods that do not take a `&self` or `&mut self` parameter are automatically
56+
exported as static methods. These can be called on the class itself without
57+
creating an instance.
58+
59+
```rust,ignore
60+
#[php_impl]
61+
impl MyClass {
62+
// Static method - no self parameter
63+
pub fn create_default() -> Self {
64+
Self { /* ... */ }
65+
}
66+
67+
// Instance method - takes &self
68+
pub fn get_value(&self) -> i32 {
69+
self.value
70+
}
71+
}
72+
```
73+
74+
From PHP:
75+
76+
```php
77+
$obj = MyClass::createDefault(); // Static call
78+
$val = $obj->getValue(); // Instance call
79+
```
80+
5381
### Constructors
5482

5583
By default, if a class does not have a constructor, it is not constructable from

0 commit comments

Comments
 (0)