Laravel 9.x Collection ->except() bug with dot notation
I believe i have found a bug with the ->except() function on Collections, or i am misunderstanding its behavior
given the following tinker example:
> $collection = collect([ 'a' => [ 'b' => 1, 'c' => 2 ], 'e' => [ 'f' => 3, 'g' => 4 ] ]);
= Illuminate\Support\Collection {#8266
all: [
"a" => [
"b" => 1,
"c" => 2,
],
"e" => [
"f" => 3,
"g" => 4,
],
],
}
> $collection->except(['missing','c']);
= Illuminate\Support\Collection {#8257
all: [
"a" => [
"b" => 1,
"c" => 2,
],
"e" => [
"f" => 3,
"g" => 4,
],
],
}
> $collection->except(['c', 'a.missing']);
= Illuminate\Support\Collection {#8256
all: [
"a" => [
"b" => 1,
"c" => 2,
],
"e" => [
"f" => 3,
"g" => 4,
],
],
}
> $collection->except(['a.missing','c']);
= Illuminate\Support\Collection {#8251
all: [
"a" => [
"b" => 1,
],
"e" => [
"f" => 3,
"g" => 4,
],
],
}
all of these behave as expected (NOOP), except for the final invocation. the key a.c is being removed, but i did not pass a.c, i only passed c. Recreation of this error is dependent on the order in which keys are passed, and will occur if a dot notation key immediately precedes a top level key (which has no dots)
i believe this is an error with the way the underlying implementation is written:
public static function forget(&$array, $keys)
{
$original = &$array;
$keys = (array) $keys;
if (count($keys) === 0) {
return;
}
foreach ($keys as $key) {
// if the exact key exists in the top-level, remove it
if (static::exists($array, $key)) {
unset($array[$key]);
continue;
}
$parts = explode('.', $key);
// clean up before each pass
$array = &$original;
while (count($parts) > 1) {
$part = array_shift($parts);
if (isset($array[$part]) && static::accessible($array[$part])) {
$array = &$array[$part];
} else {
continue 2;
}
}
unset($array[array_shift($parts)]);
}
}
specifically the location of this line:
// clean up before each pass
$array = &$original;
what happens is on the next iteration of the loop, the $array is still pointing at the nested data, when it should be instead be pointing at the root object again
i believe this cleanup should instead occur immediately after this line:
unset($array[array_shift($parts)]);
please let me know if i am misunderstanding how to use this function, and if so what the preferred alternative is!
currently i am sorting my keys before invoking ->except to mitigate this error:
function except_keys(Collection $collection, Collection $keys): Collection
{
return $collection->except(
// HACK: this fixes a bug in laravel where the except method doesn't work as intended when a dotted string precedes
// a non-dotted string. This is a workaround until the bug is fixed in Laravel.
$keys->sort(fn(string $a, string $b) => Str::substrCount($a, '.') - Str::substrCount($b, '.'))
);
}
Please or to participate in this conversation.