@@ -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 }
@@ -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,22 @@ impl TypedArg<'_> {
692735 }
693736}
694737
695- /// Returns true of the given type is nullable in PHP.
738+ /// Returns true if the given type is nullable in PHP (i.e., it's an `Option<T>`).
739+ ///
740+ /// Note: Having a default value does NOT make a type nullable. A parameter with
741+ /// a default value is optional (can be omitted), but passing `null` explicitly
742+ /// should still be rejected unless the type is `Option<T>`.
696743// TODO(david): Eventually move to compile-time constants for this (similar to
697744// FromZval::NULLABLE).
698- pub fn type_is_nullable ( ty : & Type , has_default : bool ) -> Result < bool > {
745+ pub fn type_is_nullable ( ty : & Type ) -> Result < bool > {
699746 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 */
747+ Type :: Path ( path) => path
748+ . path
749+ . segments
750+ . iter ( )
751+ . next_back ( )
752+ . is_some_and ( |seg| seg. ident == "Option" ) ,
753+ Type :: Reference ( _) => false , /* Reference cannot be nullable unless */
710754 // wrapped in `Option` (in that case it'd be a Path).
711755 _ => bail ! ( ty => "Unsupported argument type." ) ,
712756 } )
0 commit comments