66
77use ComplexHeart \Domain \Model \Traits \HasAttributes ;
88use ComplexHeart \Domain \Model \Traits \HasInvariants ;
9+ use InvalidArgumentException ;
10+ use ReflectionClass ;
11+ use ReflectionMethod ;
12+ use ReflectionNamedType ;
13+ use ReflectionUnionType ;
14+ use RuntimeException ;
15+ use TypeError ;
916
1017/**
1118 * Trait IsModel
@@ -35,16 +42,16 @@ trait IsModel
3542 *
3643 * @param mixed ...$params Constructor parameters
3744 * @return static
38- * @throws \ InvalidArgumentException When required parameters are missing
39- * @throws \ TypeError When parameter types don't match
45+ * @throws InvalidArgumentException When required parameters are missing
46+ * @throws TypeError When parameter types don't match
4047 */
4148 final public static function make (mixed ...$ params ): static
4249 {
43- $ reflection = new \ ReflectionClass (static ::class);
50+ $ reflection = new ReflectionClass (static ::class);
4451 $ constructor = $ reflection ->getConstructor ();
4552
4653 if (!$ constructor ) {
47- throw new \ RuntimeException (
54+ throw new RuntimeException (
4855 sprintf ('%s must have a constructor to use make() ' , static ::class)
4956 );
5057 }
@@ -66,14 +73,14 @@ final public static function make(mixed ...$params): static
6673 /**
6774 * Validate parameters match constructor signature.
6875 *
69- * @param \ ReflectionMethod $constructor
76+ * @param ReflectionMethod $constructor
7077 * @param array<int, mixed> $params
7178 * @return void
72- * @throws \ InvalidArgumentException
73- * @throws \ TypeError
79+ * @throws InvalidArgumentException
80+ * @throws TypeError
7481 */
7582 private static function validateConstructorParameters (
76- \ ReflectionMethod $ constructor ,
83+ ReflectionMethod $ constructor ,
7784 array $ params
7885 ): void {
7986 $ constructorParams = $ constructor ->getParameters ();
@@ -83,7 +90,7 @@ private static function validateConstructorParameters(
8390 if (count ($ params ) < $ required ) {
8491 $ missing = array_slice ($ constructorParams , count ($ params ), $ required - count ($ params ));
8592 $ names = array_map (fn ($ p ) => $ p ->getName (), $ missing );
86- throw new \ InvalidArgumentException (
93+ throw new InvalidArgumentException (
8794 sprintf (
8895 '%s::make() missing required parameters: %s ' ,
8996 basename (str_replace ('\\' , '/ ' , static ::class)),
@@ -101,20 +108,35 @@ private static function validateConstructorParameters(
101108 $ value = $ params [$ index ];
102109 $ type = $ param ->getType ();
103110
104- if (! $ type instanceof \ReflectionNamedType ) {
105- continue ; // No type hint or union type
111+ if ($ type === null ) {
112+ continue ; // No type hint
106113 }
107114
108- $ typeName = $ type ->getName ();
109- $ isValid = self ::validateType ($ value , $ typeName , $ type ->allowsNull ());
115+ $ isValid = false ;
116+ $ expectedTypes = '' ;
117+
118+ if ($ type instanceof ReflectionNamedType) {
119+ // Single type
120+ $ isValid = self ::validateType ($ value , $ type ->getName (), $ type ->allowsNull ());
121+ $ expectedTypes = $ type ->getName ();
122+ } elseif ($ type instanceof ReflectionUnionType) {
123+ // Union type (e.g., int|float|string)
124+ $ isValid = self ::validateUnionType ($ value , $ type );
125+ $ expectedTypes = implode ('| ' , array_map (
126+ fn ($ t ) => $ t instanceof ReflectionNamedType ? $ t ->getName () : 'mixed ' ,
127+ $ type ->getTypes ()
128+ ));
129+ } else {
130+ continue ; // Intersection types or other complex types not supported yet
131+ }
110132
111133 if (!$ isValid ) {
112- throw new \ TypeError (
134+ throw new TypeError (
113135 sprintf (
114136 '%s::make() parameter "%s" must be of type %s, %s given ' ,
115137 basename (str_replace ('\\' , '/ ' , static ::class)),
116138 $ param ->getName (),
117- $ typeName ,
139+ $ expectedTypes ,
118140 get_debug_type ($ value )
119141 )
120142 );
@@ -150,6 +172,45 @@ private static function validateType(mixed $value, string $typeName, bool $allow
150172 };
151173 }
152174
175+ /**
176+ * Validate a value matches one of the types in a union type.
177+ *
178+ * @param mixed $value
179+ * @param ReflectionUnionType $unionType
180+ * @return bool
181+ */
182+ private static function validateUnionType (mixed $ value , ReflectionUnionType $ unionType ): bool
183+ {
184+ // Check if null is allowed in the union
185+ $ allowsNull = $ unionType ->allowsNull ();
186+
187+ if ($ value === null ) {
188+ return $ allowsNull ;
189+ }
190+
191+ // Try to match against each type in the union
192+ foreach ($ unionType ->getTypes () as $ type ) {
193+ if (!$ type instanceof ReflectionNamedType) {
194+ continue ; // Skip non-named types (shouldn't happen in practice)
195+ }
196+
197+ $ typeName = $ type ->getName ();
198+
199+ // Skip 'null' type as we already handled it
200+ if ($ typeName === 'null ' ) {
201+ continue ;
202+ }
203+
204+ // If value matches this type, union is satisfied
205+ if (self ::validateType ($ value , $ typeName , false )) {
206+ return true ;
207+ }
208+ }
209+
210+ // Value didn't match any type in the union
211+ return false ;
212+ }
213+
153214 /**
154215 * Determine if invariants should be checked automatically after construction.
155216 *
0 commit comments