Skip to content

Commit 8d9072e

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

File tree

8 files changed

+228
-5
lines changed

8 files changed

+228
-5
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: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ 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>,
@@ -114,6 +116,10 @@ impl Property<'_> {
114116
.rename
115117
.rename(self.ident.to_string(), RenameRule::Camel)
116118
}
119+
120+
pub fn is_static(&self) -> bool {
121+
self.attr.static_.is_present()
122+
}
117123
}
118124

119125
/// Generates an implementation of `RegisteredClass` for struct `ident`.
@@ -130,9 +136,14 @@ fn generate_registered_class_impl(
130136
) -> TokenStream {
131137
let modifier = modifier.option_tokens();
132138

133-
let fields = fields.iter().map(|prop| {
139+
// Separate instance properties from static properties
140+
let (instance_props, static_props): (Vec<_>, Vec<_>) =
141+
fields.iter().partition(|prop| !prop.is_static());
142+
143+
// Generate instance properties (with Rust handlers)
144+
let instance_fields = instance_props.iter().map(|prop| {
134145
let name = prop.name();
135-
let ident = prop.ident;
146+
let field_ident = prop.ident;
136147
let flags = prop
137148
.attr
138149
.flags
@@ -143,13 +154,33 @@ fn generate_registered_class_impl(
143154

144155
quote! {
145156
(#name, ::ext_php_rs::internal::property::PropertyInfo {
146-
prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#ident),
157+
prop: ::ext_php_rs::props::Property::field(|this: &mut Self| &mut this.#field_ident),
147158
flags: #flags,
148159
docs: &[#(#docs,)*]
149160
})
150161
}
151162
});
152163

164+
// 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+
let static_fields = static_props.iter().map(|prop| {
167+
let name = prop.name();
168+
let base_flags = prop
169+
.attr
170+
.flags
171+
.as_ref()
172+
.map(ToTokens::to_token_stream)
173+
.unwrap_or(quote! { ::ext_php_rs::flags::PropertyFlags::Public });
174+
let docs = &prop.docs;
175+
176+
// Use from_bits_retain to combine flags in a const context
177+
quote! {
178+
(#name, ::ext_php_rs::flags::PropertyFlags::from_bits_retain(
179+
(#base_flags).bits() | ::ext_php_rs::flags::PropertyFlags::Static.bits()
180+
), &[#(#docs,)*] as &[&str])
181+
}
182+
});
183+
153184
let flags = match flags {
154185
Some(flags) => flags.to_token_stream(),
155186
None => quote! { ::ext_php_rs::flags::ClassFlags::empty() }.to_token_stream(),
@@ -204,10 +235,16 @@ fn generate_registered_class_impl(
204235
> {
205236
use ::std::iter::FromIterator;
206237
::std::collections::HashMap::from_iter([
207-
#(#fields,)*
238+
#(#instance_fields,)*
208239
])
209240
}
210241

242+
#[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,)*];
245+
STATIC_PROPS
246+
}
247+
211248
#[inline]
212249
fn method_builders() -> ::std::vec::Vec<
213250
(::ext_php_rs::builders::FunctionBuilder<'static>, ::ext_php_rs::flags::MethodFlags)

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ 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+
&'static [&'static str],
35+
)] {
36+
static STATIC_PROPS: &[(&str, ::ext_php_rs::flags::PropertyFlags, &[&str])] = &[];
37+
STATIC_PROPS
38+
}
3039
#[inline]
3140
fn method_builders() -> ::std::vec::Vec<
3241
(

src/builders/module.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ impl ModuleBuilder<'_> {
254254
for (name, prop_info) in T::get_properties() {
255255
builder = builder.property(name, prop_info.flags, prop_info.docs);
256256
}
257+
for (name, flags, docs) in T::static_properties() {
258+
builder = builder.property(*name, *flags, docs);
259+
}
257260
if let Some(modifier) = T::BUILDER_MODIFIER {
258261
builder = modifier(builder);
259262
}

src/class.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::{
1313
convert::IntoZvalDyn,
1414
describe::DocComments,
1515
exception::PhpException,
16-
flags::{ClassFlags, MethodFlags},
16+
flags::{ClassFlags, MethodFlags, PropertyFlags},
1717
internal::property::PropertyInfo,
1818
zend::{ClassEntry, ExecuteData, ZendObjectHandlers},
1919
};
@@ -72,6 +72,15 @@ pub trait RegisteredClass: Sized + 'static {
7272

7373
/// Returns the constants provided by the class.
7474
fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)];
75+
76+
/// Returns the static properties provided by the class.
77+
///
78+
/// Static properties are declared at the class level and managed by PHP,
79+
/// not by Rust handlers. Each tuple contains (name, flags, docs).
80+
#[must_use]
81+
fn static_properties() -> &'static [(&'static str, PropertyFlags, DocComments)] {
82+
&[]
83+
}
7584
}
7685

7786
/// Stores metadata about a classes Rust constructor, including the function

src/zend/class.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ use crate::ffi::instanceof_function_slow;
44
use crate::types::{ZendIterator, Zval};
55
use crate::{
66
boxed::ZBox,
7+
convert::{FromZval, IntoZval},
8+
error::Result,
79
ffi::zend_class_entry,
810
flags::ClassFlags,
911
types::{ZendObject, ZendStr},
1012
zend::ExecutorGlobals,
1113
};
14+
use std::ffi::CString;
1215
use std::ptr;
1316
use std::{convert::TryInto, fmt::Debug};
1417

@@ -132,6 +135,74 @@ impl ClassEntry {
132135
pub fn name(&self) -> Option<&str> {
133136
unsafe { self.name.as_ref().and_then(|s| s.as_str().ok()) }
134137
}
138+
139+
/// Reads a static property from the class.
140+
///
141+
/// # Parameters
142+
///
143+
/// * `name` - The name of the static property to read.
144+
///
145+
/// # Returns
146+
///
147+
/// Returns the value of the static property if it exists and can be
148+
/// converted to type `T`, or `None` otherwise.
149+
///
150+
/// # Example
151+
///
152+
/// ```no_run
153+
/// use ext_php_rs::zend::ClassEntry;
154+
///
155+
/// let ce = ClassEntry::try_find("MyClass").unwrap();
156+
/// let value: Option<i64> = ce.get_static_property("counter");
157+
/// ```
158+
#[must_use]
159+
pub fn get_static_property<'a, T: FromZval<'a>>(&'a self, name: &str) -> Option<T> {
160+
let name = CString::new(name).ok()?;
161+
let zval = unsafe {
162+
crate::ffi::zend_read_static_property(
163+
ptr::from_ref(self).cast_mut(),
164+
name.as_ptr(),
165+
name.as_bytes().len(),
166+
true, // silent - don't throw if property doesn't exist
167+
)
168+
.as_ref()?
169+
};
170+
T::from_zval(zval)
171+
}
172+
173+
/// Sets a static property on the class.
174+
///
175+
/// # Parameters
176+
///
177+
/// * `name` - The name of the static property to set.
178+
/// * `value` - The value to set the property to.
179+
///
180+
/// # Errors
181+
///
182+
/// Returns an error if the property name contains a null byte, or if the
183+
/// value could not be converted to a Zval.
184+
///
185+
/// # Example
186+
///
187+
/// ```no_run
188+
/// use ext_php_rs::zend::ClassEntry;
189+
///
190+
/// let ce = ClassEntry::try_find("MyClass").unwrap();
191+
/// ce.set_static_property("counter", 42).unwrap();
192+
/// ```
193+
pub fn set_static_property<T: IntoZval>(&self, name: &str, value: T) -> Result<()> {
194+
let name = CString::new(name)?;
195+
let mut zval = value.into_zval(false)?;
196+
unsafe {
197+
crate::ffi::zend_update_static_property(
198+
ptr::from_ref(self).cast_mut(),
199+
name.as_ptr(),
200+
name.as_bytes().len(),
201+
&raw mut zval,
202+
);
203+
}
204+
Ok(())
205+
}
135206
}
136207

137208
impl PartialEq for ClassEntry {

tests/src/integration/class/class.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,46 @@
5151

5252
$classReflection = new ReflectionClass(TestClassProtectedConstruct::class);
5353
assert($classReflection->getMethod('__construct')->isProtected());
54+
55+
// Test static properties (Issue #252)
56+
$staticObj = new TestStaticProps(42);
57+
assert($staticObj->instanceValue === 42, 'Instance property should work');
58+
59+
// Verify static property exists and is accessible
60+
$reflection = new ReflectionClass(TestStaticProps::class);
61+
$staticCounterProp = $reflection->getProperty('staticCounter');
62+
assert($staticCounterProp->isStatic(), 'staticCounter should be a static property');
63+
assert($staticCounterProp->isPublic(), 'staticCounter should be public');
64+
65+
// Verify private static property
66+
$privateStaticProp = $reflection->getProperty('privateStatic');
67+
assert($privateStaticProp->isStatic(), 'privateStatic should be a static property');
68+
assert($privateStaticProp->isPrivate(), 'privateStatic should be private');
69+
70+
// Test accessing static property via class
71+
TestStaticProps::$staticCounter = 100;
72+
assert(TestStaticProps::$staticCounter === 100, 'Should be able to set and get static property');
73+
74+
// Test that static property is shared across instances
75+
$obj1 = new TestStaticProps(1);
76+
$obj2 = new TestStaticProps(2);
77+
TestStaticProps::$staticCounter = 200;
78+
assert(TestStaticProps::$staticCounter === 200, 'Static property value should be shared');
79+
80+
// Test static methods that interact with static properties
81+
TestStaticProps::setCounter(0);
82+
assert(TestStaticProps::getCounter() === 0, 'Counter should be 0 after reset');
83+
84+
TestStaticProps::incrementCounter();
85+
assert(TestStaticProps::getCounter() === 1, 'Counter should be 1 after increment');
86+
87+
TestStaticProps::incrementCounter();
88+
TestStaticProps::incrementCounter();
89+
assert(TestStaticProps::getCounter() === 3, 'Counter should be 3 after 3 increments');
90+
91+
// Test that PHP access and Rust access see the same value
92+
TestStaticProps::$staticCounter = 50;
93+
assert(TestStaticProps::getCounter() === 50, 'Rust should see PHP-set value');
94+
95+
TestStaticProps::setCounter(100);
96+
assert(TestStaticProps::$staticCounter === 100, 'PHP should see Rust-set value');

tests/src/integration/class/mod.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![allow(clippy::unused_self)]
22
use ext_php_rs::{
3+
class::RegisteredClass,
34
convert::IntoZval,
45
prelude::*,
56
types::{ZendClassObject, Zval},
@@ -175,6 +176,53 @@ impl TestClassProtectedConstruct {
175176
}
176177
}
177178

179+
/// Test class with static properties (Issue #252)
180+
#[php_class]
181+
pub struct TestStaticProps {
182+
/// Instance property for comparison
183+
#[php(prop)]
184+
pub instance_value: i32,
185+
/// Static property - managed by PHP, not Rust handlers
186+
#[php(prop, static)]
187+
pub static_counter: i32,
188+
/// Private static property
189+
#[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)]
190+
pub private_static: String,
191+
}
192+
193+
#[php_impl]
194+
impl TestStaticProps {
195+
pub fn __construct(value: i32) -> Self {
196+
Self {
197+
instance_value: value,
198+
// Note: static fields have default values in PHP, not from Rust constructor
199+
static_counter: 0,
200+
private_static: String::new(),
201+
}
202+
}
203+
204+
/// Static method to increment the static counter
205+
pub fn increment_counter() {
206+
let ce = Self::get_metadata().ce();
207+
let current: i64 = ce.get_static_property("staticCounter").unwrap_or(0);
208+
ce.set_static_property("staticCounter", current + 1)
209+
.expect("Failed to set static property");
210+
}
211+
212+
/// Static method to get the current counter value
213+
pub fn get_counter() -> i64 {
214+
let ce = Self::get_metadata().ce();
215+
ce.get_static_property("staticCounter").unwrap_or(0)
216+
}
217+
218+
/// Static method to set the counter to a specific value
219+
pub fn set_counter(value: i64) {
220+
let ce = Self::get_metadata().ce();
221+
ce.set_static_property("staticCounter", value)
222+
.expect("Failed to set static property");
223+
}
224+
}
225+
178226
pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
179227
builder
180228
.class::<TestClass>()
@@ -183,6 +231,7 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
183231
.class::<TestClassExtendsImpl>()
184232
.class::<TestClassMethodVisibility>()
185233
.class::<TestClassProtectedConstruct>()
234+
.class::<TestStaticProps>()
186235
.function(wrap_function!(test_class))
187236
.function(wrap_function!(throw_exception))
188237
}

0 commit comments

Comments
 (0)