@@ -301,63 +301,70 @@ protected function isEmpty(array $values): bool
301301 * Validate object attribute query values.
302302 *
303303 * Disallows ambiguous nested structures like:
304- * ['a' => [1, 'b' => [212]]]
305- * ['role' => ['name' => [...], 'ex' => [...]]]
304+ * ['a' => [1, 'b' => [212]]] // mixed list
305+ * ['role' => ['name' => [...], 'ex' => [...]]] // multiple nested paths
306306 *
307307 * but allows:
308- * ['a' => [1, 2], 'b' => [212]]
308+ * ['a' => [1, 2], 'b' => [212]] // multiple top-level paths
309+ * ['projects' => [[...]]] // list of objects
309310 *
310311 * @param array<mixed> $values
311312 * @return bool
312313 */
313314 private function isValidObjectQueryValues (array $ values ): bool
314315 {
315- $ validateNode = function (mixed $ node , bool $ isInList = false ) use (&$ validateNode ): bool {
316+ $ validate = function (mixed $ node , int $ depth = 0 , bool $ inDataContext = false ) use (&$ validate ): bool {
316317 if (!\is_array ($ node )) {
317318 return true ;
318319 }
319320
320321 if (\array_is_list ($ node )) {
322+ // Check if list is mixed (has both assoc arrays and non-assoc items)
323+ $ hasAssoc = false ;
324+ $ hasNonAssoc = false ;
325+
321326 foreach ($ node as $ item ) {
322- if (!$ validateNode ($ item , true )) {
323- return false ;
327+ if (\is_array ($ item ) && !\array_is_list ($ item )) {
328+ $ hasAssoc = true ;
329+ } else {
330+ $ hasNonAssoc = true ;
324331 }
325332 }
326333
327- return true ;
328- }
334+ // Mixed lists are invalid
335+ if ($ hasAssoc && $ hasNonAssoc ) {
336+ return false ;
337+ }
329338
330- if (!$ isInList && \count ($ node ) !== 1 ) {
331- return false ;
332- }
339+ // If list contains associative arrays, they're data objects
340+ $ enterDataContext = $ hasAssoc ;
333341
334- if ($ isInList ) {
335- foreach ($ node as $ value ) {
336- // When in a list context, values of associative arrays are also object structures,
337- // not navigation paths, so pass isInList=true for nested associative arrays
338- $ valueIsInList = \is_array ($ value ) && !\array_is_list ($ value );
339- if (!$ validateNode ($ value , $ valueIsInList )) {
342+ foreach ($ node as $ item ) {
343+ if (!$ validate ($ item , $ depth + 1 , $ enterDataContext || $ inDataContext )) {
340344 return false ;
341345 }
342346 }
343347 return true ;
344348 }
345349
346- $ firstKey = \array_key_first ($ node );
347- return $ validateNode ($ node [$ firstKey ], false );
348- };
349-
350- // Check if values is an indexed array (list)
351- // If so, its elements should be validated with isInList=true
352- $ valuesIsIndexed = \array_is_list ($ values );
353-
354- foreach ($ values as $ value ) {
355- if (!$ validateNode ($ value , $ valuesIsIndexed )) {
350+ // Associative array
351+ // If in data context, multiple keys are OK (it's an object)
352+ // If depth > 0 and NOT in data context, only 1 key allowed (navigation)
353+ if (!$ inDataContext && $ depth > 0 && \count ($ node ) !== 1 ) {
356354 return false ;
357355 }
358- }
359356
360- return true ;
357+ // Validate all values
358+ foreach ($ node as $ value ) {
359+ if (!$ validate ($ value , $ depth + 1 , $ inDataContext )) {
360+ return false ;
361+ }
362+ }
363+
364+ return true ;
365+ };
366+
367+ return $ validate ($ values , 0 , false );
361368 }
362369
363370 /**
0 commit comments