Skip to content

Conversation

@ahmedsaed27
Copy link

This PR updates the Arr::array() method to use an empty array ([]) as the default return value when no key exists, instead of null.

Previously, the method signature allowed a null default (?array $default = null), but this created a mismatch with the strict : array return type. If a developer passed null as the default, the method would still throw an exception because null is not an array.

* Get an array item from an array using "dot" notation.
*/
public static function array(ArrayAccess|array $array, string|int|null $key, ?array $default = null): array
public static function array(ArrayAccess|array $array, string|int|null $key, ?array $default = []): array
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if people who rely on this would want something like throwOnNotFound or the like

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s a fair thought. My perspective is that since this method already has a strict : array return type, keeping it restrictive and throwing when the resolved value is not actually an array makes it safer and more predictable.
It guarantees that consumers of Arr::array() can confidently continue working with the result without additional type checks.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem with the return type per se. But throwing on non-array and throwing on non-existence doesn't need to conflict since there's the ItemNotFoundException that could be used for the latter case.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — I see the distinction. In my approach, I wanted missing keys to gracefully fall back to the provided default (or [] by default) to avoid forcing consumers into try/catch for common “not found” cases.
Using ItemNotFoundException for non-existence could work, but I feel it shifts the ergonomics — many developers just want a safe array back. The stricter exception path then remains focused on type mismatch, which keeps usage predictable.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't either or—that's why I suggested a throwOnNotFound flag

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about making a throwOnNotFound flag so we can support both approaches?
By default, the method could keep its current behavior — returning the provided default or an empty array — which keeps things ergonomic and avoids forcing try/catch for common cases.
But if throwOnNotFound is set to true, it could throw an ItemNotFoundException, giving developers who want stricter handling the option to opt in.

Copy link
Contributor

@shaedrich shaedrich Sep 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thoughts exactly 👍🏻

This would be especially helpful to lower the impact of the breaking change even if we would remove the flag or revert the behavior in 14.x

{
$value = Arr::get($array, $key, $default);

if ($throwOnNotFound && $value === $default && ! Arr::has($array, $key)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't !Arr::has($array, $key) work without $value === $default or am I missing something here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

! Arr::has($array, $key) on its own can’t distinguish between key is missing and key exists but its value equals the default.

For example:

$data = ['roles' => []];
// Key exists, value is [], same as default
Arr::array($data, 'roles', [], true);

If we only relied on ! Arr::has(), this would incorrectly throw. That’s why the check combines both:

$value === $default → catch when get() gave us the default

! Arr::has($array, $key) → confirm it’s truly missing, not just equal.

This way existing keys are respected, and only missing ones trigger ItemNotFoundException.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? 🤨

/**
* Determine if the given key exists in the provided array.
*
* @param \ArrayAccess|array $array
* @param string|int|float $key
* @return bool
*/
public static function exists($array, $key)
{
if ($array instanceof Enumerable) {
return $array->has($key);
}
if ($array instanceof ArrayAccess) {
return $array->offsetExists($key);
}
if (is_float($key)) {
$key = (string) $key;
}
return array_key_exists($key, $array);
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you’re right that Arr::exists() (and therefore Arr::has()) will correctly detect the presence of a key, even if its value is null or [].

The subtlety is that Arr::get() falls back to the default when the key is missing, so without the $value === $default guard, we can’t tell whether the returned value actually came from the array or from the default.

Example:

$data = ['roles' => []];

// Both `Arr::get($data, 'roles', [])` and `Arr::get([], 'roles', [])` return []

Here exists() will return true in the first case and false in the second — so combining ! Arr::has($array, $key) with $value === $default lets us safely tell the difference between key truly missing and key exists with default-like value.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? 🤨

/**
* Determine if the given key exists in the provided array.
*
* @param \ArrayAccess|array $array
* @param string|int|float $key
* @return bool
*/
public static function exists($array, $key)
{
if ($array instanceof Enumerable) {
return $array->has($key);
}
if ($array instanceof ArrayAccess) {
return $array->offsetExists($key);
}
if (is_float($key)) {
$key = (string) $key;
}
return array_key_exists($key, $array);
}

value === $default was mainly there as an extra safeguard — kind of a double-check to ensure we only throw when the key is truly missing.
In practice Arr::has() already guarantees that distinction, so it’s not strictly needed, but that was the reasoning behind having it in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you’re right that Arr::exists() (and therefore Arr::has()) will correctly detect the presence of a key, even if its value is null or [].

The subtlety is that Arr::get() falls back to the default when the key is missing, so without the $value === $default guard, we can’t tell whether the returned value actually came from the array or from the default.

Example:

$data = ['roles' => []];

// Both `Arr::get($data, 'roles', [])` and `Arr::get([], 'roles', [])` return []

Here exists() will return true in the first case and false in the second — so combining ! Arr::has($array, $key) with $value === $default lets us safely tell the difference between key truly missing and key exists with default-like value.

Is this written by AI?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no I was just trying to explain my perspective with an example. I thought it might help to catch any potential edge cases and thats why I initially left it in as a kind of double check.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, thanks for the explanation 👍🏻

Copy link
Contributor

@shaedrich shaedrich left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review request, but since I'm not a maintainer, I cannot approve or reject your changes. Taylor will do that.

@ahmedsaed27
Copy link
Author

ty you for your time. i will wait for Taylor review and acceptance.

@taylorotwell
Copy link
Member

We should fix the return type.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants