Skip to content
Merged
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
58 changes: 52 additions & 6 deletions src/Arr.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -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];
Expand Down
86 changes: 86 additions & 0 deletions tests/ArrTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
Loading