Skip to content

Commit 9b0c7f3

Browse files
committed
fix(class): Constructors with subclasses #138
# Conflicts: # tests/src/integration/class/class.php # tests/src/integration/class/mod.rs
1 parent 2d6026f commit 9b0c7f3

File tree

3 files changed

+130
-4
lines changed

3 files changed

+130
-4
lines changed

src/builders/class.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,38 @@ impl ClassBuilder {
211211
/// class name specified when creating the builder.
212212
pub fn object_override<T: RegisteredClass>(mut self) -> Self {
213213
extern "C" fn create_object<T: RegisteredClass>(ce: *mut ClassEntry) -> *mut ZendObject {
214+
let meta = T::get_metadata();
215+
let ce_ref = unsafe { ce.as_ref() };
216+
217+
// Check if this is our exact class or a PHP subclass.
218+
let is_our_class = ce_ref.is_some_and(|ce| ptr::eq(ce, meta.ce()));
219+
220+
if !is_our_class {
221+
// For PHP subclasses: create a ZendClassObject with a default/empty Rust backing.
222+
// This allows:
223+
// 1. Inherited Rust methods to work (they have an object to operate on)
224+
// 2. Method overriding to work (PHP's standard method resolution handles this)
225+
//
226+
// Note: We still use the subclass's ce so that:
227+
// - get_class() returns the subclass name
228+
// - instanceof works correctly
229+
// - PHP's method resolution finds overridden methods first
230+
if let Some(instance) = T::default_init() {
231+
let obj = ZendClassObject::<T>::new(instance);
232+
let zend_obj = obj.into_raw().get_mut_zend_obj();
233+
// Override the ce with the subclass's ce so that:
234+
// - get_class() returns the subclass name
235+
// - instanceof works correctly
236+
zend_obj.ce = ce;
237+
return zend_obj;
238+
}
239+
240+
// If no default_init, fall back to creating an uninitialized object
241+
// The constructor will initialize it
242+
let obj = unsafe { ZendClassObject::<T>::new_uninit(ce.as_ref()) };
243+
return obj.into_raw().get_mut_zend_obj();
244+
}
245+
214246
// Try to initialize with a default instance if available.
215247
// This is critical for exception classes that extend \Exception, because
216248
// PHP's zend_throw_exception_ex creates objects via create_object without
@@ -255,10 +287,10 @@ impl ClassBuilder {
255287

256288
// Use get_object_uninit because the Rust backing is not yet initialized.
257289
// We need access to the ZendClassObject to call initialize() on it.
290+
// For PHP subclasses (which don't have our custom handlers), this returns None.
291+
// In that case, we skip Rust initialization - the PHP subclass will work as
292+
// a regular PHP object without Rust backing (issue #138).
258293
let Some(this_obj) = ex.get_object_uninit::<T>() else {
259-
PhpException::default("Failed to retrieve reference to `this` object.".into())
260-
.throw()
261-
.expect("Failed to throw exception while constructing class");
262294
return;
263295
};
264296

tests/src/integration/class/class.php

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,8 @@
234234
assert(!$abstractReflection->getMethod('concreteMethod')->isAbstract(), 'concreteMethod should NOT be marked as abstract');
235235

236236
// Test extending the abstract class in PHP
237+
// Note: PHP subclasses of Rust classes don't have Rust backing, so inherited Rust
238+
// methods cannot be called. The subclass must override any methods it wants to use.
237239
class ConcreteTestClass extends TestAbstractClass {
238240
public function __construct() {
239241
parent::__construct();
@@ -242,11 +244,16 @@ public function __construct() {
242244
public function abstractMethod(): string {
243245
return 'implemented abstract method';
244246
}
247+
248+
// Must override concreteMethod since we can't call the inherited Rust method
249+
public function concreteMethod(): string {
250+
return 'concrete method from PHP subclass';
251+
}
245252
}
246253

247254
$concreteObj = new ConcreteTestClass();
248255
assert($concreteObj->abstractMethod() === 'implemented abstract method', 'Implemented abstract method should work');
249-
assert($concreteObj->concreteMethod() === 'concrete method in abstract class', 'Concrete method from abstract class should work');
256+
assert($concreteObj->concreteMethod() === 'concrete method from PHP subclass', 'Concrete method from PHP subclass should work');
250257

251258
// Test lazy objects (PHP 8.4+)
252259
if (PHP_VERSION_ID >= 80400) {
@@ -348,3 +355,71 @@ public function __construct(string $data) {
348355

349356
$uncloneable = new TestUncloneableClass('test');
350357
assert_exception_thrown(fn() => clone $uncloneable, 'Cloning uncloneable class should throw');
358+
359+
// Test issue #138 - PHP subclass of Rust class extending a non-abstract class
360+
class PhpSubclassOfArrayAccess extends TestClassArrayAccess {
361+
public function __construct() {
362+
// Call parent constructor
363+
parent::__construct();
364+
}
365+
366+
public function customMethod(): string {
367+
return 'custom method result';
368+
}
369+
370+
// Must override inherited methods since PHP subclass doesn't have Rust backing
371+
public function offsetExists($offset): bool {
372+
// Reimplement the logic instead of calling parent
373+
return is_int($offset);
374+
}
375+
}
376+
377+
$phpSubclass = new PhpSubclassOfArrayAccess();
378+
assert($phpSubclass instanceof TestClassArrayAccess, 'PHP subclass should be instanceof parent class');
379+
assert($phpSubclass instanceof TestClassArrayAccess, 'PHP subclass should work with parent class methods');
380+
// Test overridden method
381+
assert($phpSubclass->offsetExists(1) === true, 'Overridden method should work');
382+
// Test custom method
383+
assert($phpSubclass->customMethod() === 'custom method result', 'Custom method should work');
384+
385+
// Test issue #138 - Greeter example from the issue
386+
// Test regular Rust class works
387+
$greeter = new TestGreeter('world');
388+
assert($greeter->greet() === 'Hello, world!', 'Regular Rust class should work');
389+
390+
// Test PHP subclass can override methods
391+
$greeterSubclass = new class extends TestGreeter {
392+
public function __construct() {
393+
parent::__construct('php');
394+
}
395+
396+
// Must override to use it
397+
public function greet(): string {
398+
return 'Hello from PHP!';
399+
}
400+
};
401+
assert($greeterSubclass->greet() === 'Hello from PHP!', 'PHP subclass method override should work');
402+
// The overridden method is called, not the parent's
403+
404+
// Test calling inherited Rust methods on PHP subclass (issue #138)
405+
class PhpSubclassGreeter extends TestGreeter {
406+
public function __construct() {
407+
parent::__construct('inherited');
408+
}
409+
// NOT overriding greet - should call parent's Rust method
410+
}
411+
$inheritedGreeter = new PhpSubclassGreeter();
412+
assert($inheritedGreeter->greet() === 'Hello, inherited!', 'Inherited Rust method should work on PHP subclass');
413+
assert(get_class($inheritedGreeter) === 'PhpSubclassGreeter', 'get_class should return subclass name');
414+
assert($inheritedGreeter instanceof TestGreeter, 'instanceof should work for parent class');
415+
assert($inheritedGreeter instanceof PhpSubclassGreeter, 'instanceof should work for subclass');
416+
417+
// Test anonymous class with inherited method
418+
$anonInherited = new class extends TestGreeter {
419+
public function __construct() {
420+
parent::__construct('anon');
421+
}
422+
// NOT overriding greet
423+
};
424+
assert($anonInherited->greet() === 'Hello, anon!', 'Anonymous class should inherit Rust method');
425+

tests/src/integration/class/mod.rs

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

604+
/// Test class for issue #138 - Greeter example from the issue
605+
#[php_class]
606+
#[derive(Default)]
607+
pub struct TestGreeter {
608+
name: String,
609+
}
610+
611+
#[php_impl]
612+
impl TestGreeter {
613+
pub fn __construct(name: String) -> Self {
614+
Self { name }
615+
}
616+
617+
pub fn greet(&self) -> String {
618+
format!("Hello, {}!", self.name)
619+
}
620+
}
621+
604622
pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
605623
let builder = builder
606624
.class::<TestClass>()
625+
.class::<TestGreeter>()
607626
.class::<TestClassArrayAccess>()
608627
.class::<TestClassExtends>()
609628
.class::<TestClassExtendsImpl>()

0 commit comments

Comments
 (0)