diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 96949bad7..0b93d1e1b 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -398,6 +398,98 @@ extern crate proc_macro; /// echo Counter::getCount(); // 2 /// ``` /// +/// ## Using Classes as Properties +/// +/// By default, `#[php_class]` types cannot be used directly as properties of +/// other `#[php_class]` types because they don't implement `FromZval`. To +/// enable this, use the `class_derives_clone!` macro on any class that needs to +/// be used as a property. +/// +/// The class must implement `Clone`, and calling `class_derives_clone!` will +/// implement `FromZval` and `FromZendObject` for the type, allowing PHP objects +/// to be cloned into Rust values. +/// +/// ```rust,ignore +/// use ext_php_rs::prelude::*; +/// use ext_php_rs::class_derives_clone; +/// +/// // Inner class that will be used as a property +/// #[php_class] +/// #[derive(Clone)] +/// pub struct Address { +/// #[php(prop)] +/// pub street: String, +/// #[php(prop)] +/// pub city: String, +/// } +/// +/// // Enable this class to be used as a property +/// class_derives_clone!(Address); +/// +/// #[php_impl] +/// impl Address { +/// pub fn __construct(street: String, city: String) -> Self { +/// Self { street, city } +/// } +/// } +/// +/// // Outer class containing the inner class as a property +/// #[php_class] +/// pub struct Person { +/// #[php(prop)] +/// pub name: String, +/// #[php(prop)] +/// pub address: Address, // Works because we called class_derives_clone! +/// } +/// +/// #[php_impl] +/// impl Person { +/// pub fn __construct(name: String, address: Address) -> Self { +/// Self { name, address } +/// } +/// +/// pub fn get_city(&self) -> String { +/// self.address.city.clone() +/// } +/// } +/// +/// #[php_module] +/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { +/// module +/// .class::
() +/// .class::() +/// } +/// ``` +/// +/// From PHP: +/// +/// ```php +/// name; // "John Doe" +/// echo $person->address->city; // "Springfield" +/// echo $person->getCity(); // "Springfield" +/// +/// // You can also set the nested property +/// $newAddress = new Address("456 Oak Ave", "Shelbyville"); +/// $person->address = $newAddress; +/// echo $person->address->city; // "Shelbyville" +/// ``` +/// +/// **Important notes:** +/// +/// - The inner class must implement `Clone` +/// - Call `class_derives_clone!` after the `#[php_class]` definition +/// - When accessed from PHP, the property returns a clone of the Rust value +/// - Modifications to the returned object don't affect the original unless +/// reassigned +/// +/// See [GitHub issue #182](https://github.com/extphprs/ext-php-rs/issues/182) +/// for more context. +/// /// ## Abstract Classes /// /// Abstract classes cannot be instantiated directly and may contain abstract diff --git a/guide/src/macros/classes.md b/guide/src/macros/classes.md index 219d58e3b..b64959cd8 100644 --- a/guide/src/macros/classes.md +++ b/guide/src/macros/classes.md @@ -360,6 +360,97 @@ echo Counter::$count; // 2 echo Counter::getCount(); // 2 ``` +## Using Classes as Properties + +By default, `#[php_class]` types cannot be used directly as properties of other +`#[php_class]` types because they don't implement `FromZval`. To enable this, +use the `class_derives_clone!` macro on any class that needs to be used as a +property. + +The class must implement `Clone`, and calling `class_derives_clone!` will +implement `FromZval` and `FromZendObject` for the type, allowing PHP objects +to be cloned into Rust values. + +```rust,ignore +use ext_php_rs::prelude::*; +use ext_php_rs::class_derives_clone; + +// Inner class that will be used as a property +#[php_class] +#[derive(Clone)] +pub struct Address { + #[php(prop)] + pub street: String, + #[php(prop)] + pub city: String, +} + +// Enable this class to be used as a property +class_derives_clone!(Address); + +#[php_impl] +impl Address { + pub fn __construct(street: String, city: String) -> Self { + Self { street, city } + } +} + +// Outer class containing the inner class as a property +#[php_class] +pub struct Person { + #[php(prop)] + pub name: String, + #[php(prop)] + pub address: Address, // Works because we called class_derives_clone! +} + +#[php_impl] +impl Person { + pub fn __construct(name: String, address: Address) -> Self { + Self { name, address } + } + + pub fn get_city(&self) -> String { + self.address.city.clone() + } +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module + .class::
() + .class::() +} +``` + +From PHP: + +```php +name; // "John Doe" +echo $person->address->city; // "Springfield" +echo $person->getCity(); // "Springfield" + +// You can also set the nested property +$newAddress = new Address("456 Oak Ave", "Shelbyville"); +$person->address = $newAddress; +echo $person->address->city; // "Shelbyville" +``` + +**Important notes:** + +- The inner class must implement `Clone` +- Call `class_derives_clone!` after the `#[php_class]` definition +- When accessed from PHP, the property returns a clone of the Rust value +- Modifications to the returned object don't affect the original unless reassigned + +See [GitHub issue #182](https://github.com/extphprs/ext-php-rs/issues/182) +for more context. + ## Abstract Classes Abstract classes cannot be instantiated directly and may contain abstract methods diff --git a/src/macros.rs b/src/macros.rs index eb3abf731..a57757dd1 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -395,6 +395,67 @@ macro_rules! class_derives { }; } +/// Derives additional traits for cloneable [`RegisteredClass`] types to enable +/// using them as properties of other `#[php_class]` structs. +/// +/// This macro should be called for any `#[php_class]` struct that: +/// 1. Implements [`Clone`] +/// 2. Needs to be used as a property in another `#[php_class]` struct +/// +/// The macro implements [`FromZendObject`] and [`FromZval`] for the owned type, +/// allowing PHP objects to be cloned into Rust values. +/// +/// # Example +/// +/// ```ignore +/// use ext_php_rs::prelude::*; +/// use ext_php_rs::class_derives_clone; +/// +/// #[php_class] +/// #[derive(Clone)] +/// struct Bar { +/// #[php(prop)] +/// value: String, +/// } +/// +/// class_derives_clone!(Bar); +/// +/// #[php_class] +/// struct Foo { +/// #[php(prop)] +/// bar: Bar, // Now works because Bar implements FromZval +/// } +/// ``` +/// +/// See: +/// +/// [`RegisteredClass`]: crate::class::RegisteredClass +/// [`FromZendObject`]: crate::convert::FromZendObject +/// [`FromZval`]: crate::convert::FromZval +#[macro_export] +macro_rules! class_derives_clone { + ($type: ty) => { + impl $crate::convert::FromZendObject<'_> for $type { + fn from_zend_object(obj: &$crate::types::ZendObject) -> $crate::error::Result { + let class_obj = $crate::types::ZendClassObject::<$type>::from_zend_obj(obj) + .ok_or($crate::error::Error::InvalidScope)?; + Ok((**class_obj).clone()) + } + } + + impl $crate::convert::FromZval<'_> for $type { + const TYPE: $crate::flags::DataType = $crate::flags::DataType::Object(Some( + <$type as $crate::class::RegisteredClass>::CLASS_NAME, + )); + + fn from_zval(zval: &$crate::types::Zval) -> ::std::option::Option { + let obj = zval.object()?; + ::from_zend_object(obj).ok() + } + } + }; +} + /// Derives `From for Zval` and `IntoZval` for a given type. macro_rules! into_zval { ($type: ty, $fn: ident, $dt: ident) => { diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index d25d0080d..afdeac61c 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -348,3 +348,28 @@ public function __construct(string $data) { $uncloneable = new TestUncloneableClass('test'); assert_exception_thrown(fn() => clone $uncloneable, 'Cloning uncloneable class should throw'); + +// Test issue #182 - class structs containing class struct properties +$inner = new InnerClass('hello world'); +assert($inner->getValue() === 'hello world', 'InnerClass getValue should work'); +assert($inner->value === 'hello world', 'InnerClass property should be accessible'); + +$outer = new OuterClass($inner); +assert($outer->getInnerValue() === 'hello world', 'OuterClass should be able to access inner value'); + +// Test that the inner property is properly accessible +assert($outer->inner instanceof InnerClass, 'outer->inner should be InnerClass instance'); +assert($outer->inner->value === 'hello world', 'outer->inner->value should be accessible'); + +// Test setting inner property +$newInner = new InnerClass('new value'); +$outer->inner = $newInner; +assert($outer->getInnerValue() === 'new value', 'After setting inner, value should be updated'); + +// Test clone-on-read behavior +$outer->inner = new InnerClass('original'); +$a = $outer->inner; +$b = $outer->inner; +assert($a !== $b, 'Each read should return a clone'); +$a->value = 'changed'; +assert($outer->inner->value === 'original', 'Original should be unchanged'); diff --git a/tests/src/integration/class/mod.rs b/tests/src/integration/class/mod.rs index 35abb9fc6..f74bc90e8 100644 --- a/tests/src/integration/class/mod.rs +++ b/tests/src/integration/class/mod.rs @@ -601,6 +601,47 @@ impl TestUncloneableClass { } } +// Test for issue #182 - class structs containing class struct properties +// The inner class must derive Clone and call class_derives_clone! + +#[php_class] +#[derive(Clone, Default)] +pub struct InnerClass { + #[php(prop)] + pub value: String, +} + +ext_php_rs::class_derives_clone!(InnerClass); + +#[php_impl] +impl InnerClass { + pub fn __construct(value: String) -> Self { + Self { value } + } + + pub fn get_value(&self) -> String { + self.value.clone() + } +} + +#[php_class] +#[derive(Default)] +pub struct OuterClass { + #[php(prop)] + pub inner: InnerClass, +} + +#[php_impl] +impl OuterClass { + pub fn __construct(inner: InnerClass) -> Self { + Self { inner } + } + + pub fn get_inner_value(&self) -> String { + self.inner.value.clone() + } +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { let builder = builder .class::() @@ -621,6 +662,8 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { .class::() .class::() .class::() + .class::() + .class::() .function(wrap_function!(test_class)) .function(wrap_function!(throw_exception));