Skip to content

Commit 2a8fa61

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

File tree

11 files changed

+287
-44
lines changed

11 files changed

+287
-44
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: 99 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,70 @@ 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+
/// ```rust,no_run,ignore
220+
/// # #![cfg_attr(windows, feature(abi_vectorcall))]
221+
/// # extern crate ext_php_rs;
222+
/// use ext_php_rs::prelude::*;
223+
/// use ext_php_rs::class::RegisteredClass;
224+
///
225+
/// #[php_class]
226+
/// pub struct Counter {
227+
/// #[php(prop)]
228+
/// pub instance_value: i32,
229+
/// #[php(prop, static)]
230+
/// pub count: i32,
231+
/// #[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)]
232+
/// pub internal_state: String,
233+
/// }
234+
///
235+
/// #[php_impl]
236+
/// impl Counter {
237+
/// pub fn __construct(value: i32) -> Self {
238+
/// Self {
239+
/// instance_value: value,
240+
/// count: 0,
241+
/// internal_state: String::new(),
242+
/// }
243+
/// }
244+
///
245+
/// /// Increment the static counter from Rust
246+
/// pub fn increment() {
247+
/// let ce = Self::get_metadata().ce();
248+
/// let current: i64 = ce.get_static_property("count").unwrap_or(0);
249+
/// ce.set_static_property("count", current + 1).unwrap();
250+
/// }
251+
///
252+
/// /// Get the current count
253+
/// pub fn get_count() -> i64 {
254+
/// let ce = Self::get_metadata().ce();
255+
/// ce.get_static_property("count").unwrap_or(0)
256+
/// }
257+
/// }
258+
///
259+
/// #[php_module]
260+
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
261+
/// module.class::<Counter>()
262+
/// }
263+
/// # fn main() {}
264+
/// ```
265+
///
266+
/// From PHP, you can access static properties directly on the class:
267+
///
268+
/// ```php
269+
/// Counter::$count = 0;
270+
/// Counter::increment();
271+
/// Counter::increment();
272+
/// echo Counter::$count; // 2
273+
/// echo Counter::getCount(); // 2
274+
/// ```
207275
// END DOCS FROM classes.md
208276
#[proc_macro_attribute]
209277
pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream {
@@ -812,6 +880,34 @@ fn php_module_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2
812880
/// The `#[php(defaults)]` and `#[php(optional)]` attributes operate the same as
813881
/// the equivalent function attribute parameters.
814882
///
883+
/// ### Static Methods
884+
///
885+
/// Methods that do not take a `&self` or `&mut self` parameter are
886+
/// automatically exported as static methods. These can be called on the class
887+
/// itself without creating an instance.
888+
///
889+
/// ```rust,ignore
890+
/// #[php_impl]
891+
/// impl MyClass {
892+
/// // Static method - no self parameter
893+
/// pub fn create_default() -> Self {
894+
/// Self { /* ... */ }
895+
/// }
896+
///
897+
/// // Instance method - takes &self
898+
/// pub fn get_value(&self) -> i32 {
899+
/// self.value
900+
/// }
901+
/// }
902+
/// ```
903+
///
904+
/// From PHP:
905+
///
906+
/// ```php
907+
/// $obj = MyClass::createDefault(); // Static call
908+
/// $val = $obj->getValue(); // Instance call
909+
/// ```
910+
///
815911
/// ### Constructors
816912
///
817913
/// By default, if a class does not have a constructor, it is not constructable

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, default = "initial", 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)