diff --git a/allowed_bindings.rs b/allowed_bindings.rs index 5f73f60d9..42dbb8750 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -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, diff --git a/crates/macros/src/class.rs b/crates/macros/src/class.rs index 4264d22c5..1e4da0c32 100644 --- a/crates/macros/src/class.rs +++ b/crates/macros/src/class.rs @@ -76,9 +76,12 @@ pub fn parser(mut input: ItemStruct) -> Result { #[darling(attributes(php), forward_attrs(doc), default)] struct PropAttributes { prop: Flag, + #[darling(rename = "static")] + static_: Flag, #[darling(flatten)] rename: PhpRename, flags: Option, + default: Option, attrs: Vec, } @@ -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, @@ -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 @@ -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(), @@ -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) diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index fa6f8c7fe..87cde609f 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -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 /// @@ -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::() +/// } +/// # 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 { @@ -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 diff --git a/crates/macros/tests/expand/class.expanded.rs b/crates/macros/tests/expand/class.expanded.rs index f08ca30a6..88ac1df6c 100644 --- a/crates/macros/tests/expand/class.expanded.rs +++ b/crates/macros/tests/expand/class.expanded.rs @@ -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< ( diff --git a/docsrs_bindings.rs b/docsrs_bindings.rs index 450211170..29a2ee8c6 100644 --- a/docsrs_bindings.rs +++ b/docsrs_bindings.rs @@ -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); } diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 4ea14c8f3..22b306496 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -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 @@ -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::() +} +# 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 +``` diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index 878e9c484..0e3ea1697 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -50,6 +50,34 @@ The rest of the options are passed as separate attributes: 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 from diff --git a/src/builders/class.rs b/src/builders/class.rs index e14a82c6b..665650641 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -18,6 +18,7 @@ use crate::{ }; type ConstantEntry = (String, Box Result>, DocComments); +type PropertyDefault = Option Result>>; /// Builder for registering a class in PHP. #[must_use] @@ -28,7 +29,7 @@ pub struct ClassBuilder { pub(crate) interfaces: Vec, pub(crate) methods: Vec<(FunctionBuilder<'static>, MethodFlags)>, object_override: Option *mut ZendObject>, - pub(crate) properties: Vec<(String, PropertyFlags, DocComments)>, + pub(crate) properties: Vec<(String, PropertyFlags, PropertyDefault, DocComments)>, pub(crate) constants: Vec, register: Option, pub(crate) docs: DocComments, @@ -105,14 +106,16 @@ impl ClassBuilder { /// /// * `name` - The name of the property to add to the class. /// * `flags` - Flags relating to the property. See [`PropertyFlags`]. + /// * `default` - Optional default value for the property. /// * `docs` - Documentation comments for the property. pub fn property>( mut self, name: T, flags: PropertyFlags, + default: PropertyDefault, docs: DocComments, ) -> Self { - self.properties.push((name.into(), flags, docs)); + self.properties.push((name.into(), flags, default, docs)); self } @@ -356,13 +359,17 @@ impl ClassBuilder { unsafe { zend_do_implement_interface(class, ptr::from_ref(interface).cast_mut()) }; } - for (name, flags, _) in self.properties { + for (name, flags, default, _) in self.properties { + let mut default_zval = match default { + Some(f) => f()?, + None => Zval::new(), + }; unsafe { zend_declare_property( class, CString::new(name.as_str())?.as_ptr(), name.len() as _, - &mut Zval::new(), + &raw mut default_zval, flags.bits().try_into()?, ); } @@ -409,7 +416,7 @@ mod tests { assert_eq!(class.interfaces, vec![]); assert_eq!(class.methods.len(), 0); assert_eq!(class.object_override, None); - assert_eq!(class.properties, vec![]); + assert!(class.properties.is_empty()); assert_eq!(class.constants.len(), 0); assert_eq!(class.register, None); assert_eq!(class.docs, &[] as DocComments); @@ -438,15 +445,13 @@ mod tests { #[test] fn test_property() { - let class = ClassBuilder::new("Foo").property("bar", PropertyFlags::Public, &["Doc 1"]); - assert_eq!( - class.properties, - vec![( - "bar".to_string(), - PropertyFlags::Public, - &["Doc 1"] as DocComments - )] - ); + let class = + ClassBuilder::new("Foo").property("bar", PropertyFlags::Public, None, &["Doc 1"]); + assert_eq!(class.properties.len(), 1); + assert_eq!(class.properties[0].0, "bar"); + assert_eq!(class.properties[0].1, PropertyFlags::Public); + assert!(class.properties[0].2.is_none()); + assert_eq!(class.properties[0].3, &["Doc 1"] as DocComments); } #[test] diff --git a/src/builders/module.rs b/src/builders/module.rs index 0d334d0d6..88b69b796 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -252,7 +252,14 @@ impl ModuleBuilder<'_> { .expect("Failed to register constant"); } for (name, prop_info) in T::get_properties() { - builder = builder.property(name, prop_info.flags, prop_info.docs); + builder = builder.property(name, prop_info.flags, None, prop_info.docs); + } + for (name, flags, default, docs) in T::static_properties() { + let default_fn = default.map(|v| { + Box::new(move || v.as_zval(true)) + as Box crate::error::Result> + }); + builder = builder.property(*name, *flags, default_fn, docs); } if let Some(modifier) = T::BUILDER_MODIFIER { builder = modifier(builder); diff --git a/src/class.rs b/src/class.rs index ad6846c9f..3301a821e 100644 --- a/src/class.rs +++ b/src/class.rs @@ -13,7 +13,7 @@ use crate::{ convert::IntoZvalDyn, describe::DocComments, exception::PhpException, - flags::{ClassFlags, MethodFlags}, + flags::{ClassFlags, MethodFlags, PropertyFlags}, internal::property::PropertyInfo, zend::{ClassEntry, ExecuteData, ZendObjectHandlers}, }; @@ -72,6 +72,21 @@ pub trait RegisteredClass: Sized + 'static { /// Returns the constants provided by the class. fn constants() -> &'static [(&'static str, &'static dyn IntoZvalDyn, DocComments)]; + + /// Returns the static properties provided by the class. + /// + /// Static properties are declared at the class level and managed by PHP, + /// not by Rust handlers. Each tuple contains (name, flags, default, docs). + /// The default value is optional - `None` means null default. + #[must_use] + fn static_properties() -> &'static [( + &'static str, + PropertyFlags, + Option<&'static (dyn IntoZvalDyn + Sync)>, + DocComments, + )] { + &[] + } } /// Stores metadata about a classes Rust constructor, including the function diff --git a/src/describe/mod.rs b/src/describe/mod.rs index bb361d313..d86cc5f20 100644 --- a/src/describe/mod.rs +++ b/src/describe/mod.rs @@ -379,9 +379,9 @@ pub struct Property { pub default: Option, } -impl From<(String, PropertyFlags, DocComments)> for Property { - fn from(value: (String, PropertyFlags, DocComments)) -> Self { - let (name, flags, docs) = value; +impl From<(String, PropertyFlags, D, DocComments)> for Property { + fn from(value: (String, PropertyFlags, D, DocComments)) -> Self { + let (name, flags, _default, docs) = value; let static_ = flags.contains(PropertyFlags::Static); let vis = Visibility::from(flags); // TODO: Implement ty #376 @@ -656,7 +656,7 @@ mod tests { .extends((|| todo!(), "BaseClass")) .implements((|| todo!(), "Interface1")) .implements((|| todo!(), "Interface2")) - .property("prop1", PropertyFlags::Public, &["doc1"]) + .property("prop1", PropertyFlags::Public, None, &["doc1"]) .method( FunctionBuilder::new("test_function", test_function), MethodFlags::Protected, @@ -702,8 +702,13 @@ mod tests { #[test] fn test_property_from() { let docs: &'static [&'static str] = &["doc1", "doc2"]; - let property: Property = - ("test_property".to_string(), PropertyFlags::Protected, docs).into(); + let property: Property = ( + "test_property".to_string(), + PropertyFlags::Protected, + (), + docs, + ) + .into(); assert_eq!(property.name, "test_property".into()); assert_eq!(property.docs.0.len(), 2); assert_eq!(property.vis, Visibility::Protected); diff --git a/src/zend/class.rs b/src/zend/class.rs index ca6ed6a21..2de9b4ab3 100644 --- a/src/zend/class.rs +++ b/src/zend/class.rs @@ -4,11 +4,14 @@ use crate::ffi::instanceof_function_slow; use crate::types::{ZendIterator, Zval}; use crate::{ boxed::ZBox, - ffi::zend_class_entry, + convert::{FromZval, IntoZval}, + error::{Error, Result}, + ffi::{ZEND_RESULT_CODE_SUCCESS, zend_class_entry}, flags::ClassFlags, types::{ZendObject, ZendStr}, zend::ExecutorGlobals, }; +use std::ffi::CString; use std::ptr; use std::{convert::TryInto, fmt::Debug}; @@ -132,6 +135,79 @@ impl ClassEntry { pub fn name(&self) -> Option<&str> { unsafe { self.name.as_ref().and_then(|s| s.as_str().ok()) } } + + /// Reads a static property from the class. + /// + /// # Parameters + /// + /// * `name` - The name of the static property to read. + /// + /// # Returns + /// + /// Returns the value of the static property if it exists and can be + /// converted to type `T`, or `None` otherwise. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::zend::ClassEntry; + /// + /// let ce = ClassEntry::try_find("MyClass").unwrap(); + /// let value: Option = ce.get_static_property("counter"); + /// ``` + #[must_use] + pub fn get_static_property<'a, T: FromZval<'a>>(&'a self, name: &str) -> Option { + let name = CString::new(name).ok()?; + let zval = unsafe { + crate::ffi::zend_read_static_property( + ptr::from_ref(self).cast_mut(), + name.as_ptr(), + name.as_bytes().len(), + true, // silent - don't throw if property doesn't exist + ) + .as_ref()? + }; + T::from_zval(zval) + } + + /// Sets a static property on the class. + /// + /// # Parameters + /// + /// * `name` - The name of the static property to set. + /// * `value` - The value to set the property to. + /// + /// # Errors + /// + /// Returns an error if the property name contains a null byte, if the + /// value could not be converted to a Zval, or if the property could not + /// be updated (e.g., the property does not exist). + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::zend::ClassEntry; + /// + /// let ce = ClassEntry::try_find("MyClass").unwrap(); + /// ce.set_static_property("counter", 42).unwrap(); + /// ``` + pub fn set_static_property(&self, name: &str, value: T) -> Result<()> { + let name = CString::new(name)?; + let mut zval = value.into_zval(false)?; + let result = unsafe { + crate::ffi::zend_update_static_property( + ptr::from_ref(self).cast_mut(), + name.as_ptr(), + name.as_bytes().len(), + &raw mut zval, + ) + }; + if result == ZEND_RESULT_CODE_SUCCESS { + Ok(()) + } else { + Err(Error::InvalidProperty) + } + } } impl PartialEq for ClassEntry { diff --git a/tests/sapi.rs b/tests/sapi.rs index 73136d4f6..688fd63dc 100644 --- a/tests/sapi.rs +++ b/tests/sapi.rs @@ -166,7 +166,7 @@ fn test_sapi_multithread() { Ok(zval) => { assert!(zval.is_string()); let string = zval.string().unwrap(); - let output = string.to_string(); + let output = string.clone(); assert_eq!(output, format!("Hello, thread-{i}!")); results.lock().unwrap().push((i, output)); diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index d6c6ee6c1..5a0005a1d 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -51,3 +51,46 @@ $classReflection = new ReflectionClass(TestClassProtectedConstruct::class); assert($classReflection->getMethod('__construct')->isProtected()); + +// Test static properties (Issue #252) +$staticObj = new TestStaticProps(42); +assert($staticObj->instanceValue === 42, 'Instance property should work'); + +// Verify static property exists and is accessible +$reflection = new ReflectionClass(TestStaticProps::class); +$staticCounterProp = $reflection->getProperty('staticCounter'); +assert($staticCounterProp->isStatic(), 'staticCounter should be a static property'); +assert($staticCounterProp->isPublic(), 'staticCounter should be public'); + +// Verify private static property +$privateStaticProp = $reflection->getProperty('privateStatic'); +assert($privateStaticProp->isStatic(), 'privateStatic should be a static property'); +assert($privateStaticProp->isPrivate(), 'privateStatic should be private'); + +// Test accessing static property via class +TestStaticProps::$staticCounter = 100; +assert(TestStaticProps::$staticCounter === 100, 'Should be able to set and get static property'); + +// Test that static property is shared across instances +$obj1 = new TestStaticProps(1); +$obj2 = new TestStaticProps(2); +TestStaticProps::$staticCounter = 200; +assert(TestStaticProps::$staticCounter === 200, 'Static property value should be shared'); + +// Test static methods that interact with static properties +TestStaticProps::setCounter(0); +assert(TestStaticProps::getCounter() === 0, 'Counter should be 0 after reset'); + +TestStaticProps::incrementCounter(); +assert(TestStaticProps::getCounter() === 1, 'Counter should be 1 after increment'); + +TestStaticProps::incrementCounter(); +TestStaticProps::incrementCounter(); +assert(TestStaticProps::getCounter() === 3, 'Counter should be 3 after 3 increments'); + +// Test that PHP access and Rust access see the same value +TestStaticProps::$staticCounter = 50; +assert(TestStaticProps::getCounter() === 50, 'Rust should see PHP-set value'); + +TestStaticProps::setCounter(100); +assert(TestStaticProps::$staticCounter === 100, 'PHP should see Rust-set value'); diff --git a/tests/src/integration/class/mod.rs b/tests/src/integration/class/mod.rs index 3c3a2bb0c..293b5f6f1 100644 --- a/tests/src/integration/class/mod.rs +++ b/tests/src/integration/class/mod.rs @@ -1,5 +1,6 @@ #![allow(clippy::unused_self)] use ext_php_rs::{ + class::RegisteredClass, convert::IntoZval, prelude::*, types::{ZendClassObject, Zval}, @@ -175,6 +176,53 @@ impl TestClassProtectedConstruct { } } +/// Test class with static properties (Issue #252) +#[php_class] +pub struct TestStaticProps { + /// Instance property for comparison + #[php(prop)] + pub instance_value: i32, + /// Static property - managed by PHP, not Rust handlers + #[php(prop, static)] + pub static_counter: i32, + /// Private static property + #[php(prop, static, flags = ext_php_rs::flags::PropertyFlags::Private)] + pub private_static: String, +} + +#[php_impl] +impl TestStaticProps { + pub fn __construct(value: i32) -> Self { + Self { + instance_value: value, + // Note: static fields have default values in PHP, not from Rust constructor + static_counter: 0, + private_static: String::new(), + } + } + + /// Static method to increment the static counter + pub fn increment_counter() { + let ce = Self::get_metadata().ce(); + let current: i64 = ce.get_static_property("staticCounter").unwrap_or(0); + ce.set_static_property("staticCounter", current + 1) + .expect("Failed to set static property"); + } + + /// Static method to get the current counter value + pub fn get_counter() -> i64 { + let ce = Self::get_metadata().ce(); + ce.get_static_property("staticCounter").unwrap_or(0) + } + + /// Static method to set the counter to a specific value + pub fn set_counter(value: i64) { + let ce = Self::get_metadata().ce(); + ce.set_static_property("staticCounter", value) + .expect("Failed to set static property"); + } +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { builder .class::() @@ -183,6 +231,7 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { .class::() .class::() .class::() + .class::() .function(wrap_function!(test_class)) .function(wrap_function!(throw_exception)) }