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
175 changes: 150 additions & 25 deletions system/Helpers/Array/ArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

namespace CodeIgniter\Helpers\Array;

use ArrayAccess;
use CodeIgniter\Exceptions\InvalidArgumentException;
use ReflectionMethod;

/**
* @internal This is internal implementation for the framework.
Expand All @@ -34,11 +36,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 +81,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 +96,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 +105,7 @@ private static function arraySearchDot(array $indexes, array $array)
$answer = [];

foreach ($array as $value) {
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.

When $array is an object this iterates public properties via PHP default object iteration. For an Entity, that would give you internal properties like $_options, $_cast, etc. - not the data attributes.

You need to normalize the object to an array before iterating:

$iterable = is_object($array) ? self::toIterable($array) : $array;
foreach ($iterable as $value) {
private static function toIterable(object $data): array
{
    $array = self::objectToArray($data);

    if ($array !== null) {
        return $array;
    }

    if ($data instanceof Traversable) {
        return iterator_to_array($data);
    }

    return get_object_vars($data);
}

Same applies to hasByDotPath.

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

Expand All @@ -119,12 +125,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 +144,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 +162,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 +175,29 @@ private static function hasByDotPath(array $array, array $indexes): bool

if ($currentIndex === '*') {
foreach ($array as $item) {
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.

Normalize first:

$iterable = is_object($array) ? self::toIterable($array) : $array;

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 +343,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 +362,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 +460,118 @@ 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);
}

$array = self::objectToArray($data);

if ($array !== null) {
return array_key_exists($key, $array);
}

if ($data instanceof ArrayAccess && $data->offsetExists($key)) {
return true;
}

if (array_key_exists($key, get_object_vars($data))) {
return true;
}

return isset($data->{$key});
}

/**
* @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]);
}

$array = self::objectToArray($data);

if ($array !== null) {
return isset($array[$key]);
}

if ($data instanceof ArrayAccess && $data->offsetExists($key)) {
return true;
}

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

return isset($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];
}

$array = self::objectToArray($data);

if ($array !== null) {
return $array[$key];
}

if ($data instanceof ArrayAccess && $data->offsetExists($key)) {
return $data->offsetGet($key);
}

$properties = get_object_vars($data);

if (array_key_exists($key, $properties)) {
return $properties[$key];
}

return $data->{$key};
}

/**
* @return array<array-key, mixed>|null
*/
private static function objectToArray(object $data): ?array
{
if (method_exists($data, 'toRawArray')) {
$method = new ReflectionMethod($data, 'toRawArray');

if ($method->isPublic() && $method->getNumberOfRequiredParameters() === 0) {
$array = $data->toRawArray();

if (is_array($array)) {
return $array;
}
}
}

if (method_exists($data, 'toArray')) {
$method = new ReflectionMethod($data, 'toArray');

if ($method->isPublic() && $method->getNumberOfRequiredParameters() === 0) {
$array = $data->toArray();

if (is_array($array)) {
return $array;
}
}
}
Comment on lines +548 to +570
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.

Creating reflection instances on every property access is very expensive. We only want to support entities. This gives users the cast values they would expect.

Suggested change
if (method_exists($data, 'toRawArray')) {
$method = new ReflectionMethod($data, 'toRawArray');
if ($method->isPublic() && $method->getNumberOfRequiredParameters() === 0) {
$array = $data->toRawArray();
if (is_array($array)) {
return $array;
}
}
}
if (method_exists($data, 'toArray')) {
$method = new ReflectionMethod($data, 'toArray');
if ($method->isPublic() && $method->getNumberOfRequiredParameters() === 0) {
$array = $data->toArray();
if (is_array($array)) {
return $array;
}
}
}
if ($data instanceof Entity) {
return $data->toArray();
}


return null;
}

/**
* Throws exception for invalid wildcard patterns.
*/
Expand Down Expand Up @@ -606,7 +731,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 +742,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
Loading
Loading