Skip to content

Commit c9ed5c4

Browse files
committed
fix(class): Return Self ($this) #502
1 parent 6523879 commit c9ed5c4

File tree

4 files changed

+233
-23
lines changed

4 files changed

+233
-23
lines changed

crates/macros/src/function.rs

Lines changed: 149 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::HashMap;
22

33
use darling::{FromAttributes, ToTokens};
44
use proc_macro2::{Ident, Span, TokenStream};
5-
use quote::{format_ident, quote};
5+
use quote::{format_ident, quote, quote_spanned};
66
use syn::spanned::Spanned as _;
77
use syn::{Expr, FnArg, GenericArgument, ItemFn, PatType, PathArguments, Type, TypePath};
88

@@ -11,6 +11,37 @@ use crate::parsing::{PhpRename, RenameRule, Visibility};
1111
use crate::prelude::*;
1212
use crate::syn_ext::DropLifetimes;
1313

14+
/// Checks if the return type is a reference to Self (`&Self` or `&mut Self`).
15+
/// This is used to detect methods that return `$this` in PHP.
16+
fn returns_self_ref(output: Option<&Type>) -> bool {
17+
let Some(ty) = output else {
18+
return false;
19+
};
20+
if let Type::Reference(ref_) = ty
21+
&& let Type::Path(path) = &*ref_.elem
22+
&& path.path.segments.len() == 1
23+
&& let Some(segment) = path.path.segments.last()
24+
{
25+
return segment.ident == "Self";
26+
}
27+
false
28+
}
29+
30+
/// Checks if the return type is `Self` (not a reference).
31+
/// This is used to detect methods that return a new instance of the same class.
32+
fn returns_self(output: Option<&Type>) -> bool {
33+
let Some(ty) = output else {
34+
return false;
35+
};
36+
if let Type::Path(path) = ty
37+
&& path.path.segments.len() == 1
38+
&& let Some(segment) = path.path.segments.last()
39+
{
40+
return segment.ident == "Self";
41+
}
42+
false
43+
}
44+
1445
pub fn wrap(input: &syn::Path) -> Result<TokenStream> {
1546
let Some(func_name) = input.get_ident() else {
1647
bail!(input => "Pass a PHP function name into `wrap_function!()`.");
@@ -145,7 +176,7 @@ impl<'a> Function<'a> {
145176
.map(TypedArg::arg_builder)
146177
.collect::<Vec<_>>();
147178

148-
let returns = self.build_returns();
179+
let returns = self.build_returns(None);
149180
let docs = if self.docs.is_empty() {
150181
quote! {}
151182
} else {
@@ -166,7 +197,7 @@ impl<'a> Function<'a> {
166197
}
167198

168199
/// Generates the function builder for the function.
169-
pub fn function_builder(&self, call_type: CallType) -> TokenStream {
200+
pub fn function_builder(&self, call_type: &CallType) -> TokenStream {
170201
let name = &self.name;
171202
let (required, not_required) = self.args.split_args(self.optional.as_ref());
172203

@@ -188,7 +219,7 @@ impl<'a> Function<'a> {
188219
.map(TypedArg::arg_builder)
189220
.collect::<Vec<_>>();
190221

191-
let returns = self.build_returns();
222+
let returns = self.build_returns(Some(call_type));
192223
let result = self.build_result(call_type, required, not_required);
193224
let docs = if self.docs.is_empty() {
194225
quote! {}
@@ -199,24 +230,70 @@ impl<'a> Function<'a> {
199230
}
200231
};
201232

233+
// Static methods cannot return &Self or &mut Self
234+
if returns_self_ref(self.output)
235+
&& let CallType::Method {
236+
receiver: MethodReceiver::Static,
237+
..
238+
} = call_type
239+
&& let Some(output) = self.output
240+
{
241+
return quote_spanned! { output.span() =>
242+
compile_error!(
243+
"Static methods cannot return `&Self` or `&mut Self`. \
244+
Only instance methods can use fluent interface pattern returning `$this`."
245+
)
246+
};
247+
}
248+
249+
// Check if this method returns &Self or &mut Self
250+
// In that case, we need to return `this` (the ZendClassObject) directly
251+
let returns_this = returns_self_ref(self.output)
252+
&& matches!(
253+
call_type,
254+
CallType::Method {
255+
receiver: MethodReceiver::Class | MethodReceiver::ZendClassObject,
256+
..
257+
}
258+
);
259+
260+
let handler_body = if returns_this {
261+
quote! {
262+
use ::ext_php_rs::convert::IntoZval;
263+
264+
#(#arg_declarations)*
265+
#result
266+
267+
// The method returns &Self or &mut Self, use `this` directly
268+
if let Err(e) = this.set_zval(retval, false) {
269+
let e: ::ext_php_rs::exception::PhpException = e.into();
270+
e.throw().expect("Failed to throw PHP exception.");
271+
}
272+
}
273+
} else {
274+
quote! {
275+
use ::ext_php_rs::convert::IntoZval;
276+
277+
#(#arg_declarations)*
278+
let result = {
279+
#result
280+
};
281+
282+
if let Err(e) = result.set_zval(retval, false) {
283+
let e: ::ext_php_rs::exception::PhpException = e.into();
284+
e.throw().expect("Failed to throw PHP exception.");
285+
}
286+
}
287+
};
288+
202289
quote! {
203290
::ext_php_rs::builders::FunctionBuilder::new(#name, {
204291
::ext_php_rs::zend_fastcall! {
205292
extern fn handler(
206293
ex: &mut ::ext_php_rs::zend::ExecuteData,
207294
retval: &mut ::ext_php_rs::types::Zval,
208295
) {
209-
use ::ext_php_rs::convert::IntoZval;
210-
211-
#(#arg_declarations)*
212-
let result = {
213-
#result
214-
};
215-
216-
if let Err(e) = result.set_zval(retval, false) {
217-
let e: ::ext_php_rs::exception::PhpException = e.into();
218-
e.throw().expect("Failed to throw PHP exception.");
219-
}
296+
#handler_body
220297
}
221298
}
222299
handler
@@ -229,9 +306,38 @@ impl<'a> Function<'a> {
229306
}
230307
}
231308

232-
fn build_returns(&self) -> Option<TokenStream> {
309+
fn build_returns(&self, call_type: Option<&CallType>) -> Option<TokenStream> {
233310
self.output.cloned().map(|mut output| {
234311
output.drop_lifetimes();
312+
313+
// If returning &Self or &mut Self from a method, use the class type
314+
// for return type information since we return `this` (ZendClassObject)
315+
if returns_self_ref(self.output)
316+
&& let Some(CallType::Method { class, .. }) = call_type
317+
{
318+
return quote! {
319+
.returns(
320+
<&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::TYPE,
321+
false,
322+
<&mut ::ext_php_rs::types::ZendClassObject<#class> as ::ext_php_rs::convert::IntoZval>::NULLABLE,
323+
)
324+
};
325+
}
326+
327+
// If returning Self (new instance) from a method, replace Self with
328+
// the actual class type since Self won't resolve in generated code
329+
if returns_self(self.output)
330+
&& let Some(CallType::Method { class, .. }) = call_type
331+
{
332+
return quote! {
333+
.returns(
334+
<#class as ::ext_php_rs::convert::IntoZval>::TYPE,
335+
false,
336+
<#class as ::ext_php_rs::convert::IntoZval>::NULLABLE,
337+
)
338+
};
339+
}
340+
235341
quote! {
236342
.returns(
237343
<#output as ::ext_php_rs::convert::IntoZval>::TYPE,
@@ -244,7 +350,7 @@ impl<'a> Function<'a> {
244350

245351
fn build_result(
246352
&self,
247-
call_type: CallType,
353+
call_type: &CallType,
248354
required: &[TypedArg<'_>],
249355
not_required: &[TypedArg<'_>],
250356
) -> TokenStream {
@@ -274,6 +380,9 @@ impl<'a> Function<'a> {
274380
})
275381
});
276382

383+
// Check if this method returns &Self or &mut Self
384+
let returns_this = returns_self_ref(self.output);
385+
277386
match call_type {
278387
CallType::Function => quote! {
279388
let parse = ex.parser()
@@ -306,15 +415,33 @@ impl<'a> Function<'a> {
306415
};
307416
},
308417
};
309-
let call = match receiver {
310-
MethodReceiver::Static => {
418+
419+
// When returning &Self or &mut Self, discard the return value
420+
// (we'll use `this` directly in the handler)
421+
let call = match (receiver, returns_this) {
422+
(MethodReceiver::Static, _) => {
311423
quote! { #class::#ident(#({#arg_accessors}),*) }
312424
}
313-
MethodReceiver::Class => quote! { this.#ident(#({#arg_accessors}),*) },
314-
MethodReceiver::ZendClassObject => {
425+
(MethodReceiver::Class, true) => {
426+
quote! { let _ = this.#ident(#({#arg_accessors}),*); }
427+
}
428+
(MethodReceiver::Class, false) => {
429+
quote! { this.#ident(#({#arg_accessors}),*) }
430+
}
431+
(MethodReceiver::ZendClassObject, true) => {
432+
// Explicit scope helps with mutable borrow lifetime when
433+
// the method returns `&mut Self`
434+
quote! {
435+
{
436+
let _ = #class::#ident(this, #({#arg_accessors}),*);
437+
}
438+
}
439+
}
440+
(MethodReceiver::ZendClassObject, false) => {
315441
quote! { #class::#ident(this, #({#arg_accessors}),*) }
316442
}
317443
};
444+
318445
quote! {
319446
#this
320447
let parse_result = parse
@@ -336,7 +463,7 @@ impl<'a> Function<'a> {
336463
/// Generates a struct and impl for the `PhpFunction` trait.
337464
pub fn php_function_impl(&self) -> TokenStream {
338465
let internal_ident = self.internal_ident();
339-
let builder = self.function_builder(CallType::Function);
466+
let builder = self.function_builder(&CallType::Function);
340467

341468
quote! {
342469
#[doc(hidden)]

crates/macros/src/impl_.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ impl<'a> ParsedImpl<'a> {
241241
modifiers.insert(MethodModifier::Abstract);
242242
}
243243

244-
let builder = func.function_builder(call_type);
244+
let builder = func.function_builder(&call_type);
245245

246246
self.functions.push(FnBuilder {
247247
builder,

tests/src/integration/class/class.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@
1515
$class->selfMultiRef("bar");
1616
assert($class->getString() === 'Changed to bar');
1717

18+
// Test method returning Self (new instance)
19+
$newClass = $class->withString('new string');
20+
assert($newClass instanceof TestClass, 'withString should return TestClass instance');
21+
assert($newClass->getString() === 'new string', 'new instance should have new string');
22+
assert($class->getString() === 'Changed to bar', 'original instance should be unchanged');
23+
assert($newClass !== $class, 'should be different instances');
24+
1825
assert($class->getNumber() === 2022);
1926
$class->setNumber(2023);
2027
assert($class->getNumber() === 2023);
@@ -94,3 +101,24 @@
94101

95102
TestStaticProps::setCounter(100);
96103
assert(TestStaticProps::$staticCounter === 100, 'PHP should see Rust-set value');
104+
105+
// Test FluentBuilder - returning $this for method chaining (Issue #502)
106+
$builder = new FluentBuilder();
107+
assert($builder->getValue() === 0);
108+
assert($builder->getName() === '');
109+
110+
// Test single method call returning $this
111+
$result = $builder->setValue(42);
112+
assert($result === $builder, 'setValue should return $this');
113+
assert($builder->getValue() === 42);
114+
115+
// Test fluent interface / method chaining
116+
$builder2 = new FluentBuilder();
117+
$chainResult = $builder2->setValue(100)->setName('test');
118+
assert($chainResult === $builder2, 'Chained methods should return $this');
119+
assert($builder2->getValue() === 100);
120+
assert($builder2->getName() === 'test');
121+
122+
// Test returning &Self (immutable reference)
123+
$selfRef = $builder2->getSelf();
124+
assert($selfRef === $builder2, 'getSelf should return $this');

tests/src/integration/class/mod.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ impl TestClass {
5858
self_.string = format!("Changed to {val}");
5959
self_
6060
}
61+
62+
/// Returns a new instance with a different string (tests returning Self)
63+
pub fn with_string(&self, string: String) -> Self {
64+
Self {
65+
string,
66+
number: self.number,
67+
boolean_prop: self.boolean_prop,
68+
}
69+
}
6170
}
6271

6372
#[php_function]
@@ -223,6 +232,51 @@ impl TestStaticProps {
223232
}
224233
}
225234

235+
/// Test class for returning $this (Issue #502)
236+
/// This demonstrates returning &mut Self from methods for fluent interfaces
237+
#[php_class]
238+
pub struct FluentBuilder {
239+
value: i32,
240+
name: String,
241+
}
242+
243+
#[php_impl]
244+
impl FluentBuilder {
245+
pub fn __construct() -> Self {
246+
Self {
247+
value: 0,
248+
name: String::new(),
249+
}
250+
}
251+
252+
/// Set value and return $this for method chaining
253+
pub fn set_value(&mut self, value: i32) -> &mut Self {
254+
self.value = value;
255+
self
256+
}
257+
258+
/// Set name and return $this for method chaining
259+
pub fn set_name(&mut self, name: String) -> &mut Self {
260+
self.name = name;
261+
self
262+
}
263+
264+
/// Get the current value
265+
pub fn get_value(&self) -> i32 {
266+
self.value
267+
}
268+
269+
/// Get the current name
270+
pub fn get_name(&self) -> String {
271+
self.name.clone()
272+
}
273+
274+
/// Test returning &Self (immutable reference to self)
275+
pub fn get_self(&self) -> &Self {
276+
self
277+
}
278+
}
279+
226280
pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
227281
builder
228282
.class::<TestClass>()
@@ -232,6 +286,7 @@ pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder {
232286
.class::<TestClassMethodVisibility>()
233287
.class::<TestClassProtectedConstruct>()
234288
.class::<TestStaticProps>()
289+
.class::<FluentBuilder>()
235290
.function(wrap_function!(test_class))
236291
.function(wrap_function!(throw_exception))
237292
}

0 commit comments

Comments
 (0)