1313
1414namespace CodeIgniter \Helpers \Array ;
1515
16+ use ArrayAccess ;
17+ use CodeIgniter \Entity \Entity ;
1618use CodeIgniter \Exceptions \InvalidArgumentException ;
19+ use Traversable ;
1720
1821/**
1922 * @internal This is internal implementation for the framework.
@@ -34,11 +37,12 @@ final class ArrayHelper
3437 *
3538 * @used-by dot_array_search()
3639 *
37- * @param string $index The index as dot array syntax.
40+ * @param string $index The index as dot array syntax.
41+ * @param array<array-key, mixed>|object $array
3842 *
39- * @return array|bool|int|object|string|null
43+ * @return array<array-key, mixed> |bool|int|object|string|null
4044 */
41- public static function dotSearch (string $ index , array $ array )
45+ public static function dotSearch (string $ index , array | object $ array )
4246 {
4347 return self ::arraySearchDot (self ::convertToArray ($ index ), $ array );
4448 }
@@ -78,9 +82,12 @@ private static function convertToArray(string $index): array
7882 *
7983 * @used-by dotSearch()
8084 *
81- * @return array|bool|float|int|object|string|null
85+ * @param list<string> $indexes
86+ * @param array<array-key, mixed>|object $array
87+ *
88+ * @return array<array-key, mixed>|bool|float|int|object|string|null
8289 */
83- private static function arraySearchDot (array $ indexes , array $ array )
90+ private static function arraySearchDot (array $ indexes , array | object $ array )
8491 {
8592 // If index is empty, returns null.
8693 if ($ indexes === []) {
@@ -90,16 +97,17 @@ private static function arraySearchDot(array $indexes, array $array)
9097 // Grab the current index
9198 $ currentIndex = array_shift ($ indexes );
9299
93- if (! isset ($ array[ $ currentIndex] ) && $ currentIndex !== '* ' ) {
100+ if (! self :: valueExists ($ array, $ currentIndex ) && $ currentIndex !== '* ' ) {
94101 return null ;
95102 }
96103
97104 // Handle Wildcard (*)
98105 if ($ currentIndex === '* ' ) {
99- $ answer = [];
106+ $ answer = [];
107+ $ iterable = is_object ($ array ) ? self ::toIterable ($ array ) : $ array ;
100108
101- foreach ($ array as $ value ) {
102- if (! is_array ($ value )) {
109+ foreach ($ iterable as $ value ) {
110+ if (! is_array ($ value ) && ! is_object ( $ value ) ) {
103111 return null ;
104112 }
105113
@@ -119,12 +127,14 @@ private static function arraySearchDot(array $indexes, array $array)
119127 // If this is the last index, make sure to return it now,
120128 // and not try to recurse through things.
121129 if ($ indexes === []) {
122- return $ array[ $ currentIndex] ;
130+ return self :: value ( $ array, $ currentIndex) ;
123131 }
124132
133+ $ value = self ::value ($ array , $ currentIndex );
134+
125135 // Do we need to recursively search this value?
126- if (is_array ($ array [ $ currentIndex ] ) && $ array [ $ currentIndex ] !== []) {
127- return self ::arraySearchDot ($ indexes , $ array [ $ currentIndex ] );
136+ if (( is_array ($ value ) && $ value !== []) || is_object ( $ value ) ) {
137+ return self ::arraySearchDot ($ indexes , $ value );
128138 }
129139
130140 // Otherwise, not found.
@@ -333,13 +343,16 @@ public static function groupBy(array $array, array $indexes, bool $includeEmpty
333343
334344 /**
335345 * Recursively attach $row to the $indexes path of values found by
336- * `dot_array_search()` .
346+ * dot syntax .
337347 *
338348 * @used-by groupBy()
349+ *
350+ * @param array<array-key, mixed>|object $row
351+ * @param list<string> $indexes
339352 */
340353 private static function arrayAttachIndexedValue (
341354 array $ result ,
342- array $ row ,
355+ array | object $ row ,
343356 array $ indexes ,
344357 bool $ includeEmpty ,
345358 ): array {
@@ -349,7 +362,7 @@ private static function arrayAttachIndexedValue(
349362 return $ result ;
350363 }
351364
352- $ value = dot_array_search ($ index , $ row );
365+ $ value = self :: dotSearch ($ index , $ row );
353366
354367 if (! is_scalar ($ value )) {
355368 $ value = '' ;
@@ -447,6 +460,96 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null):
447460 });
448461 }
449462
463+ /**
464+ * @param array<array-key, mixed>|object $data
465+ */
466+ private static function valueExists (array |object $ data , string $ key ): bool
467+ {
468+ if (is_array ($ data )) {
469+ return isset ($ data [$ key ]);
470+ }
471+
472+ $ array = self ::entityToArray ($ data );
473+
474+ if ($ array !== null ) {
475+ return isset ($ array [$ key ]);
476+ }
477+
478+ if ($ data instanceof ArrayAccess && $ data ->offsetExists ($ key )) {
479+ return true ;
480+ }
481+
482+ if (isset (get_object_vars ($ data )[$ key ])) {
483+ return true ;
484+ }
485+
486+ return isset ($ data ->{$ key });
487+ }
488+
489+ /**
490+ * @param array<array-key, mixed>|object $data
491+ */
492+ private static function value (array |object $ data , string $ key ): mixed
493+ {
494+ if (is_array ($ data )) {
495+ return $ data [$ key ];
496+ }
497+
498+ $ array = self ::entityToArray ($ data );
499+
500+ if ($ array !== null ) {
501+ return $ array [$ key ];
502+ }
503+
504+ if ($ data instanceof ArrayAccess && $ data ->offsetExists ($ key )) {
505+ return $ data ->offsetGet ($ key );
506+ }
507+
508+ $ properties = get_object_vars ($ data );
509+
510+ if (array_key_exists ($ key , $ properties )) {
511+ return $ properties [$ key ];
512+ }
513+
514+ return $ data ->{$ key };
515+ }
516+
517+ /**
518+ * @return array<array-key, mixed>|null
519+ */
520+ private static function entityToArray (object $ data ): ?array
521+ {
522+ if ($ data instanceof Entity) {
523+ return $ data ->toArray ();
524+ }
525+
526+ return null ;
527+ }
528+
529+ /**
530+ * Normalize an object to an array safe to iterate with foreach.
531+ *
532+ * Entities are converted via toArray() so internal properties like
533+ * `_options` or `_cast` are not exposed. Other Traversable objects are
534+ * materialized; plain objects fall back to their public properties.
535+ *
536+ * @return array<array-key, mixed>
537+ */
538+ private static function toIterable (object $ data ): array
539+ {
540+ $ array = self ::entityToArray ($ data );
541+
542+ if ($ array !== null ) {
543+ return $ array ;
544+ }
545+
546+ if ($ data instanceof Traversable) {
547+ return iterator_to_array ($ data , false );
548+ }
549+
550+ return get_object_vars ($ data );
551+ }
552+
450553 /**
451554 * Throws exception for invalid wildcard patterns.
452555 */
0 commit comments