@@ -2,7 +2,7 @@ use std::collections::HashMap;
22
33use darling:: { FromAttributes , ToTokens } ;
44use proc_macro2:: { Ident , Span , TokenStream } ;
5- use quote:: { format_ident, quote} ;
5+ use quote:: { format_ident, quote, quote_spanned } ;
66use syn:: spanned:: Spanned as _;
77use syn:: { Expr , FnArg , GenericArgument , ItemFn , PatType , PathArguments , Type , TypePath } ;
88
@@ -11,6 +11,37 @@ use crate::parsing::{PhpRename, RenameRule, Visibility};
1111use crate :: prelude:: * ;
1212use 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+
1445pub 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) ]
0 commit comments