@@ -493,7 +493,7 @@ impl<'a> Args<'a> {
493493
494494 // If the variable is `&[&Zval]` treat it as the variadic argument.
495495 let default = defaults. remove ( ident) ;
496- let nullable = type_is_nullable ( ty. as_ref ( ) , default . is_some ( ) ) ?;
496+ let nullable = type_is_nullable ( ty. as_ref ( ) ) ?;
497497 let ( variadic, as_ref, ty) = Self :: parse_typed ( ty) ;
498498 result. typed . push ( TypedArg {
499499 name : ident,
@@ -570,18 +570,20 @@ impl<'a> Args<'a> {
570570 /// # Parameters
571571 ///
572572 /// * `optional` - The first optional argument. If [`None`], the optional
573- /// arguments will be from the first nullable argument after the last
574- /// non-nullable argument to the end of the arguments.
573+ /// arguments will be from the first optional argument (nullable or has
574+ /// default) after the last required argument to the end of the arguments.
575575 pub fn split_args ( & self , optional : Option < & Ident > ) -> ( & [ TypedArg < ' a > ] , & [ TypedArg < ' a > ] ) {
576576 let mut mid = None ;
577577 for ( i, arg) in self . typed . iter ( ) . enumerate ( ) {
578+ // An argument is optional if it's nullable (Option<T>) or has a default value.
579+ let is_optional = arg. nullable || arg. default . is_some ( ) ;
578580 if let Some ( optional) = optional {
579581 if optional == arg. name {
580582 mid. replace ( i) ;
581583 }
582- } else if mid. is_none ( ) && arg . nullable {
584+ } else if mid. is_none ( ) && is_optional {
583585 mid. replace ( i) ;
584- } else if !arg . nullable {
586+ } else if !is_optional {
585587 mid. take ( ) ;
586588 }
587589 }
@@ -635,7 +637,7 @@ impl TypedArg<'_> {
635637 None
636638 } ;
637639 let default = self . default . as_ref ( ) . map ( |val| {
638- let val = val . to_token_stream ( ) . to_string ( ) ;
640+ let val = expr_to_php_stub ( val ) ;
639641 quote ! {
640642 . default ( #val)
641643 }
@@ -659,8 +661,49 @@ impl TypedArg<'_> {
659661 fn accessor ( & self , bail_fn : impl Fn ( TokenStream ) -> TokenStream ) -> TokenStream {
660662 let name = self . name ;
661663 if let Some ( default) = & self . default {
662- quote ! {
663- #name. val( ) . unwrap_or( #default . into( ) )
664+ if self . nullable {
665+ // For nullable types with defaults, null is acceptable
666+ quote ! {
667+ #name. val( ) . unwrap_or( #default . into( ) )
668+ }
669+ } else {
670+ // For non-nullable types with defaults:
671+ // - If argument was omitted: use default
672+ // - If null was explicitly passed: throw TypeError
673+ // - If a value was passed: try to convert it
674+ let bail_null = bail_fn ( quote ! {
675+ :: ext_php_rs:: exception:: PhpException :: new(
676+ concat!( "Argument `$" , stringify!( #name) , "` must not be null" ) . into( ) ,
677+ 0 ,
678+ :: ext_php_rs:: zend:: ce:: type_error( ) ,
679+ )
680+ } ) ;
681+ let bail_invalid = bail_fn ( quote ! {
682+ :: ext_php_rs:: exception:: PhpException :: default (
683+ concat!( "Invalid value given for argument `" , stringify!( #name) , "`." ) . into( )
684+ )
685+ } ) ;
686+ quote ! {
687+ match #name. zval( ) {
688+ Some ( zval) if zval. is_null( ) => {
689+ // Null was explicitly passed to a non-nullable parameter
690+ #bail_null
691+ }
692+ Some ( _) => {
693+ // A value was passed, try to convert it
694+ match #name. val( ) {
695+ Some ( val) => val,
696+ None => {
697+ #bail_invalid
698+ }
699+ }
700+ }
701+ None => {
702+ // Argument was omitted, use default
703+ #default . into( )
704+ }
705+ }
706+ }
664707 }
665708 } else if self . variadic {
666709 let variadic_name = format_ident ! ( "__variadic_{}" , name) ;
@@ -692,21 +735,132 @@ impl TypedArg<'_> {
692735 }
693736}
694737
695- /// Returns true of the given type is nullable in PHP.
738+ /// Converts a Rust expression to a PHP stub-compatible default value string.
739+ ///
740+ /// This function handles common Rust patterns and converts them to valid PHP
741+ /// syntax for use in generated stub files:
742+ ///
743+ /// - `None` → `"null"`
744+ /// - `Some(expr)` → converts the inner expression
745+ /// - `42`, `3.14` → numeric literals as-is
746+ /// - `true`/`false` → as-is
747+ /// - `"string"` → `"string"`
748+ /// - `"string".to_string()` or `String::from("string")` → `"string"`
749+ fn expr_to_php_stub ( expr : & Expr ) -> String {
750+ match expr {
751+ // Handle None -> null
752+ Expr :: Path ( path) => {
753+ let path_str = path. path . to_token_stream ( ) . to_string ( ) ;
754+ if path_str == "None" {
755+ "null" . to_string ( )
756+ } else if path_str == "true" || path_str == "false" {
757+ path_str
758+ } else {
759+ // For other paths (constants, etc.), use the raw representation
760+ path_str
761+ }
762+ }
763+
764+ // Handle Some(expr) -> convert inner expression
765+ Expr :: Call ( call) => {
766+ if let Expr :: Path ( func_path) = & * call. func {
767+ let func_name = func_path. path . to_token_stream ( ) . to_string ( ) ;
768+
769+ // Some(value) -> convert inner value
770+ if func_name == "Some"
771+ && let Some ( arg) = call. args . first ( )
772+ {
773+ return expr_to_php_stub ( arg) ;
774+ }
775+
776+ // String::from("...") -> "..."
777+ if ( func_name == "String :: from" || func_name == "String::from" )
778+ && let Some ( arg) = call. args . first ( )
779+ {
780+ return expr_to_php_stub ( arg) ;
781+ }
782+ }
783+
784+ // Default: use raw representation
785+ expr. to_token_stream ( ) . to_string ( )
786+ }
787+
788+ // Handle method calls like "string".to_string()
789+ Expr :: MethodCall ( method_call) => {
790+ let method_name = method_call. method . to_string ( ) ;
791+
792+ // "...".to_string() or "...".to_owned() or "...".into() -> "..."
793+ if method_name == "to_string" || method_name == "to_owned" || method_name == "into" {
794+ return expr_to_php_stub ( & method_call. receiver ) ;
795+ }
796+
797+ // Default: use raw representation
798+ expr. to_token_stream ( ) . to_string ( )
799+ }
800+
801+ // String literals -> keep as-is (already valid PHP)
802+ Expr :: Lit ( lit) => match & lit. lit {
803+ syn:: Lit :: Str ( s) => format ! (
804+ "\" {}\" " ,
805+ s. value( ) . replace( '\\' , "\\ \\ " ) . replace( '"' , "\\ \" " )
806+ ) ,
807+ syn:: Lit :: Int ( i) => i. to_string ( ) ,
808+ syn:: Lit :: Float ( f) => f. to_string ( ) ,
809+ syn:: Lit :: Bool ( b) => if b. value { "true" } else { "false" } . to_string ( ) ,
810+ syn:: Lit :: Char ( c) => format ! ( "\" {}\" " , c. value( ) ) ,
811+ _ => expr. to_token_stream ( ) . to_string ( ) ,
812+ } ,
813+
814+ // Handle arrays: [] or vec![]
815+ Expr :: Array ( arr) => {
816+ if arr. elems . is_empty ( ) {
817+ "[]" . to_string ( )
818+ } else {
819+ let elems: Vec < String > = arr. elems . iter ( ) . map ( expr_to_php_stub) . collect ( ) ;
820+ format ! ( "[{}]" , elems. join( ", " ) )
821+ }
822+ }
823+
824+ // Handle vec![] macro
825+ Expr :: Macro ( m) => {
826+ let macro_name = m. mac . path . to_token_stream ( ) . to_string ( ) ;
827+ if macro_name == "vec" {
828+ let tokens = m. mac . tokens . to_string ( ) ;
829+ if tokens. trim ( ) . is_empty ( ) {
830+ return "[]" . to_string ( ) ;
831+ }
832+ }
833+ // Default: use raw representation
834+ expr. to_token_stream ( ) . to_string ( )
835+ }
836+
837+ // Handle unary expressions like -42
838+ Expr :: Unary ( unary) => {
839+ let inner = expr_to_php_stub ( & unary. expr ) ;
840+ format ! ( "{}{}" , unary. op. to_token_stream( ) , inner)
841+ }
842+
843+ // Default: use raw representation
844+ _ => expr. to_token_stream ( ) . to_string ( ) ,
845+ }
846+ }
847+
848+ /// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
849+ ///
850+ /// Note: Having a default value does NOT make a type nullable. A parameter with
851+ /// a default value is optional (can be omitted), but passing `null` explicitly
852+ /// should still be rejected unless the type is `Option<T>`.
696853// TODO(david): Eventually move to compile-time constants for this (similar to
697854// FromZval::NULLABLE).
698- pub fn type_is_nullable ( ty : & Type , has_default : bool ) -> Result < bool > {
855+ pub fn type_is_nullable ( ty : & Type ) -> Result < bool > {
699856 Ok ( match ty {
700- syn:: Type :: Path ( path) => {
701- has_default
702- || path
703- . path
704- . segments
705- . iter ( )
706- . next_back ( )
707- . is_some_and ( |seg| seg. ident == "Option" )
708- }
709- syn:: Type :: Reference ( _) => false , /* Reference cannot be nullable unless */
857+ Type :: Path ( path) => path
858+ . path
859+ . segments
860+ . iter ( )
861+ . next_back ( )
862+ . is_some_and ( |seg| seg. ident == "Option" ) ,
863+ Type :: Reference ( _) => false , /* Reference cannot be nullable unless */
710864 // wrapped in `Option` (in that case it'd be a Path).
711865 _ => bail ! ( ty => "Unsupported argument type." ) ,
712866 } )
0 commit comments