Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 72 additions & 25 deletions system/Helpers/Array/ArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ final class ArrayHelper
*
* @used-by dot_array_search()
*
* @param string $index The index as dot array syntax.
* @param string $index The index as dot array syntax.
* @param array<array-key, mixed>|object $array
*
* @return array|bool|int|object|string|null
* @return array<array-key, mixed>|bool|int|object|string|null
*/
public static function dotSearch(string $index, array $array)
public static function dotSearch(string $index, array|object $array)
{
return self::arraySearchDot(self::convertToArray($index), $array);
}
Expand Down Expand Up @@ -78,9 +79,12 @@ private static function convertToArray(string $index): array
*
* @used-by dotSearch()
*
* @return array|bool|float|int|object|string|null
* @param list<string> $indexes
* @param array<array-key, mixed>|object $array
*
* @return array<array-key, mixed>|bool|float|int|object|string|null
*/
private static function arraySearchDot(array $indexes, array $array)
private static function arraySearchDot(array $indexes, array|object $array)
{
// If index is empty, returns null.
if ($indexes === []) {
Expand All @@ -90,7 +94,7 @@ private static function arraySearchDot(array $indexes, array $array)
// Grab the current index
$currentIndex = array_shift($indexes);

if (! isset($array[$currentIndex]) && $currentIndex !== '*') {
if (! self::valueExists($array, $currentIndex) && $currentIndex !== '*') {
return null;
}

Expand All @@ -99,7 +103,7 @@ private static function arraySearchDot(array $indexes, array $array)
$answer = [];

foreach ($array as $value) {
if (! is_array($value)) {
if (! is_array($value) && ! is_object($value)) {
return null;
}

Expand All @@ -119,12 +123,14 @@ private static function arraySearchDot(array $indexes, array $array)
// If this is the last index, make sure to return it now,
// and not try to recurse through things.
if ($indexes === []) {
return $array[$currentIndex];
return self::value($array, $currentIndex);
}

$value = self::value($array, $currentIndex);

// Do we need to recursively search this value?
if (is_array($array[$currentIndex]) && $array[$currentIndex] !== []) {
return self::arraySearchDot($indexes, $array[$currentIndex]);
if ((is_array($value) && $value !== []) || is_object($value)) {
return self::arraySearchDot($indexes, $value);
}

// Otherwise, not found.
Expand All @@ -136,9 +142,9 @@ private static function arraySearchDot(array $indexes, array $array)
*
* If wildcard `*` is used, all items for the key after it must have the key.
*
* @param array<array-key, mixed> $array
* @param array<array-key, mixed>|object $array
*/
public static function dotHas(string $index, array $array): bool
public static function dotHas(string $index, array|object $array): bool
{
self::ensureValidWildcardPattern($index);

Expand All @@ -154,10 +160,10 @@ public static function dotHas(string $index, array $array): bool
/**
* Recursively check key existence by dot path, including wildcard support.
*
* @param array<array-key, mixed> $array
* @param list<string> $indexes
* @param array<array-key, mixed>|object $array
* @param list<string> $indexes
*/
private static function hasByDotPath(array $array, array $indexes): bool
private static function hasByDotPath(array|object $array, array $indexes): bool
{
if ($indexes === []) {
return true;
Expand All @@ -167,27 +173,29 @@ private static function hasByDotPath(array $array, array $indexes): bool

if ($currentIndex === '*') {
foreach ($array as $item) {
if (! is_array($item) || ! self::hasByDotPath($item, $indexes)) {
if ((! is_array($item) && ! is_object($item)) || ! self::hasByDotPath($item, $indexes)) {
return false;
}
}

return true;
}

if (! array_key_exists($currentIndex, $array)) {
if (! self::keyExists($array, $currentIndex)) {
return false;
}

if ($indexes === []) {
return true;
}

if (! is_array($array[$currentIndex])) {
$value = self::value($array, $currentIndex);

if (! is_array($value) && ! is_object($value)) {
return false;
}

return self::hasByDotPath($array[$currentIndex], $indexes);
return self::hasByDotPath($value, $indexes);
}

/**
Expand Down Expand Up @@ -333,13 +341,16 @@ public static function groupBy(array $array, array $indexes, bool $includeEmpty

/**
* Recursively attach $row to the $indexes path of values found by
* `dot_array_search()`.
* dot syntax.
*
* @used-by groupBy()
*
* @param array<array-key, mixed>|object $row
* @param list<string> $indexes
*/
private static function arrayAttachIndexedValue(
array $result,
array $row,
array|object $row,
array $indexes,
bool $includeEmpty,
): array {
Expand All @@ -349,7 +360,7 @@ private static function arrayAttachIndexedValue(
return $result;
}

$value = dot_array_search($index, $row);
$value = self::dotSearch($index, $row);

if (! is_scalar($value)) {
$value = '';
Expand Down Expand Up @@ -447,6 +458,42 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null):
});
}

/**
* @param array<array-key, mixed>|object $data
*/
private static function keyExists(array|object $data, string $key): bool
{
if (is_array($data)) {
return array_key_exists($key, $data);
}

return array_key_exists($key, get_object_vars($data));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't rely on get_object_vars() as it ignores magic properties, __get(), __isset(), toRawArray(), toArray(), and ArrayAccess-style objects.

}

/**
* @param array<array-key, mixed>|object $data
*/
private static function valueExists(array|object $data, string $key): bool
{
if (is_array($data)) {
return isset($data[$key]);
}

return isset(get_object_vars($data)[$key]);
}

/**
* @param array<array-key, mixed>|object $data
*/
private static function value(array|object $data, string $key): mixed
{
if (is_array($data)) {
return $data[$key];
}

return get_object_vars($data)[$key];
}

/**
* Throws exception for invalid wildcard patterns.
*/
Expand Down Expand Up @@ -606,7 +653,7 @@ private static function projectByDotPath(
$currentIndex = array_shift($indexes);

if ($currentIndex === '*') {
if (! is_array($source)) {
if (! is_array($source) && ! is_object($source)) {
return;
}

Expand All @@ -617,10 +664,10 @@ private static function projectByDotPath(
return;
}

if (! is_array($source) || ! array_key_exists($currentIndex, $source)) {
if ((! is_array($source) && ! is_object($source)) || ! self::keyExists($source, $currentIndex)) {
return;
}

self::projectByDotPath($source[$currentIndex], $indexes, $result, [...$prefix, $currentIndex]);
self::projectByDotPath(self::value($source, $currentIndex), $indexes, $result, [...$prefix, $currentIndex]);
}
}
10 changes: 6 additions & 4 deletions system/Helpers/array_helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
* Searches an array through dot syntax. Supports
* wildcard searches, like foo.*.bar
*
* @return array|bool|int|object|string|null
* @param array<array-key, mixed>|object $array
*
* @return array<array-key, mixed>|bool|int|object|string|null
*/
function dot_array_search(string $index, array $array)
function dot_array_search(string $index, array|object $array)
{
return ArrayHelper::dotSearch($index, $array);
}
Expand All @@ -32,9 +34,9 @@ function dot_array_search(string $index, array $array)
/**
* Checks if an array key exists using dot syntax.
*
* @param array<array-key, mixed> $array
* @param array<array-key, mixed>|object $array
*/
function dot_array_has(string $index, array $array): bool
function dot_array_has(string $index, array|object $array): bool
{
return ArrayHelper::dotHas($index, $array);
}
Expand Down
116 changes: 116 additions & 0 deletions tests/system/Helpers/ArrayHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CodeIgniter\Test\CIUnitTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use stdClass;
use ValueError;

/**
Expand Down Expand Up @@ -381,6 +382,59 @@ public function testArrayDotIgnoresLastWildcard(): void
$this->assertSame(['baz' => 23], dot_array_search('foo.bar.*', $data));
}

public function testArrayDotWithObjectValues(): void
{
$data = [
'user' => (object) [
'profile' => (object) [
'name' => 'Jane',
],
],
];

$this->assertSame('Jane', dot_array_search('user.profile.name', $data));
$this->assertTrue(dot_array_has('user.profile.name', $data));
$this->assertFalse(dot_array_has('user.profile.email', $data));
}

public function testArrayDotWildcardWithObjectValues(): void
{
$data = [
'users' => [
(object) ['name' => 'John'],
(object) ['name' => 'Maria'],
],
];

$this->assertSame(['John', 'Maria'], dot_array_search('users.*.name', $data));
$this->assertTrue(dot_array_has('users.*.name', $data));
}

public function testArrayDotOnlyWithObjectValues(): void
{
$data = [
'users' => [
(object) [
'id' => 1,
'name' => 'John',
],
(object) [
'id' => 2,
'name' => 'Maria',
],
],
];

$expected = [
'users' => [
['id' => 1],
['id' => 2],
],
];

$this->assertSame($expected, dot_array_only($data, 'users.*.id'));
}

/**
* @param int|string $key
* @param array|string|null $expected
Expand Down Expand Up @@ -1501,4 +1555,66 @@ public static function provideArrayGroupByExcludeEmpty(): iterable
],
];
}

/**
* @see https://github.com/codeigniter4/CodeIgniter4/issues/10225
*/
public function testArrayGroupByWithObjectRows(): void
{
$json = <<<'JSON'
[
{ "id": 1, "name": "Giraffe", "group": "Mammals" },
{ "id": 2, "name": "Zebra", "group": "Mammals" },
{ "id": 3, "name": "Crow", "group": "Birds" }
]
JSON;
$data = json_decode($json);

$this->assertIsArray($data);

$actual = array_group_by($data, ['group']);

$this->assertSame(
[
'Mammals' => [$data[0], $data[1]],
'Birds' => [$data[2]],
],
$actual,
);
$this->assertInstanceOf(stdClass::class, $actual['Mammals'][0]);
}

public function testArrayGroupByWithNestedObjectRows(): void
{
$data = [
(object) [
'id' => 1,
'hr' => (object) [
'department' => 'Engineering',
],
],
(object) [
'id' => 2,
'hr' => (object) [
'department' => 'Marketing',
],
],
(object) [
'id' => 3,
'hr' => (object) [
'department' => 'Engineering',
],
],
];

$actual = array_group_by($data, ['hr.department']);

$this->assertSame(
[
'Engineering' => [$data[0], $data[2]],
'Marketing' => [$data[1]],
],
$actual,
);
}
}
3 changes: 3 additions & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ Helpers and Functions
- :doc:`Array Helper </helpers/array_helper>` gained five new dot-path functions:
:php:func:`dot_array_has()`, :php:func:`dot_array_set()`, :php:func:`dot_array_unset()`,
:php:func:`dot_array_only()`, and :php:func:`dot_array_except()`.
- :doc:`Array Helper </helpers/array_helper>` dot-path read operations now support object properties
in :php:func:`dot_array_search()`, :php:func:`dot_array_has()`, :php:func:`dot_array_only()`,
and :php:func:`array_group_by()`.

HTTP
====
Expand Down
Loading
Loading