diff --git a/system/Helpers/Array/ArrayHelper.php b/system/Helpers/Array/ArrayHelper.php index c723d6f84214..86e0b4c28ebd 100644 --- a/system/Helpers/Array/ArrayHelper.php +++ b/system/Helpers/Array/ArrayHelper.php @@ -16,6 +16,7 @@ use ArrayAccess; use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\InvalidArgumentException; +use stdClass; use Traversable; /** @@ -97,17 +98,12 @@ private static function arraySearchDot(array $indexes, array|object $array) // Grab the current index $currentIndex = array_shift($indexes); - if (! self::valueExists($array, $currentIndex) && $currentIndex !== '*') { - return null; - } - // Handle Wildcard (*) if ($currentIndex === '*') { - $answer = []; - $iterable = is_object($array) ? self::toIterable($array) : $array; + $answer = []; - foreach ($iterable as $value) { - if (! is_array($value) && ! is_object($value)) { + foreach (self::entries($array) as $value) { + if (! self::isNavigable($value)) { return null; } @@ -124,14 +120,18 @@ private static function arraySearchDot(array $indexes, array|object $array) return null; } + [$found, $value] = self::resolve($array, $currentIndex); + + if (! $found) { + return null; + } + // If this is the last index, make sure to return it now, // and not try to recurse through things. if ($indexes === []) { - return self::value($array, $currentIndex); + return $value; } - $value = self::value($array, $currentIndex); - // Do we need to recursively search this value? if ((is_array($value) && $value !== []) || is_object($value)) { return self::arraySearchDot($indexes, $value); @@ -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,8 @@ 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)) { + foreach (self::entries($array) as $item) { + if (! self::isNavigable($item) || ! self::hasByDotPath($item, $indexes)) { return false; } } @@ -185,7 +185,9 @@ private static function hasByDotPath(array $array, array $indexes): bool return true; } - if (! array_key_exists($currentIndex, $array)) { + [$found, $value] = self::resolve($array, $currentIndex); + + if (! $found) { return false; } @@ -193,11 +195,11 @@ private static function hasByDotPath(array $array, array $indexes): bool return true; } - if (! is_array($array[$currentIndex])) { + if (! self::isNavigable($value)) { return false; } - return self::hasByDotPath($array[$currentIndex], $indexes); + return self::hasByDotPath($value, $indexes); } /** @@ -247,12 +249,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 +263,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 +282,18 @@ 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; + + // Open only the root into an array view; nested values (including + // objects) are preserved until a path actually descends into them. + $result = self::entries($array); foreach ($indexes as $index) { self::ensureValidWildcardPattern($index, true); @@ -299,17 +304,18 @@ public static function dotExcept(array $array, array|string $indexes): array continue; } + $segments = self::convertToArray($index); + if ($segments === []) { + continue; + } + if (str_ends_with($index, '*')) { - $segments = self::convertToArray($index); - self::clearByDotPath($result, $segments); + self::excludeChildrenByDotPath($result, $segments); continue; } - $segments = self::convertToArray($index); - if ($segments !== []) { - self::unsetByDotPath($result, $segments); - } + self::excludeByDotPath($result, $segments); } return $result; @@ -461,57 +467,70 @@ public static function sortValuesByNatural(array &$array, $sortByIndex = null): } /** - * @param array|object $data + * Resolve a key against an array or object node, walking the access chain + * (Entity, ArrayAccess, public properties, magic `__isset`/`__get`) once. + * + * @param array|object $node + * + * @return array{bool, mixed} The pair [found, value]. */ - private static function valueExists(array|object $data, string $key): bool + private static function resolve(array|object $node, string $key): array { - if (is_array($data)) { - return isset($data[$key]); + if (is_array($node)) { + return array_key_exists($key, $node) ? [true, $node[$key]] : [false, null]; } - $array = self::entityToArray($data); + $array = self::entityToArray($node); if ($array !== null) { - return isset($array[$key]); + return array_key_exists($key, $array) ? [true, $array[$key]] : [false, null]; } - if ($data instanceof ArrayAccess && $data->offsetExists($key)) { - return true; + if ($node instanceof ArrayAccess && $node->offsetExists($key)) { + return [true, $node->offsetGet($key)]; } - if (isset(get_object_vars($data)[$key])) { - return true; + $properties = get_object_vars($node); + + if (array_key_exists($key, $properties)) { + return [true, $properties[$key]]; } - return isset($data->{$key}); + return isset($node->{$key}) ? [true, $node->{$key}] : [false, null]; } /** - * @param array|object $data + * Whether keys can be resolved from this value, i.e. it is an array or an + * object that exposes a key surface: an expandable container, an + * `ArrayAccess`, or one relying on magic `__get`. Pure value-objects + * (e.g. `DateTimeImmutable`) are not navigable. + * + * Direct key lookup can support more object types than wildcard traversal: + * `ArrayAccess` and magic-only objects can resolve `user.id`, but cannot be + * enumerated for `user.*` unless they are also expandable. */ - private static function value(array|object $data, string $key): mixed + private static function isNavigable(mixed $value): bool { - if (is_array($data)) { - return $data[$key]; - } - - $array = self::entityToArray($data); - - if ($array !== null) { - return $array[$key]; - } - - if ($data instanceof ArrayAccess && $data->offsetExists($key)) { - return $data->offsetGet($key); + if (is_array($value)) { + return true; } - $properties = get_object_vars($data); - - if (array_key_exists($key, $properties)) { - return $properties[$key]; - } + return is_object($value) + && (self::isExpandable($value) + || $value instanceof ArrayAccess + || method_exists($value, '__get')); + } - return $data->{$key}; + /** + * Entries of an array or object node for wildcard traversal. + * + * @param array|object $node + * + * @return array + */ + private static function entries(array|object $node): array + { + return is_object($node) ? self::toIterable($node) : $node; } /** @@ -531,7 +550,8 @@ private static function entityToArray(object $data): ?array * * Entities are converted via toArray() so internal properties like * `_options` or `_cast` are not exposed. Other Traversable objects are - * materialized; plain objects fall back to their public properties. + * converted to an array with their keys preserved; plain objects fall back + * to their public properties. * * @return array */ @@ -544,12 +564,52 @@ private static function toIterable(object $data): array } if ($data instanceof Traversable) { - return iterator_to_array($data, false); + return iterator_to_array($data); } return get_object_vars($data); } + /** + * Whether an object should be expanded into an array when building output. + * + * Only enumerable containers are expanded: entities, `stdClass`, other + * `Traversable` objects, and plain objects exposing public properties. + * Opaque objects with no enumerable key surface (value-objects such as + * `DateTimeImmutable`, magic-only or pure `ArrayAccess` objects) are + * preserved as-is, since they cannot be faithfully rebuilt as an array. + */ + private static function isExpandable(object $value): bool + { + return $value instanceof Entity + || $value instanceof stdClass + || $value instanceof Traversable + || get_object_vars($value) !== []; + } + + /** + * Ensure a value can be descended into for a partial exclusion/projection. + * + * Arrays pass through; expandable objects are converted to an array view + * in place (this is the only point where output structure is fabricated). + * Anything else (scalars, value-objects, magic-only or pure `ArrayAccess` + * objects) is left untouched and reported as non-descendable. + */ + private static function expandForDescent(mixed &$value): bool + { + if (is_array($value)) { + return true; + } + + if (is_object($value) && self::isExpandable($value)) { + $value = self::entries($value); + + return true; + } + + return false; + } + /** * Throws exception for invalid wildcard patterns. */ @@ -688,19 +748,106 @@ private static function clearByDotPath(array &$array, array $indexes): int } /** - * Projects matching paths from source array into result with preserved structure. + * Removes a value by dot path for dotExcept(). Objects are expanded to an + * array view only when the path descends into them, so untouched branches + * keep their original values (including objects). * + * @param array $array * @param list $indexes - * @param list $prefix - * @param array $result + */ + private static function excludeByDotPath(array &$array, array $indexes): int + { + if ($indexes === []) { + return 0; + } + + $currentIndex = array_shift($indexes); + + if ($currentIndex === '*') { + $removed = 0; + + foreach ($array as &$item) { + if (self::expandForDescent($item)) { + $removed += self::excludeByDotPath($item, $indexes); + } + } + unset($item); + + return $removed; + } + + if ($indexes === []) { + if (! array_key_exists($currentIndex, $array)) { + return 0; + } + + unset($array[$currentIndex]); + + return 1; + } + + if (! array_key_exists($currentIndex, $array) || ! self::expandForDescent($array[$currentIndex])) { + return 0; + } + + return self::excludeByDotPath($array[$currentIndex], $indexes); + } + + /** + * Clears all children under the specified path for dotExcept(), expanding + * objects to an array view only along the descended path. + * + * @param array $array + * @param list $indexes + */ + private static function excludeChildrenByDotPath(array &$array, array $indexes): int + { + if ($indexes === []) { + $count = count($array); + $array = []; + + return $count; + } + + $currentIndex = array_shift($indexes); + + if ($currentIndex === '*') { + $cleared = 0; + + foreach ($array as &$item) { + if (self::expandForDescent($item)) { + $cleared += self::excludeChildrenByDotPath($item, $indexes); + } + } + unset($item); + + return $cleared; + } + + if (! array_key_exists($currentIndex, $array) || ! self::expandForDescent($array[$currentIndex])) { + return 0; + } + + return self::excludeChildrenByDotPath($array[$currentIndex], $indexes); + } + + /** + * Projects matching paths from source into result with preserved structure. + * + * @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 = [], ): void { if ($indexes === []) { + // The whole node was selected: preserve it as-is. Output structure + // is only fabricated for the projection skeleton above this leaf. self::setByDotPath($result, $prefix, $source); return; @@ -709,21 +856,35 @@ private static function projectByDotPath( $currentIndex = array_shift($indexes); if ($currentIndex === '*') { - if (! is_array($source)) { - return; - } + foreach (self::entries($source) as $key => $value) { + if (! self::isNavigable($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)) { + [$found, $value] = self::resolve($source, $currentIndex); + + if (! $found) { + return; + } + + if (! self::isNavigable($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..826a376aa081 100644 --- a/tests/system/Helpers/ArrayHelperTest.php +++ b/tests/system/Helpers/ArrayHelperTest.php @@ -16,6 +16,7 @@ use ArrayObject; use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; +use DateTimeImmutable; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use stdClass; @@ -177,6 +178,76 @@ 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 testDotArrayOnlyPreservesWholeSelectedObject(): void + { + $user = (object) ['id' => 1, 'name' => 'Ada']; + + // Selecting the object as a whole returns it untouched. + $this->assertSame(['user' => $user], dot_array_only(['user' => $user], 'user')); + } + + public function testDotArrayOnlyProjectsPartialObjectAsArray(): void + { + $created = new DateTimeImmutable(); + + $data = [ + 'user' => (object) [ + 'id' => 123, + 'created' => $created, + ], + ]; + + // A partial projection must fabricate array structure for "user"... + $this->assertSame(['user' => ['id' => 123]], dot_array_only($data, 'user.id')); + + // ...while the value-object leaf is preserved as-is. + $this->assertSame(['user' => ['created' => $created]], dot_array_only($data, 'user.created')); + } + public function testDotArrayExcept(): void { $data = [ @@ -215,6 +286,94 @@ public function testDotArrayExceptSupportsEndingWildcard(): void $this->assertSame($expected, dot_array_except($data, 'user.*')); } + public function testDotArrayExceptWithObjectValues(): void + { + $meta = (object) ['request_id' => 'abc']; + $data = (object) [ + 'user' => (object) [ + 'id' => 123, + 'name' => 'john', + ], + 'meta' => $meta, + ]; + + $result = dot_array_except($data, 'user.id'); + + // "user" is partially excluded, so it is rebuilt as an array... + $this->assertSame(['name' => 'john'], $result['user']); + // ...but the untouched "meta" object is preserved as-is. + $this->assertSame($meta, $result['meta']); + } + + 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 testDotArrayExceptPreservesUntouchedObject(): void + { + $user = (object) ['id' => 1, 'name' => 'Ada']; + + // The path does not touch "user", so the object is returned untouched. + $this->assertSame(['user' => $user], dot_array_except(['user' => $user], 'other')); + } + + public function testDotArrayExceptPreservesValueObjects(): void + { + $created = new DateTimeImmutable(); + + $data = [ + 'user' => ['id' => 1], + 'created' => $created, + ]; + + $result = dot_array_except($data, 'user.id'); + + $this->assertSame([], $result['user']); + // Untouched value-objects must survive instead of collapsing to []. + $this->assertSame($created, $result['created']); + } + + public function testDotArrayExceptPreservesTraversableKeys(): void + { + $data = new ArrayObject([ + 'user' => ['id' => 1, 'name' => 'Ada'], + 'meta' => 'm', + ]); + + $expected = [ + 'user' => ['name' => 'Ada'], + 'meta' => 'm', + ]; + + $this->assertSame($expected, dot_array_except($data, 'user.id')); + } + + public function testDotArrayOnlyAndExceptAgreeOnPublicPropertyObjects(): void + { + $user = new class () { + public int $id = 1; + public string $name = 'Ada'; + }; + + // Both helpers treat a public-property object as a container. + $this->assertSame(['user' => ['id' => 1]], dot_array_only(['user' => $user], 'user.id')); + $this->assertSame(['user' => ['name' => 'Ada']], dot_array_except(['user' => $user], 'user.id')); + } + public function testArrayDotTooManyLevels(): void { $data = [ @@ -458,6 +617,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 f1c369184f3b..bbe04725624c 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -271,8 +271,12 @@ 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()`. + :php:func:`dot_array_only()` and :php:func:`dot_array_except()` still return arrays. If the source itself is an object, + it is read as an array-like value. Object values inside the source are kept unchanged when selected as a whole or left untouched. + Partial object paths are returned as arrays. HTTP ==== diff --git a/user_guide_src/source/helpers/array_helper.rst b/user_guide_src/source/helpers/array_helper.rst index fc48cd4c6eff..8d859de077a1 100644 --- a/user_guide_src/source/helpers/array_helper.rst +++ b/user_guide_src/source/helpers/array_helper.rst @@ -22,6 +22,17 @@ Available Functions The following functions are available: +.. note:: Since v4.8.0, the dot-path helpers can read values from arrays or + objects, including ``Entity`` objects. This applies to + :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()`. + + :php:func:`dot_array_set()` and :php:func:`dot_array_unset()` still modify + arrays only. :php:func:`dot_array_only()` and + :php:func:`dot_array_except()` always return arrays. See their descriptions + for how object values are handled. + .. php:function:: dot_array_search(string $search, array|object $values) :param string $search: The dot-notation string describing how to search the array @@ -56,12 +67,10 @@ 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``. -.. 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 @@ -105,9 +114,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 +128,19 @@ 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.*``). + The result is always an array. If a selected value is an object, that object + is kept as the value. If you select a value inside an object, the returned + path is built with arrays: + + .. literalinclude:: array_helper/020.php + :lines: 2- + .. 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 +152,13 @@ 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.*``). + The result is always an array. Object values that are not changed are kept + as they are. If a key is removed from inside an object, that part of the + result is returned as an array: + + .. literalinclude:: array_helper/021.php + :lines: 2- + .. literalinclude:: array_helper/019.php :lines: 2- @@ -146,7 +169,8 @@ The following functions are available: :returns: The value found within the array, or null :rtype: mixed - Returns the value of an element with a key value in an array of uncertain depth + Returns the value of an element with a key value in an array of uncertain depth. + Only nested arrays are searched; object values are not traversed. .. php:function:: array_sort_by_multiple_keys(array &$array, array $sortColumns) @@ -189,7 +213,8 @@ The following functions are available: :returns: The flattened array This function flattens a multidimensional array to a single key-value array by using dots - as separators for the keys. + as separators for the keys. The source may be any ``iterable``. Only nested arrays are + flattened; object values are kept as-is, as leaf values. .. literalinclude:: array_helper/009.php :lines: 2- @@ -218,8 +243,6 @@ The following functions are available: 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. - .. note:: Prior to v4.8.0, only arrays were supported. Support for objects was added in v4.8.0. - The example shows some data (i.e. loaded from an API) with nested arrays. .. literalinclude:: array_helper/012.php diff --git a/user_guide_src/source/helpers/array_helper/020.php b/user_guide_src/source/helpers/array_helper/020.php new file mode 100644 index 000000000000..49e332fcf798 --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/020.php @@ -0,0 +1,11 @@ + 123, 'name' => 'John']; + +// Selecting the object itself keeps the object as the value. +$whole = dot_array_only(['user' => $user], 'user'); +// ['user' => $user] + +// Selecting a value inside the object builds the path with arrays. +$partial = dot_array_only(['user' => $user], 'user.id'); +// ['user' => ['id' => 123]] diff --git a/user_guide_src/source/helpers/array_helper/021.php b/user_guide_src/source/helpers/array_helper/021.php new file mode 100644 index 000000000000..ff73943962cf --- /dev/null +++ b/user_guide_src/source/helpers/array_helper/021.php @@ -0,0 +1,11 @@ + 123, 'name' => 'John']; + +// An untouched object is kept as it is. +$untouched = dot_array_except(['user' => $user], 'meta.id'); +// ['user' => $user] + +// Removing a key from inside an object returns that part as an array. +$partial = dot_array_except(['user' => $user], 'user.id'); +// ['user' => ['name' => 'John']]