Skip to content

Commit 8345b9a

Browse files
committed
feat(class): Class structs can now contain other class structs #182
1 parent 4628b3e commit 8345b9a

File tree

5 files changed

+312
-0
lines changed

5 files changed

+312
-0
lines changed

crates/macros/src/lib.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,98 @@ extern crate proc_macro;
398398
/// echo Counter::getCount(); // 2
399399
/// ```
400400
///
401+
/// ## Using Classes as Properties
402+
///
403+
/// By default, `#[php_class]` types cannot be used directly as properties of
404+
/// other `#[php_class]` types because they don't implement `FromZval`. To
405+
/// enable this, use the `class_derives_clone!` macro on any class that needs to
406+
/// be used as a property.
407+
///
408+
/// The class must implement `Clone`, and calling `class_derives_clone!` will
409+
/// implement `FromZval` and `FromZendObject` for the type, allowing PHP objects
410+
/// to be cloned into Rust values.
411+
///
412+
/// ```rust,ignore
413+
/// use ext_php_rs::prelude::*;
414+
/// use ext_php_rs::class_derives_clone;
415+
///
416+
/// // Inner class that will be used as a property
417+
/// #[php_class]
418+
/// #[derive(Clone)]
419+
/// pub struct Address {
420+
/// #[php(prop)]
421+
/// pub street: String,
422+
/// #[php(prop)]
423+
/// pub city: String,
424+
/// }
425+
///
426+
/// // Enable this class to be used as a property
427+
/// class_derives_clone!(Address);
428+
///
429+
/// #[php_impl]
430+
/// impl Address {
431+
/// pub fn __construct(street: String, city: String) -> Self {
432+
/// Self { street, city }
433+
/// }
434+
/// }
435+
///
436+
/// // Outer class containing the inner class as a property
437+
/// #[php_class]
438+
/// pub struct Person {
439+
/// #[php(prop)]
440+
/// pub name: String,
441+
/// #[php(prop)]
442+
/// pub address: Address, // Works because we called class_derives_clone!
443+
/// }
444+
///
445+
/// #[php_impl]
446+
/// impl Person {
447+
/// pub fn __construct(name: String, address: Address) -> Self {
448+
/// Self { name, address }
449+
/// }
450+
///
451+
/// pub fn get_city(&self) -> String {
452+
/// self.address.city.clone()
453+
/// }
454+
/// }
455+
///
456+
/// #[php_module]
457+
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
458+
/// module
459+
/// .class::<Address>()
460+
/// .class::<Person>()
461+
/// }
462+
/// ```
463+
///
464+
/// From PHP:
465+
///
466+
/// ```php
467+
/// <?php
468+
///
469+
/// $address = new Address("123 Main St", "Springfield");
470+
/// $person = new Person("John Doe", $address);
471+
///
472+
/// echo $person->name; // "John Doe"
473+
/// echo $person->address->city; // "Springfield"
474+
/// echo $person->getCity(); // "Springfield"
475+
///
476+
/// // You can also set the nested property
477+
/// $newAddress = new Address("456 Oak Ave", "Shelbyville");
478+
/// $person->address = $newAddress;
479+
/// echo $person->address->city; // "Shelbyville"
480+
/// ```
481+
///
482+
/// **Important notes:**
483+
///
484+
/// - The inner class must implement `Clone`
485+
/// - Call `class_derives_clone!` after the `#[php_class]` definition
486+
/// - When accessed from PHP, the property returns a clone of the Rust value
487+
/// - Modifications to the returned object don't affect the original unless
488+
/// reassigned
489+
///
490+
/// See [GitHub issue #182](https://github.com/extphprs/ext-php-rs/issues/182)
491+
/// for more context.
492+
///
401493
/// ## Abstract Classes
402494
///
403495
/// Abstract classes cannot be instantiated directly and may contain abstract

