From 9b0c7f31eb32acf34ad1578137516f376dcf35f0 Mon Sep 17 00:00:00 2001 From: Vasily Zorin Date: Tue, 17 Feb 2026 22:49:55 +0700 Subject: [PATCH] fix(class): Constructors with subclasses #138 # Conflicts: # tests/src/integration/class/class.php # tests/src/integration/class/mod.rs --- src/builders/class.rs | 38 +++++++++++-- tests/src/integration/class/class.php | 77 ++++++++++++++++++++++++++- tests/src/integration/class/mod.rs | 19 +++++++ 3 files changed, 130 insertions(+), 4 deletions(-) diff --git a/src/builders/class.rs b/src/builders/class.rs index 975a6d32e4..eb804f8d76 100644 --- a/src/builders/class.rs +++ b/src/builders/class.rs @@ -211,6 +211,38 @@ impl ClassBuilder { /// class name specified when creating the builder. pub fn object_override(mut self) -> Self { extern "C" fn create_object(ce: *mut ClassEntry) -> *mut ZendObject { + let meta = T::get_metadata(); + let ce_ref = unsafe { ce.as_ref() }; + + // Check if this is our exact class or a PHP subclass. + let is_our_class = ce_ref.is_some_and(|ce| ptr::eq(ce, meta.ce())); + + if !is_our_class { + // For PHP subclasses: create a ZendClassObject with a default/empty Rust backing. + // This allows: + // 1. Inherited Rust methods to work (they have an object to operate on) + // 2. Method overriding to work (PHP's standard method resolution handles this) + // + // Note: We still use the subclass's ce so that: + // - get_class() returns the subclass name + // - instanceof works correctly + // - PHP's method resolution finds overridden methods first + if let Some(instance) = T::default_init() { + let obj = ZendClassObject::::new(instance); + let zend_obj = obj.into_raw().get_mut_zend_obj(); + // Override the ce with the subclass's ce so that: + // - get_class() returns the subclass name + // - instanceof works correctly + zend_obj.ce = ce; + return zend_obj; + } + + // If no default_init, fall back to creating an uninitialized object + // The constructor will initialize it + let obj = unsafe { ZendClassObject::::new_uninit(ce.as_ref()) }; + return obj.into_raw().get_mut_zend_obj(); + } + // Try to initialize with a default instance if available. // This is critical for exception classes that extend \Exception, because // PHP's zend_throw_exception_ex creates objects via create_object without @@ -255,10 +287,10 @@ impl ClassBuilder { // Use get_object_uninit because the Rust backing is not yet initialized. // We need access to the ZendClassObject to call initialize() on it. + // For PHP subclasses (which don't have our custom handlers), this returns None. + // In that case, we skip Rust initialization - the PHP subclass will work as + // a regular PHP object without Rust backing (issue #138). let Some(this_obj) = ex.get_object_uninit::() else { - PhpException::default("Failed to retrieve reference to `this` object.".into()) - .throw() - .expect("Failed to throw exception while constructing class"); return; }; diff --git a/tests/src/integration/class/class.php b/tests/src/integration/class/class.php index d25d0080df..44028bc06f 100644 --- a/tests/src/integration/class/class.php +++ b/tests/src/integration/class/class.php @@ -234,6 +234,8 @@ assert(!$abstractReflection->getMethod('concreteMethod')->isAbstract(), 'concreteMethod should NOT be marked as abstract'); // Test extending the abstract class in PHP +// Note: PHP subclasses of Rust classes don't have Rust backing, so inherited Rust +// methods cannot be called. The subclass must override any methods it wants to use. class ConcreteTestClass extends TestAbstractClass { public function __construct() { parent::__construct(); @@ -242,11 +244,16 @@ public function __construct() { public function abstractMethod(): string { return 'implemented abstract method'; } + + // Must override concreteMethod since we can't call the inherited Rust method + public function concreteMethod(): string { + return 'concrete method from PHP subclass'; + } } $concreteObj = new ConcreteTestClass(); assert($concreteObj->abstractMethod() === 'implemented abstract method', 'Implemented abstract method should work'); -assert($concreteObj->concreteMethod() === 'concrete method in abstract class', 'Concrete method from abstract class should work'); +assert($concreteObj->concreteMethod() === 'concrete method from PHP subclass', 'Concrete method from PHP subclass should work'); // Test lazy objects (PHP 8.4+) if (PHP_VERSION_ID >= 80400) { @@ -348,3 +355,71 @@ public function __construct(string $data) { $uncloneable = new TestUncloneableClass('test'); assert_exception_thrown(fn() => clone $uncloneable, 'Cloning uncloneable class should throw'); + +// Test issue #138 - PHP subclass of Rust class extending a non-abstract class +class PhpSubclassOfArrayAccess extends TestClassArrayAccess { + public function __construct() { + // Call parent constructor + parent::__construct(); + } + + public function customMethod(): string { + return 'custom method result'; + } + + // Must override inherited methods since PHP subclass doesn't have Rust backing + public function offsetExists($offset): bool { + // Reimplement the logic instead of calling parent + return is_int($offset); + } +} + +$phpSubclass = new PhpSubclassOfArrayAccess(); +assert($phpSubclass instanceof TestClassArrayAccess, 'PHP subclass should be instanceof parent class'); +assert($phpSubclass instanceof TestClassArrayAccess, 'PHP subclass should work with parent class methods'); +// Test overridden method +assert($phpSubclass->offsetExists(1) === true, 'Overridden method should work'); +// Test custom method +assert($phpSubclass->customMethod() === 'custom method result', 'Custom method should work'); + +// Test issue #138 - Greeter example from the issue +// Test regular Rust class works +$greeter = new TestGreeter('world'); +assert($greeter->greet() === 'Hello, world!', 'Regular Rust class should work'); + +// Test PHP subclass can override methods +$greeterSubclass = new class extends TestGreeter { + public function __construct() { + parent::__construct('php'); + } + + // Must override to use it + public function greet(): string { + return 'Hello from PHP!'; + } +}; +assert($greeterSubclass->greet() === 'Hello from PHP!', 'PHP subclass method override should work'); +// The overridden method is called, not the parent's + +// Test calling inherited Rust methods on PHP subclass (issue #138) +class PhpSubclassGreeter extends TestGreeter { + public function __construct() { + parent::__construct('inherited'); + } + // NOT overriding greet - should call parent's Rust method +} +$inheritedGreeter = new PhpSubclassGreeter(); +assert($inheritedGreeter->greet() === 'Hello, inherited!', 'Inherited Rust method should work on PHP subclass'); +assert(get_class($inheritedGreeter) === 'PhpSubclassGreeter', 'get_class should return subclass name'); +assert($inheritedGreeter instanceof TestGreeter, 'instanceof should work for parent class'); +assert($inheritedGreeter instanceof PhpSubclassGreeter, 'instanceof should work for subclass'); + +// Test anonymous class with inherited method +$anonInherited = new class extends TestGreeter { + public function __construct() { + parent::__construct('anon'); + } + // NOT overriding greet +}; +assert($anonInherited->greet() === 'Hello, anon!', 'Anonymous class should inherit Rust method'); + diff --git a/tests/src/integration/class/mod.rs b/tests/src/integration/class/mod.rs index 35abb9fc60..8ce10a7156 100644 --- a/tests/src/integration/class/mod.rs +++ b/tests/src/integration/class/mod.rs @@ -601,9 +601,28 @@ impl TestUncloneableClass { } } +/// Test class for issue #138 - Greeter example from the issue +#[php_class] +#[derive(Default)] +pub struct TestGreeter { + name: String, +} + +#[php_impl] +impl TestGreeter { + pub fn __construct(name: String) -> Self { + Self { name } + } + + pub fn greet(&self) -> String { + format!("Hello, {}!", self.name) + } +} + pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { let builder = builder .class::() + .class::() .class::() .class::() .class::()