diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index 1c33fbaef0d1..4a61a8591ec4 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -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|object $array * - * @return array|bool|int|object|string|null + * @return array|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); } @@ -78,9 +79,12 @@ private static function convertToArray(string $index): array * * @used-by dotSearch() * - * @return array|bool|float|int|object|string|null + * @param list $indexes + * @param array|object $array + * + * @return array|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 === []) { @@ -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; } @@ -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; } @@ -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. @@ -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 + * @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); @@ -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 - * @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; @@ -167,7 +173,7 @@ 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; } } @@ -175,7 +181,7 @@ private static function hasByDotPath(array $array, array $indexes): bool return true; } - if (! array_key_exists($currentIndex, $array)) { + if (! self::keyExists($array, $currentIndex)) { return false; } @@ -183,11 +189,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); } /** @@ -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|object $row + * @param list $indexes */ private static function arrayAttachIndexedValue( array $result, - array $row, + array|object $row, array $indexes, bool $includeEmpty, ): array { @@ -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 = ''; @@ -447,6 +458,42 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null): }); } + /** + * @param array|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)); + } + + /** + * @param array|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|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. */ @@ -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; } @@ -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]); } } diff --git a/system/Helpers/array_helper.php b/system/Helpers/array_helper.php index 4f24674397e4..5d91d32a8013 100644 --- a/system/Helpers/array_helper.php +++ b/system/Helpers/array_helper.php @@ -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|object $array + * + * @return array|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); } @@ -32,9 +34,9 @@ function dot_array_search(string $index, array $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); } diff --git a/tests/system/Helpers/ArrayHelperTest.php b/tests/system/Helpers/ArrayHelperTest.php index 8cbf62235835..7abf64a4f2fa 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -17,6 +17,7 @@ use CodeIgniter\Test\CIUnitTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; +use stdClass; use ValueError; /** @@ -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 @@ -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, + ); + } } diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 7670598cde1f..33322b57509f 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -255,6 +255,9 @@ Helpers and Functions - :doc:`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 ` 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 ==== diff --git a/user_guide_src/source/helpers/array_helper.rst b/user_guide_src/source/helpers/array_helper.rst index fd13e0ae6f6f..4831d5357b7b 100644 --- a/user_guide_src/source/helpers/array_helper.rst +++ b/user_guide_src/source/helpers/array_helper.rst @@ -22,15 +22,15 @@ Available Functions The following functions are available: -.. php:function:: dot_array_search(string $search, array $values) +.. php:function:: dot_array_search(string $search, array|object $values) :param string $search: The dot-notation string describing how to search the array - :param array $values: The array to search + :param array|object $values: The array or object to search :returns: The value found within the array, or null :rtype: mixed - This method allows you to use dot-notation to search through an array for a specific-key, - and allows the use of a the ``*`` wildcard. Given the following array: + This method allows you to use dot-notation to search through arrays and objects for a specific + key or property, and allows the use of the ``*`` wildcard. Given the following array: .. literalinclude:: array_helper/002.php :lines: 2- @@ -56,16 +56,16 @@ The following functions are available: .. note:: Prior to v4.2.0, ``dot_array_search('foo.bar.baz', ['foo' => ['bar' => 23]])`` returned ``23`` due to a bug. v4.2.0 and later returns ``null``. -.. 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 .. versionadded:: 4.8.0 - Checks if an array key exists using dot syntax. + Checks if an array key or object property exists using dot syntax. This method supports wildcard ``*`` in the same way as ``dot_array_search()``. .. literalinclude:: array_helper/015.php @@ -113,6 +113,7 @@ The following functions are available: .. versionadded:: 4.8.0 Gets only the specified keys using dot syntax while preserving nested structure. + Nested object properties can be selected in the same way as array keys. Wildcard ``*`` is supported. Unlike ``dot_array_set()`` and ``dot_array_unset()``, this method also allows wildcard at the end (for example ``user.*``). @@ -214,6 +215,7 @@ The following functions are available: This function allows you to group data rows together by index values. The depth of returned array equals the number of indexes passed as parameter. + Data rows may be arrays or objects, and dot syntax can read nested array keys or object properties. The example shows some data (i.e. loaded from an API) with nested arrays. diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon index d69a2551e1e2..5429043d5712 100644 --- a/utils/phpstan-baseline/missingType.iterableValue.neon +++ b/utils/phpstan-baseline/missingType.iterableValue.neon @@ -2942,51 +2942,16 @@ parameters: count: 1 path: ../../system/HTTP/URI.php - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) has parameter \$result with no value type specified in iterable type array\.$#' count: 1 path: ../../system/Helpers/Array/ArrayHelper.php - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) has parameter \$row with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arrayAttachIndexedValue\(\) return type has no value type specified in iterable type array\.$#' count: 1 path: ../../system/Helpers/Array/ArrayHelper.php - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arraySearchDot\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arraySearchDot\(\) has parameter \$indexes with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:arraySearchDot\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:dotSearch\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:dotSearch\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/Array/ArrayHelper.php - - message: '#^Method CodeIgniter\\Helpers\\Array\\ArrayHelper\:\:groupBy\(\) has parameter \$array with no value type specified in iterable type array\.$#' count: 1 @@ -3067,16 +3032,6 @@ parameters: count: 1 path: ../../system/Helpers/array_helper.php - - - message: '#^Function dot_array_search\(\) has parameter \$array with no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - - - message: '#^Function dot_array_search\(\) return type has no value type specified in iterable type array\.$#' - count: 1 - path: ../../system/Helpers/array_helper.php - - message: '#^Function directory_map\(\) return type has no value type specified in iterable type array\.$#' count: 1