@@ -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,
@@ -568,18 +568,20 @@ impl<'a> Args<'a> {
568568 /// # Parameters
569569 ///
570570 /// * `optional` - The first optional argument. If [`None`], the optional
571- /// arguments will be from the first nullable argument after the last
572- /// non-nullable argument to the end of the arguments.
571+ /// arguments will be from the first optional argument (nullable or has
572+ /// default) after the last required argument to the end of the arguments.
573573 pub fn split_args ( & self , optional : Option < & Ident > ) -> ( & [ TypedArg < ' a > ] , & [ TypedArg < ' a > ] ) {
574574 let mut mid = None ;
575575 for ( i, arg) in self . typed . iter ( ) . enumerate ( ) {
576+ // An argument is optional if it's nullable (Option<T>) or has a default value.
577+ let is_optional = arg. nullable || arg. default . is_some ( ) ;
576578 if let Some ( optional) = optional {
577579 if optional == arg. name {
578580 mid. replace ( i) ;
579581 }
580- } else if mid. is_none ( ) && arg . nullable {
582+ } else if mid. is_none ( ) && is_optional {
581583 mid. replace ( i) ;
582- } else if !arg . nullable {
584+ } else if !is_optional {
583585 mid. take ( ) ;
584586 }
585587 }
@@ -657,8 +659,49 @@ impl TypedArg<'_> {
657659 fn accessor ( & self , bail_fn : impl Fn ( TokenStream ) -> TokenStream ) -> TokenStream {
658660 let name = self . name ;
659661 if let Some ( default) = & self . default {
660- quote ! {
661- #name. val( ) . unwrap_or( #default . into( ) )
662+ if self . nullable {
663+ // For nullable types with defaults, null is acceptable
664+ quote ! {
665+ #name. val( ) . unwrap_or( #default . into( ) )
666+ }
667+ } else {
668+ // For non-nullable types with defaults:
669+ // - If argument was omitted: use default
670+ // - If null was explicitly passed: throw TypeError
671+ // - If a value was passed: try to convert it
672+ let bail_null = bail_fn ( quote ! {
673+ :: ext_php_rs:: exception:: PhpException :: new(
674+ concat!( "Argument `$" , stringify!( #name) , "` must not be null" ) . into( ) ,
675+ 0 ,
676+ :: ext_php_rs:: zend:: ce:: type_error( ) ,
677+ )
678+ } ) ;
679+ let bail_invalid = bail_fn ( quote ! {
680+ :: ext_php_rs:: exception:: PhpException :: default (
681+ concat!( "Invalid value given for argument `" , stringify!( #name) , "`." ) . into( )
682+ )
683+ } ) ;
684+ quote ! {
685+ match #name. zval( ) {
686+ Some ( zval) if zval. is_null( ) => {
687+ // Null was explicitly passed to a non-nullable parameter
688+ #bail_null
689+ }
690+ Some ( _) => {
691+ // A value was passed, try to convert it
692+ match #name. val( ) {
693+ Some ( val) => val,
694+ None => {
695+ #bail_invalid
696+ }
697+ }
698+ }
699+ None => {
700+ // Argument was omitted, use default
701+ #default . into( )
702+ }
703+ }
704+ }
662705 }
663706 } else if self . variadic {
664707 let variadic_name = format_ident ! ( "__variadic_{}" , name) ;
@@ -690,21 +733,22 @@ impl TypedArg<'_> {
690733 }
691734}
692735
693- /// Returns true of the given type is nullable in PHP.
736+ /// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
737+ ///
738+ /// Note: Having a default value does NOT make a type nullable. A parameter with
739+ /// a default value is optional (can be omitted), but passing `null` explicitly
740+ /// should still be rejected unless the type is `Option<T>`.
694741// TODO(david): Eventually move to compile-time constants for this (similar to
695742// FromZval::NULLABLE).
696- pub fn type_is_nullable ( ty : & Type , has_default : bool ) -> Result < bool > {
743+ pub fn type_is_nullable ( ty : & Type ) -> Result < bool > {
697744 Ok ( match ty {
698- syn:: Type :: Path ( path) => {
699- has_default
700- || path
701- . path
702- . segments
703- . iter ( )
704- . next_back ( )
705- . is_some_and ( |seg| seg. ident == "Option" )
706- }
707- syn:: Type :: Reference ( _) => false , /* Reference cannot be nullable unless */
745+ Type :: Path ( path) => path
746+ . path
747+ . segments
748+ . iter ( )
749+ . next_back ( )
750+ . is_some_and ( |seg| seg. ident == "Option" ) ,
751+ Type :: Reference ( _) => false , /* Reference cannot be nullable unless */
708752 // wrapped in `Option` (in that case it'd be a Path).
709753 _ => bail ! ( ty => "Unsupported argument type." ) ,
710754 } )
0 commit comments