Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Address>()
/// .class::<Person>()
/// }
/// ```
///
/// From PHP:
///
/// ```php
/// <?php
///
/// $address = new Address("123 Main St", "Springfield");
/// $person = new Person("John Doe", $address);
///
/// echo $person->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
Expand Down
91 changes: 91 additions & 0 deletions guide/src/macros/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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::<Address>()
.class::<Person>()
}
```

From PHP:

```php
<?php

$address = new Address("123 Main St", "Springfield");
$person = new Person("John Doe", $address);

echo $person->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
Expand Down
61 changes: 61 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: <https://github.com/extphprs/ext-php-rs/issues/182>
///
/// [`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<Self> {
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<Self> {
let obj = zval.object()?;
<Self as $crate::convert::FromZendObject>::from_zend_object(obj).ok()
}
}
};
}

/// Derives `From<T> for Zval` and `IntoZval` for a given type.
macro_rules! into_zval {
($type: ty, $fn: ident, $dt: ident) => {
Expand Down
25 changes: 25 additions & 0 deletions tests/src/integration/class/class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
43 changes: 43 additions & 0 deletions tests/src/integration/class/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<TestClass>()
Expand All @@ -621,6 +662,8 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
.class::<TestChildClass>()
.class::<TestCloneableClass>()
.class::<TestUncloneableClass>()
.class::<InnerClass>()
.class::<OuterClass>()
.function(wrap_function!(test_class))
.function(wrap_function!(throw_exception));

Expand Down
Loading