Skip to content

Commit f669ddb

Browse files
committed
feat(class): Class structs can now contain other class structs #182
1 parent 85c8e3e commit f669ddb

5 files changed

Lines changed: 304 additions & 0 deletions

File tree

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
@@ -333,6 +333,67 @@ macro_rules! class_derives {
333333
};
334334
}
335335

336+
/// Derives additional traits for cloneable [`RegisteredClass`] types to enable
337+
/// using them as properties of other `#[php_class]` structs.
338+
///
339+
/// This macro should be called for any `#[php_class]` struct that:
340+
/// 1. Implements [`Clone`]
341+
/// 2. Needs to be used as a property in another `#[php_class]` struct
342+
///
343+
/// The macro implements [`FromZendObject`] and [`FromZval`] for the owned type,
344+
/// allowing PHP objects to be cloned into Rust values.
345+
///
346+
/// # Example
347+
///
348+
/// ```ignore
349+
/// use ext_php_rs::prelude::*;
350+
/// use ext_php_rs::class_derives_clone;
351+
///
352+
/// #[php_class]
353+
/// #[derive(Clone)]
354+
/// struct Bar {
355+
/// #[php(prop)]
356+
/// value: String,
357+
/// }
358+
///
359+
/// class_derives_clone!(Bar);
360+
///
361+
/// #[php_class]
362+
/// struct Foo {
363+
/// #[php(prop)]
364+
/// bar: Bar, // Now works because Bar implements FromZval
365+
/// }
366+
/// ```
367+
///
368+
/// See: <https://github.com/extphprs/ext-php-rs/issues/182>
369+
///
370+
/// [`RegisteredClass`]: crate::class::RegisteredClass
371+
/// [`FromZendObject`]: crate::convert::FromZendObject
372+
/// [`FromZval`]: crate::convert::FromZval
373+
#[macro_export]
374+
macro_rules! class_derives_clone {
375+
($type: ty) => {
376+
impl $crate::convert::FromZendObject<'_> for $type {
377+
fn from_zend_object(obj: &$crate::types::ZendObject) -> $crate::error::Result<Self> {
378+
let class_obj = $crate::types::ZendClassObject::<$type>::from_zend_obj(obj)
379+
.ok_or($crate::error::Error::InvalidScope)?;
380+
Ok((**class_obj).clone())
381+
}
382+
}
383+
384+
impl $crate::convert::FromZval<'_> for $type {
385+
const TYPE: $crate::flags::DataType = $crate::flags::DataType::Object(Some(
386+
<$type as $crate::class::RegisteredClass>::CLASS_NAME,
387+
));
388+
389+
fn from_zval(zval: &$crate::types::Zval) -> ::std::option::Option<Self> {
390+
let obj = zval.object()?;
391+
<Self as $crate::convert::FromZendObject>::from_zend_object(obj).ok()
392+
}
393+
}
394+
};
395+
}
396+
336397
/// Derives `From<T> for Zval` and `IntoZval` for a given type.
337398
macro_rules! into_zval {
338399
($type: ty, $fn: ident, $dt: ident) => {

tests/src/integration/class/class.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,20 @@ public function __construct(string $data) {
318318
$childReflection = new ReflectionClass(TestChildClass::class);
319319
assert($childReflection->getParentClass()->getName() === TestBaseClass::class, 'TestChildClass should extend TestBaseClass');
320320
assert($childObj instanceof TestBaseClass, 'TestChildClass instance should be instanceof TestBaseClass');
321+
322+
// Test issue #182 - class structs containing class struct properties
323+
$inner = new InnerClass('hello world');
324+
assert($inner->getValue() === 'hello world', 'InnerClass getValue should work');
325+
assert($inner->value === 'hello world', 'InnerClass property should be accessible');
326+
327+
$outer = new OuterClass($inner);
328+
assert($outer->getInnerValue() === 'hello world', 'OuterClass should be able to access inner value');
329+
330+
// Test that the inner property is properly accessible
331+
assert($outer->inner instanceof InnerClass, 'outer->inner should be InnerClass instance');
332+
assert($outer->inner->value === 'hello world', 'outer->inner->value should be accessible');
333+
334+
// Test setting inner property
335+
$newInner = new InnerClass('new value');
336+
$outer->inner = $newInner;
337+
assert($outer->getInnerValue() === 'new value', 'After setting inner, value should be updated');

tests/src/integration/class/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,47 @@ impl TestChildClass {
568568
}
569569
}
570570

571+
// Test for issue #182 - class structs containing class struct properties
572+
// The inner class must derive Clone and call class_derives_clone!
573+
574+
#[php_class]
575+
#[derive(Clone, Default)]
576+
pub struct InnerClass {
577+
#[php(prop)]
578+
pub value: String,
579+
}
580+
581+
ext_php_rs::class_derives_clone!(InnerClass);
582+
583+
#[php_impl]
584+
impl InnerClass {
585+
pub fn __construct(value: String) -> Self {
586+
Self { value }
587+
}
588+
589+
pub fn get_value(&self) -> String {
590+
self.value.clone()
591+
}
592+
}
593+
594+
#[php_class]
595+
#[derive(Default)]
596+
pub struct OuterClass {
597+
#[php(prop)]
598+
pub inner: InnerClass,
599+
}
600+
601+
#[php_impl]
602+
impl OuterClass {
603+
pub fn __construct(inner: InnerClass) -> Self {
604+
Self { inner }
605+
}
606+
607+
pub fn get_inner_value(&self) -> String {
608+
self.inner.value.clone()
609+
}
610+
}
611+
571612
pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
572613
let builder = builder
573614
.class::<TestClass>()
@@ -586,6 +627,8 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
586627
.class::<TestClassStaticStrGetter>()
587628
.class::<TestBaseClass>()
588629
.class::<TestChildClass>()
630+
.class::<InnerClass>()
631+
.class::<OuterClass>()
589632
.function(wrap_function!(test_class))
590633
.function(wrap_function!(throw_exception));
591634

0 commit comments

Comments
 (0)