guide/src/macros/classes.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,97 @@ echo Counter::$count; // 2
360360
echo Counter::getCount(); // 2
361361
```
362362

363+
## Using Classes as Properties
364+
365+
By default, `#[php_class]` types cannot be used directly as properties of other
366+
`#[php_class]` types because they don't implement `FromZval`. To enable this,
367+
use the `class_derives_clone!` macro on any class that needs to be used as a
368+
property.
369+
370+
The class must implement `Clone`, and calling `class_derives_clone!` will
371+
implement `FromZval` and `FromZendObject` for the type, allowing PHP objects
372+
to be cloned into Rust values.
373+
374+
```rust,ignore
375+
use ext_php_rs::prelude::*;
376+
use ext_php_rs::class_derives_clone;
377+
378+
// Inner class that will be used as a property
379+
#[php_class]
380+
#[derive(Clone)]
381+
pub struct Address {
382+
#[php(prop)]
383+
pub street: String,
384+
#[php(prop)]
385+
pub city: String,
386+
}
387+
388+
// Enable this class to be used as a property
389+
class_derives_clone!(Address);
390+
391+
#[php_impl]
392+
impl Address {
393+
pub fn __construct(street: String, city: String) -> Self {
394+
Self { street, city }
395+
}
396+
}
397+
398+
// Outer class containing the inner class as a property
399+
#[php_class]
400+
pub struct Person {
401+
#[php(prop)]
402+
pub name: String,
403+
#[php(prop)]
404+
pub address: Address, // Works because we called class_derives_clone!
405+
}
406+
407+
#[php_impl]
408+
impl Person {
409+
pub fn __construct(name: String, address: Address) -> Self {
410+
Self { name, address }
411+
}
412+
413+
pub fn get_city(&self) -> String {
414+
self.address.city.clone()
415+
}
416+
}
417+
418+
#[php_module]
419+
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
420+
module
421+
.class::<Address>()
422+
.class::<Person>()
423+
}
424+
```
425+
426+
From PHP:
427+
428+
```php
429+
<?php
430+
431+
$address = new Address("123 Main St", "Springfield");
432+
$person = new Person("John Doe", $address);
433+
434+
echo $person->name; // "John Doe"
435+
echo $person->address->city; // "Springfield"
436+
echo $person->getCity(); // "Springfield"
437+
438+
// You can also set the nested property
439+
$newAddress = new Address("456 Oak Ave", "Shelbyville");
440+
$person->address = $newAddress;
441+
echo $person->address->city; // "Shelbyville"
442+
```
443+
444+
**Important notes:**
445+
446+
- The inner class must implement `Clone`
447+
- Call `class_derives_clone!` after the `#[php_class]` definition
448+
- When accessed from PHP, the property returns a clone of the Rust value
449+
- Modifications to the returned object don't affect the original unless reassigned
450+
451+
See [GitHub issue #182](https://github.com/extphprs/ext-php-rs/issues/182)
452+
for more context.
453+
363454
## Abstract Classes
364455

365456
Abstract classes cannot be instantiated directly and may contain abstract methods

src/macros.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,67 @@ macro_rules! class_derives {
395395
};
396396
}
397397

