Skip to content

Commit c2dcbbd

Browse files
authored
feat: support object values in array dot helpers (#10226)
1 parent 2ef1571 commit c2dcbbd

6 files changed

Lines changed: 299 additions & 66 deletions

File tree

system/Helpers/Array/ArrayHelper.php

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313

1414
namespace CodeIgniter\Helpers\Array;
1515

16+
use ArrayAccess;
17+
use CodeIgniter\Entity\Entity;
1618
use 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
*/

system/Helpers/array_helper.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@
2020
* Searches an array through dot syntax. Supports
2121
* wildcard searches, like foo.*.bar
2222
*
23-
* @return array|bool|int|object|string|null
23+
* @param array<array-key, mixed>|object $array
24+
*
25+
* @return array<array-key, mixed>|bool|int|object|string|null
2426
*/
25-
function dot_array_search(string $index, array $array)
27+
function dot_array_search(string $index, array|object $array)
2628
{
2729
return ArrayHelper::dotSearch($index, $array);
2830
}

0 commit comments

Comments
 (0)