diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index c723d6f84214..8213bd1528c7 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -146,9 +146,9 @@ private static function arraySearchDot(array $indexes, array|object $array) * * If wildcard `*` is used, all items for the key after it must have the key. * - * @param array $array + * @param array|object $array */ - public static function dotHas(string $index, array $array): bool + public static function dotHas(string $index, array|object $array): bool { self::ensureValidWildcardPattern($index); @@ -164,10 +164,10 @@ public static function dotHas(string $index, array $array): bool /** * Recursively check key existence by dot path, including wildcard support. * - * @param array $array - * @param list $indexes + * @param array|object $array + * @param list $indexes */ - private static function hasByDotPath(array $array, array $indexes): bool + private static function hasByDotPath(array|object $array, array $indexes): bool { if ($indexes === []) { return true; @@ -176,8 +176,10 @@ private static function hasByDotPath(array $array, array $indexes): bool $currentIndex = array_shift($indexes); if ($currentIndex === '*') { - foreach ($array as $item) { - if (! is_array($item) || ! self::hasByDotPath($item, $indexes)) { + $iterable = is_object($array) ? self::toIterable($array) : $array; + + foreach ($iterable as $item) { + if ((! is_array($item) && ! is_object($item)) || ! self::hasByDotPath($item, $indexes)) { return false; } } @@ -185,7 +187,7 @@ private static function hasByDotPath(array $array, array $indexes): bool return true; } - if (! array_key_exists($currentIndex, $array)) { + if (! self::valueExists($array, $currentIndex)) { return false; } @@ -193,11 +195,13 @@ private static function hasByDotPath(array $array, array $indexes): bool 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); } /** @@ -247,12 +251,12 @@ public static function dotUnset(array &$array, string $index): bool /** * Gets only the specified keys using dot syntax. * - * @param array $array - * @param list|string $indexes + * @param array|object $array + * @param list|string $indexes * * @return array */ - public static function dotOnly(array $array, array|string $indexes): array + public static function dotOnly(array|object $array, array|string $indexes): array { $indexes = is_string($indexes) ? [$indexes] : $indexes; $result = []; @@ -261,7 +265,7 @@ public static function dotOnly(array $array, array|string $indexes): array self::ensureValidWildcardPattern($index, true); if ($index === '*') { - $result = [...$result, ...$array]; + $result = [...$result, ...(is_object($array) ? self::toIterable($array) : $array)]; continue; } @@ -280,15 +284,15 @@ public static function dotOnly(array $array, array|string $indexes): array /** * Gets all keys except the specified ones using dot syntax. * - * @param array $array - * @param list|string $indexes + * @param array|object $array + * @param list|string $indexes * * @return array */ - public static function dotExcept(array $array, array|string $indexes): array + public static function dotExcept(array|object $array, array|string $indexes): array { $indexes = is_string($indexes) ? [$indexes] : $indexes; - $result = $array; + $result = self::toArrayView($array); foreach ($indexes as $index) { self::ensureValidWildcardPattern($index, true); @@ -466,20 +470,20 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null): private static function valueExists(array|object $data, string $key): bool { if (is_array($data)) { - return isset($data[$key]); + return array_key_exists($key, $data); } $array = self::entityToArray($data); if ($array !== null) { - return isset($array[$key]); + return array_key_exists($key, $array); } if ($data instanceof ArrayAccess && $data->offsetExists($key)) { return true; } - if (isset(get_object_vars($data)[$key])) { + if (array_key_exists($key, get_object_vars($data))) { return true; } @@ -550,6 +554,26 @@ private static function toIterable(object $data): array return get_object_vars($data); } + /** + * Normalize arrays or objects to an array view safe for dotExcept(). + * + * @param array|object $data + * + * @return array + */ + private static function toArrayView(array|object $data): array + { + $array = is_object($data) ? self::toIterable($data) : $data; + + foreach ($array as $key => $value) { + if (is_array($value) || is_object($value)) { + $array[$key] = self::toArrayView($value); + } + } + + return $array; + } + /** * Throws exception for invalid wildcard patterns. */ @@ -688,14 +712,15 @@ private static function clearByDotPath(array &$array, array $indexes): int } /** - * Projects matching paths from source array into result with preserved structure. + * Projects matching paths from source into result with preserved structure. * - * @param list $indexes - * @param list $prefix - * @param array $result + * @param array|object $source + * @param list $indexes + * @param list $prefix + * @param array $result */ private static function projectByDotPath( - mixed $source, + array|object $source, array $indexes, array &$result, array $prefix = [], @@ -709,21 +734,37 @@ private static function projectByDotPath( $currentIndex = array_shift($indexes); if ($currentIndex === '*') { - if (! is_array($source)) { - return; - } + $iterable = is_object($source) ? self::toIterable($source) : $source; + + foreach ($iterable as $key => $value) { + if (! is_array($value) && ! is_object($value)) { + if ($indexes === []) { + self::setByDotPath($result, [...$prefix, (string) $key], $value); + } + + continue; + } - foreach ($source as $key => $value) { self::projectByDotPath($value, $indexes, $result, [...$prefix, (string) $key]); } return; } - if (! is_array($source) || ! array_key_exists($currentIndex, $source)) { + if (! self::valueExists($source, $currentIndex)) { + return; + } + + $value = self::value($source, $currentIndex); + + if (! is_array($value) && ! is_object($value)) { + if ($indexes === []) { + self::setByDotPath($result, [...$prefix, $currentIndex], $value); + } + return; } - self::projectByDotPath($source[$currentIndex], $indexes, $result, [...$prefix, $currentIndex]); + self::projectByDotPath($value, $indexes, $result, [...$prefix, $currentIndex]); } } diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 66ff9c8b8d81..a752b3ff2e3b 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -34,9 +34,9 @@ function dot_array_search(string $index, array|object $array) /** * Checks if an array key exists using dot syntax. * - * @param array $array + * @param array|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); } @@ -70,12 +70,12 @@ function dot_array_unset(array &$array, string $index): bool /** * Gets only the specified keys using dot syntax. * - * @param array $array - * @param list|string $indexes + * @param array|object $array + * @param list|string $indexes * * @return array */ - function dot_array_only(array $array, array|string $indexes): array + function dot_array_only(array|object $array, array|string $indexes): array { return ArrayHelper::dotOnly($array, $indexes); } @@ -85,12 +85,12 @@ function dot_array_only(array $array, array|string $indexes): array /** * Gets all keys except the specified ones using dot syntax. * - * @param array $array - * @param list|string $indexes + * @param array|object $array + * @param list|string $indexes * * @return array */ - function dot_array_except(array $array, array|string $indexes): array + function dot_array_except(array|object $array, array|string $indexes): array { return ArrayHelper::dotExcept($array, $indexes); } diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index d331f24a655b..0ba282a78ab0 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -177,6 +177,50 @@ public function testDotArrayOnlySupportsEndingWildcard(): void $this->assertSame($expected, dot_array_only($data, 'user.*')); } + public function testDotArrayOnlyWithObjectValues(): void + { + $data = (object) [ + 'user' => (object) [ + 'id' => 123, + 'profile' => (object) [ + 'name' => 'john', + ], + ], + 'meta' => ['request_id' => 'abc'], + ]; + + $expected = [ + 'user' => [ + 'profile' => [ + 'name' => 'john', + ], + ], + ]; + + $this->assertSame($expected, dot_array_only($data, 'user.profile.name')); + } + + public function testDotArrayOnlyWildcardWithEntityRows(): void + { + $a = new SomeEntity(); + $a->foo = 1; + $a->bar = 2; + + $b = new SomeEntity(); + $b->foo = 3; + $b->bar = 4; + + $this->assertSame( + [ + 'rows' => [ + ['foo' => 1], + ['foo' => 3], + ], + ], + dot_array_only(['rows' => [$a, $b]], 'rows.*.foo'), + ); + } + public function testDotArrayExcept(): void { $data = [ @@ -215,6 +259,44 @@ public function testDotArrayExceptSupportsEndingWildcard(): void $this->assertSame($expected, dot_array_except($data, 'user.*')); } + public function testDotArrayExceptWithObjectValues(): void + { + $data = (object) [ + 'user' => (object) [ + 'id' => 123, + 'name' => 'john', + ], + 'meta' => (object) ['request_id' => 'abc'], + ]; + + $expected = [ + 'user' => [ + 'name' => 'john', + ], + 'meta' => ['request_id' => 'abc'], + ]; + + $this->assertSame($expected, dot_array_except($data, 'user.id')); + } + + public function testDotArrayExceptWildcardWithObjectValues(): void + { + $data = (object) [ + 'user' => (object) [ + 'id' => 123, + 'name' => 'john', + ], + 'meta' => ['request_id' => 'abc'], + ]; + + $expected = [ + 'user' => [], + 'meta' => ['request_id' => 'abc'], + ]; + + $this->assertSame($expected, dot_array_except($data, 'user.*')); + } + public function testArrayDotTooManyLevels(): void { $data = [ @@ -458,6 +540,82 @@ public function testArrayDotWildcardWithObjectValues(): void $this->assertSame(['John', 'Maria'], dot_array_search('users.*.name', $data)); } + public function testDotArrayHasWithObjectValues(): void + { + $data = [ + 'user' => (object) [ + 'profile' => (object) [ + 'name' => 'Jane', + ], + ], + ]; + + $this->assertTrue(dot_array_has('user.profile.name', $data)); + $this->assertFalse(dot_array_has('user.profile.email', $data)); + } + + public function testDotArrayHasWithMagicObjectValues(): void + { + $data = [ + 'user' => new class () { + /** + * @var array> + */ + private array $values = [ + 'profile' => [ + 'name' => 'Jane', + ], + ]; + + public function __isset(string $key): bool + { + return array_key_exists($key, $this->values); + } + + public function __get(string $key): mixed + { + return $this->values[$key]; + } + }, + ]; + + $this->assertTrue(dot_array_has('user.profile.name', $data)); + } + + public function testDotArrayHasWithArrayAccessValues(): void + { + $data = [ + 'user' => new ArrayObject([ + 'profile' => [ + 'name' => 'Jane', + ], + ]), + ]; + + $this->assertTrue(dot_array_has('user.profile.name', $data)); + } + + public function testDotArrayHasWithEntityValues(): void + { + $entity = new SomeEntity(); + $entity->foo = 'value'; + + $this->assertTrue(dot_array_has('user.foo', ['user' => $entity])); + $this->assertFalse(dot_array_has('user._options', ['user' => $entity])); + } + + public function testDotArrayHasWildcardWithEntityValues(): void + { + $a = new SomeEntity(); + $a->foo = 1; + + $b = new SomeEntity(); + $b->foo = 2; + + $this->assertTrue(dot_array_has('rows.*.foo', ['rows' => [$a, $b]])); + $this->assertFalse(dot_array_has('rows.*._cast', ['rows' => [$a, $b]])); + } + /** * @param int|string $key * @param array|string|null $expected diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 82f4fbf95d21..d5675a6df71b 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -267,8 +267,9 @@ Helpers and 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 ` dot-path read operations now support - object rows (including ``Entity``) in :php:func:`dot_array_search()` and - :php:func:`array_group_by()`. + object rows (including ``Entity``) in :php:func:`dot_array_search()`, + :php:func:`dot_array_has()`, :php:func:`dot_array_only()`, + :php:func:`dot_array_except()`, and :php:func:`array_group_by()`. HTTP ==== diff --git a/user_guide_src/source/helpers/array_helper.rst b/user_guide_src/source/helpers/array_helper.rst index fc48cd4c6eff..739abdd79395 100644 --- a/user_guide_src/source/helpers/array_helper.rst +++ b/user_guide_src/source/helpers/array_helper.rst @@ -58,10 +58,10 @@ The following functions are available: .. note:: Prior to v4.8.0, only arrays were supported. Support for objects was added in v4.8.0. -.. php:function:: dot_array_has(string $search, array $values): bool +.. php:function:: dot_array_has(string $search, array|object $values): bool :param string $search: The dot-notation string describing how to search the array - :param array $values: The array to check + :param array|object $values: The array or object to check :returns: ``true`` if the key exists, otherwise ``false`` :rtype: bool @@ -70,6 +70,8 @@ The following functions are available: Checks if an array key exists using dot syntax. This method supports wildcard ``*`` in the same way as ``dot_array_search()``. + .. note:: Prior to v4.8.0, only arrays were supported. Support for objects was added in v4.8.0. + .. literalinclude:: array_helper/015.php :lines: 2- @@ -105,9 +107,9 @@ The following functions are available: .. literalinclude:: array_helper/017.php :lines: 2- -.. php:function:: dot_array_only(array $array, array|string $indexes): array +.. php:function:: dot_array_only(array|object $array, array|string $indexes): array - :param array $array: The source array + :param array|object $array: The source array or object :param array|string $indexes: One key or a list of keys using dot notation :returns: Nested array containing only the requested keys :rtype: array @@ -119,12 +121,14 @@ The following functions are available: Wildcard ``*`` is supported. Unlike ``dot_array_set()`` and ``dot_array_unset()``, this method also allows wildcard at the end (for example ``user.*``). + .. note:: Prior to v4.8.0, only arrays were supported. Support for objects was added in v4.8.0. + .. literalinclude:: array_helper/018.php :lines: 2- -.. php:function:: dot_array_except(array $array, array|string $indexes): array +.. php:function:: dot_array_except(array|object $array, array|string $indexes): array - :param array $array: The source array + :param array|object $array: The source array or object :param array|string $indexes: One key or a list of keys using dot notation :returns: Nested array with the specified keys removed :rtype: array @@ -136,6 +140,8 @@ The following functions are available: Wildcard ``*`` is supported. Unlike ``dot_array_set()`` and ``dot_array_unset()``, this method also allows wildcard at the end (for example ``user.*``). + .. note:: Prior to v4.8.0, only arrays were supported. Support for objects was added in v4.8.0. + .. literalinclude:: array_helper/019.php :lines: 2-