diff --git a/src/Arr.php b/src/Arr.php index a49b541..6c8a0b7 100644 --- a/src/Arr.php +++ b/src/Arr.php @@ -30,13 +30,35 @@ public static function get(array|ArrayAccess $array, string|int|null $key, mixed return $default; } - foreach (explode('.', (string) $key) as $segment) { + $keyString = (string) $key; + + // Handle simple keys (no dots) directly + if (!str_contains($keyString, '.')) { + if (is_array($array)) { + return array_key_exists($keyString, $array) ? $array[$keyString] : $default; + } + + // For ArrayAccess, use offsetExists to match array_key_exists behavior + // This correctly handles null values (unlike isset which returns false for null) + return $array->offsetExists($keyString) ? $array[$keyString] : $default; + } + + // Handle dot notation + foreach (explode('.', $keyString) as $segment) { if (!static::accessible($array)) { return $default; } - if (!array_key_exists($segment, is_array($array) ? $array : iterator_to_array($array))) { - return $default; + if (is_array($array)) { + if (!array_key_exists($segment, $array)) { + return $default; + } + } else { + // For ArrayAccess, use offsetExists instead of iterator_to_array + // This is much more efficient and avoids converting the entire object to an array + if (!$array->offsetExists($segment)) { + return $default; + } } $array = $array[$segment]; @@ -58,13 +80,37 @@ public static function get(array|ArrayAccess $array, string|int|null $key, mixed */ public static function exists(array|ArrayAccess $array, string|int $key): bool { - foreach (explode('.', (string) $key) as $segment) { + if (!static::accessible($array)) { + return false; + } + + $keyString = (string) $key; + + // Handle simple keys (no dots) directly + if (!str_contains($keyString, '.')) { + if (is_array($array)) { + return array_key_exists($keyString, $array); + } + + // For ArrayAccess, use offsetExists + return $array->offsetExists($keyString); + } + + // Handle dot notation + foreach (explode('.', $keyString) as $segment) { if (!static::accessible($array)) { return false; } - if (!array_key_exists($segment, is_array($array) ? $array : iterator_to_array($array))) { - return false; + if (is_array($array)) { + if (!array_key_exists($segment, $array)) { + return false; + } + } else { + // For ArrayAccess, use offsetExists instead of iterator_to_array + if (!$array->offsetExists($segment)) { + return false; + } } $array = $array[$segment]; diff --git a/tests/ArrTest.php b/tests/ArrTest.php index 6a85d9f..7919f70 100644 --- a/tests/ArrTest.php +++ b/tests/ArrTest.php @@ -198,3 +198,89 @@ expect(Arr::get($array, 'mixed.array'))->toBe(['a', 'b', 'c']); expect(Arr::get($array, 'mixed.scalar'))->toBe('value'); }); + +// Performance optimization tests +it('handles simple keys without dots efficiently', function () { + $array = [ + 'simple_key' => 'simple_value', + 'another' => 'test', + ]; + + // Test simple key retrieval (optimized path) + expect(Arr::get($array, 'simple_key'))->toBe('simple_value'); + expect(Arr::get($array, 'nonexistent', 'default'))->toBe('default'); + expect(Arr::exists($array, 'simple_key'))->toBeTrue(); + expect(Arr::exists($array, 'nonexistent'))->toBeFalse(); +}); + +it('handles integer keys without dots', function () { + $array = [ + 0 => 'zero', + 1 => 'one', + 123 => 'one-two-three', + ]; + + // Test integer key retrieval + expect(Arr::get($array, 0))->toBe('zero'); + expect(Arr::get($array, 123))->toBe('one-two-three'); + expect(Arr::get($array, 999, 'default'))->toBe('default'); + expect(Arr::exists($array, 0))->toBeTrue(); + expect(Arr::exists($array, 123))->toBeTrue(); + expect(Arr::exists($array, 999))->toBeFalse(); +}); + +it('handles ArrayAccess with null values correctly', function () { + $arrayObject = new ArrayObject([ + 'null_value' => null, + 'false_value' => false, + 'zero_value' => 0, + 'empty_string' => '', + ]); + + // Verify null values are retrievable (not treated as missing) + expect(Arr::get($arrayObject, 'null_value'))->toBeNull(); + expect(Arr::get($arrayObject, 'false_value'))->toBeFalse(); + expect(Arr::get($arrayObject, 'zero_value'))->toBe(0); + expect(Arr::get($arrayObject, 'empty_string'))->toBe(''); + + // Verify exists returns true for these values + expect(Arr::exists($arrayObject, 'null_value'))->toBeTrue(); + expect(Arr::exists($arrayObject, 'false_value'))->toBeTrue(); + expect(Arr::exists($arrayObject, 'zero_value'))->toBeTrue(); + expect(Arr::exists($arrayObject, 'empty_string'))->toBeTrue(); +}); + +it('handles nested ArrayAccess without performance issues', function () { + // Create nested ArrayObject structure + $deepObject = new ArrayObject([ + 'level1' => new ArrayObject([ + 'level2' => new ArrayObject([ + 'level3' => 'deep_value', + ]), + ]), + ]); + + // This should use offsetExists instead of iterator_to_array + expect(Arr::get($deepObject, 'level1.level2.level3'))->toBe('deep_value'); + expect(Arr::exists($deepObject, 'level1.level2.level3'))->toBeTrue(); + expect(Arr::get($deepObject, 'level1.level2.nonexistent', 'default'))->toBe('default'); +}); + +it('handles mixed array and ArrayAccess nesting', function () { + $mixed = new ArrayObject([ + 'array_inside' => [ + 'nested' => 'value1', + ], + ]); + + $array = [ + 'object_inside' => new ArrayObject([ + 'nested' => 'value2', + ]), + ]; + + expect(Arr::get($mixed, 'array_inside.nested'))->toBe('value1'); + expect(Arr::get($array, 'object_inside.nested'))->toBe('value2'); + expect(Arr::exists($mixed, 'array_inside.nested'))->toBeTrue(); + expect(Arr::exists($array, 'object_inside.nested'))->toBeTrue(); +});