Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

austinrulezd00d's avatar

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, '.'))
    );
}
0 likes
0 replies

Please or to participate in this conversation.