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));