398+
/// Derives additional traits for cloneable [`RegisteredClass`] types to enable
399+
/// using them as properties of other `#[php_class]` structs.
400+
///
401+
/// This macro should be called for any `#[php_class]` struct that:
402+
/// 1. Implements [`Clone`]
403+
/// 2. Needs to be used as a property in another `#[php_class]` struct
404+
///
405+
/// The macro implements [`FromZendObject`] and [`FromZval`] for the owned type,
406+
/// allowing PHP objects to be cloned into Rust values.
407+
///
408+
/// # Example
409+
///
410+
/// ```ignore
411+
/// use ext_php_rs::prelude::*;
412+
/// use ext_php_rs::class_derives_clone;
413+
///
414+
/// #[php_class]
415+
/// #[derive(Clone)]
416+
/// struct Bar {
417+
/// #[php(prop)]
418+
/// value: String,
419+
/// }
420+
///
421+
/// class_derives_clone!(Bar);
422+
///
423+
/// #[php_class]
424+
/// struct Foo {
425+
/// #[php(prop)]
426+
/// bar: Bar, // Now works because Bar implements FromZval
427+
/// }
428+
/// ```
429+
///
430+
/// See: <https://github.com/extphprs/ext-php-rs/issues/182>
431+
///
432+
/// [`RegisteredClass`]: crate::class::RegisteredClass
433+
/// [`FromZendObject`]: crate::convert::FromZendObject
434+
/// [`FromZval`]: crate::convert::FromZval
435+
#[macro_export]
436+
macro_rules! class_derives_clone {
437+
($type: ty) => {
438+
impl $crate::convert::FromZendObject<'_> for $type {
439+
fn from_zend_object(obj: &$crate::types::ZendObject) -> $crate::error::Result<Self> {
440+
let class_obj = $crate::types::ZendClassObject::<$type>::from_zend_obj(obj)
441+
.ok_or($crate::error::Error::InvalidScope)?;
442+
Ok((**class_obj).clone())
443+
}
444+
}
445+
446+
impl $crate::convert::FromZval<'_> for $type {
447+
const TYPE: $crate::flags::DataType = $crate::flags::DataType::Object(Some(
448+
<$type as $crate::class::RegisteredClass>::CLASS_NAME,
449+
));
450+
451+
fn from_zval(zval: &$crate::types::Zval) -> ::std::option::Option<Self> {
452+
let obj = zval.object()?;
453+
<Self as $crate::convert::FromZendObject>::from_zend_object(obj).ok()
454+
}
455+
}
456+
};
457+
}
458+
398459
/// Derives `From<T> for Zval` and `IntoZval` for a given type.
399460
macro_rules! into_zval {
400461
($type: ty, $fn: ident, $dt: ident) => {

tests/src/integration/class/class.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,3 +348,28 @@ public function __construct(string $data) {
348348

349349
$uncloneable = new TestUncloneableClass('test');
350350
assert_exception_thrown(fn() => clone $uncloneable, 'Cloning uncloneable class should throw');
351+
352+
// Test issue #182 - class structs containing class struct properties
353+
$inner = new InnerClass('hello world');
354+
assert($inner->getValue() === 'hello world', 'InnerClass getValue should work');
355+
assert($inner->value === 'hello world', 'InnerClass property should be accessible');
356+
357+
$outer = new OuterClass($inner);
358+
assert($outer->getInnerValue() === 'hello world', 'OuterClass should be able to access inner value');
359+
360+
// Test that the inner property is properly accessible
361+
assert($outer->inner instanceof InnerClass, 'outer->inner should be InnerClass instance');
362+
assert($outer->inner->value === 'hello world', 'outer->inner->value should be accessible');
363+
364+
// Test setting inner property
365+
$newInner = new InnerClass('new value');
366+
$outer->inner = $newInner;
367+
assert($outer->getInnerValue() === 'new value', 'After setting inner, value should be updated');
368+
369+
// Test clone-on-read behavior
370+
$outer->inner = new InnerClass('original');
371+
$a = $outer->inner;
372+
$b = $outer->inner;
373+
assert($a !== $b, 'Each read should return a clone');
374+
$a->value = 'changed';
375+
assert($outer->inner->value === 'original', 'Original should be unchanged');

tests/src/integration/class/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,47 @@ impl TestUncloneableClass {
601601
}
602602
}
603603

604+
// Test for issue #182 - class structs containing class struct properties
605+
// The inner class must derive Clone and call class_derives_clone!
606+
607+
#[php_class]
608+
#[derive(Clone, Default)]
609+
pub struct InnerClass {
610+
#[php(prop)]
611+
pub value: String,
612+
}
613+
614+
ext_php_rs::class_derives_clone!(InnerClass);
615+
616+
#[php_impl]
617+
impl InnerClass {
618+
pub fn __construct(value: String) -> Self {
619+
Self { value }
620+
}
621+
622+
pub fn get_value(&self) -> String {
623+
self.value.clone()
624+
}
625+
}
626+
627+
#[php_class]
628+
#[derive(Default)]
629+
pub struct OuterClass {
630+
#[php(prop)]
631+
pub inner: InnerClass,
632+
}
633+
634+
#[php_impl]
635+
impl OuterClass {
636+
pub fn __construct(inner: InnerClass) -> Self {
637+
Self { inner }
638+
}
639+
640+
pub fn get_inner_value(&self) -> String {
641+
self.inner.value.clone()
642+
}
643+
}
644+
604645
pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
605646
let builder = builder
606647
.class::<TestClass>()
@@ -621,6 +662,8 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
621662
.class::<TestChildClass>()
622663
.class::<TestCloneableClass>()
623664
.class::<TestUncloneableClass>()
665+
.class::<InnerClass>()
666+
.class::<OuterClass>()
624667
.function(wrap_function!(test_class))
625668
.function(wrap_function!(throw_exception));
626669

0 commit comments

Comments
 (0)