44
55namespace Ray \InputQuery ;
66
7+ use ArrayObject ;
78use InvalidArgumentException ;
89use Override ;
910use Ray \Di \Di \Named ;
1011use Ray \Di \Di \Qualifier ;
1112use Ray \Di \Exception \Unbound ;
1213use Ray \Di \InjectorInterface ;
1314use Ray \InputQuery \Attribute \Input ;
15+ use ReflectionAttribute ;
1416use ReflectionClass ;
1517use ReflectionMethod ;
1618use ReflectionNamedType ;
1719use ReflectionParameter ;
1820
21+ use function array_key_exists ;
1922use function assert ;
2023use function class_exists ;
24+ use function gettype ;
25+ use function is_array ;
2126use function is_bool ;
2227use function is_float ;
2328use function is_int ;
2429use function is_numeric ;
2530use function is_scalar ;
2631use function is_string ;
32+ use function is_subclass_of ;
2733use function lcfirst ;
2834use function sprintf ;
2935use function str_replace ;
@@ -91,7 +97,15 @@ private function resolveParameter(ReflectionParameter $param, array $query): mix
9197 return $ this ->resolveFromDI ($ param );
9298 }
9399
94- // Has #[Input] attribute - get from query
100+ return $ this ->resolveInputParameter ($ param , $ query , $ inputAttributes );
101+ }
102+
103+ /**
104+ * @param array<string, mixed> $query
105+ * @param array<ReflectionAttribute<Input>> $inputAttributes
106+ */
107+ private function resolveInputParameter (ReflectionParameter $ param , array $ query , array $ inputAttributes ): mixed
108+ {
95109 $ type = $ param ->getType ();
96110 $ paramName = $ param ->getName ();
97111
@@ -100,27 +114,99 @@ private function resolveParameter(ReflectionParameter $param, array $query): mix
100114 }
101115
102116 if ($ type ->isBuiltin ()) {
103- // Scalar type with #[Input]
104- /** @psalm-suppress MixedAssignment $value */
105- $ value = $ query [$ paramName ] ?? $ this ->getDefaultValue ($ param );
117+ return $ this ->resolveBuiltinType ($ param , $ query , $ inputAttributes , $ type );
118+ }
119+
120+ return $ this ->resolveObjectType ($ param , $ query , $ inputAttributes , $ type );
121+ }
106122
107- return $ this ->convertScalar ($ value , $ type );
123+ /**
124+ * @param array<string, mixed> $query
125+ * @param array<ReflectionAttribute<Input>> $inputAttributes
126+ */
127+ private function resolveBuiltinType (ReflectionParameter $ param , array $ query , array $ inputAttributes , ReflectionNamedType $ type ): mixed
128+ {
129+ $ paramName = $ param ->getName ();
130+
131+ if ($ type ->getName () === 'array ' ) {
132+ $ inputAttribute = $ inputAttributes [0 ]->newInstance ();
133+ if ($ inputAttribute ->item !== null ) {
134+ assert (class_exists ($ inputAttribute ->item ));
135+ $ itemClass = $ inputAttribute ->item ;
136+
137+ /** @var class-string<T> $itemClass */
138+ return $ this ->createArrayOfInputs ($ paramName , $ query , $ itemClass );
139+ }
108140 }
109141
110- // Object type with #[Input] - create nested
142+ // Scalar type with #[Input]
143+ /** @psalm-suppress MixedAssignment $value */
144+ $ value = $ query [$ paramName ] ?? $ this ->getDefaultValue ($ param );
145+
146+ return $ this ->convertScalar ($ value , $ type );
147+ }
148+
149+ /**
150+ * @param array<string, mixed> $query
151+ * @param array<ReflectionAttribute<Input>> $inputAttributes
152+ */
153+ private function resolveObjectType (ReflectionParameter $ param , array $ query , array $ inputAttributes , ReflectionNamedType $ type ): mixed
154+ {
155+ $ paramName = $ param ->getName ();
156+ $ className = $ type ->getName ();
157+
158+ // Check for ArrayObject types with item specification
159+ $ arrayObjectResult = $ this ->resolveArrayObjectType ($ paramName , $ query , $ inputAttributes , $ className );
160+ if ($ arrayObjectResult !== null ) {
161+ return $ arrayObjectResult ;
162+ }
163+
164+ // Regular object type with #[Input] - create nested
111165 $ nestedQuery = $ this ->extractNestedQuery ($ paramName , $ query );
112166
113167 // If no nested keys found, try using the entire query
114- // This handles cases like controller method parameters
115168 if (empty ($ nestedQuery )) {
116169 $ nestedQuery = $ query ;
117170 }
118171
119- $ class = $ type ->getName ();
120- assert (class_exists ($ class ));
172+ assert (class_exists ($ className ));
173+
174+ /** @var class-string<T> $className */
175+ return $ this ->create ($ className , $ nestedQuery );
176+ }
177+
178+ /**
179+ * @param array<string, mixed> $query
180+ * @param array<ReflectionAttribute<Input>> $inputAttributes
181+ */
182+ private function resolveArrayObjectType (string $ paramName , array $ query , array $ inputAttributes , string $ className ): mixed
183+ {
184+ $ isArrayObjectSubclass = class_exists ($ className ) && is_subclass_of ($ className , ArrayObject::class);
185+ $ isArrayObject = $ className === ArrayObject::class;
186+
187+ if (! $ isArrayObjectSubclass && ! $ isArrayObject ) {
188+ return null ;
189+ }
121190
122- /** @var class-string<T> $class */
123- return $ this ->create ($ class , $ nestedQuery );
191+ $ inputAttribute = $ inputAttributes [0 ]->newInstance ();
192+ if ($ inputAttribute ->item === null ) {
193+ return null ;
194+ }
195+
196+ assert (class_exists ($ inputAttribute ->item ));
197+ /** @var class-string<T> $itemClass */
198+ $ itemClass = $ inputAttribute ->item ;
199+ $ array = $ this ->createArrayOfInputs ($ paramName , $ query , $ itemClass );
200+
201+ if ($ isArrayObject ) {
202+ return new ArrayObject ($ array );
203+ }
204+
205+ assert (class_exists ($ className ));
206+ /** @var class-string $className */
207+ $ reflectionClass = new ReflectionClass ($ className );
208+
209+ return $ reflectionClass ->newInstance ($ array );
124210 }
125211
126212 private function resolveFromDI (ReflectionParameter $ param ): mixed
@@ -245,4 +331,45 @@ private function toCamelCase(string $string): string
245331
246332 return lcfirst ($ string );
247333 }
334+
335+ /**
336+ * @param array<string, mixed> $query
337+ * @param class-string<T> $itemClass
338+ *
339+ * @return array<array-key, T>
340+ */
341+ private function createArrayOfInputs (string $ paramName , array $ query , string $ itemClass ): array
342+ {
343+ if (! array_key_exists ($ paramName , $ query )) {
344+ return [];
345+ }
346+
347+ /** @var mixed $arrayData */
348+ $ arrayData = $ query [$ paramName ];
349+
350+ if (! is_array ($ arrayData )) {
351+ return [];
352+ }
353+
354+ $ result = [];
355+ /** @var mixed $itemData */
356+ foreach ($ arrayData as $ key => $ itemData ) {
357+ if (! is_array ($ itemData )) {
358+ throw new InvalidArgumentException (
359+ sprintf (
360+ 'Expected array for item at key "%s", got %s. ' ,
361+ $ key ,
362+ gettype ($ itemData ),
363+ ),
364+ );
365+ }
366+
367+ // Query parameters from HTTP requests have string keys
368+ /** @psalm-var array<string, mixed> $itemData */
369+ /** @phpstan-var array<string, mixed> $itemData */
370+ $ result [$ key ] = $ this ->create ($ itemClass , $ itemData );
371+ }
372+
373+ return $ result ;
374+ }
248375}
0 commit comments