Collection::where
Filters rows by a key/value pair, with an optional comparison operator.
public function where(callable|string $key, mixed $operator = null, mixed $value = null): staticParameters
| Parameter | Type | Description |
|---|---|---|
$key | callable|string | The key to read from each item, resolved with data_get so dot paths and object properties work. A callable is used directly as the filter. |
$operatoroptional | mixed= null | One of ===, !==, !=, ==, =, <>, >, <, >=, <=. When only two arguments are passed this is treated as the value and the operator defaults to '='. |
$valueoptional | mixed= null | The value to compare against. With one argument only, this defaults to true, so where('active') keeps rows with a truthy 'active'. |
Return value
staticA new collection of the matching items, with the original keys preserved.
where builds a comparison closure and hands it to filter. The argument count decides the meaning: one argument compares the key against true, two treat the second as the value with an implicit =, three use your operator. Supported operators are ===, !==, !=, ==, =, <>, >, <, >= and <=.
When to reach for it
Any time you would otherwise write filter(fn ($r) => $r['status'] === 'paid'). Beyond being shorter, the key goes through data_get, so where('customer.country', 'FR') reaches into nested arrays and objects with one string, and the same call works whether the rows are arrays, stdClass objects or Eloquent models.
What trips people up
The default comparison is loose, so where('id', 5) also matches the string '5'. Use whereStrict(), or pass '===' as the operator, when types matter.
where('deleted_at', null) is the classic bug: two arguments means == null, and null loosely equals 0, false and '', so falsy rows match too. Use whereNull() / whereNotNull(). Finally, keys are preserved — chain values() before serializing.
Example
When to use it
- Keep only the paid rows of a decoded invoice payload
- Filter an already-loaded collection without another database query
- Select rows above a threshold with where('score', '>', 80)
- Match on a nested field using dot notation, such as where('meta.locale', 'fr')
Frequently asked questions
How do I filter a Laravel collection by more than equality?
Pass an operator as the second argument: $collection->where('age', '>', 18). The supported operators are ===, !==, !=, ==, =, <>, >, <, >= and <=.
Why does where() match a string when I passed an integer?
Because the default '=' comparison is loose, so 5 and '5' are equal. Use whereStrict(), or pass '===' as the operator, to compare types as well as values.
Is Collection::where the same as the query builder's where?
No. The collection version filters rows already loaded into memory in PHP and supports only the operators listed above, while the builder version generates SQL. Filtering a large table belongs in the query, not in the collection.
More examples
The null trap
Row 1 is the only genuine null, yet the loose comparison also pulls in the integer 0 and the empty string, since both are == null in PHP. whereNull is implemented as whereStrict($key, null) and gets it right. This is the single most common where() bug on real data.
<?php
$records = collect([
['id' => 1, 'archived_at' => null],
['id' => 2, 'archived_at' => '2024-03-01'],
['id' => 3, 'archived_at' => 0],
['id' => 4, 'archived_at' => ''],
]);
// Loose ==: null matches 0 and '' too.
echo $records->where('archived_at', null)->pluck('id')->implode(', '), "\n";
// whereNull uses === and only matches real nulls.
echo $records->whereNull('archived_at')->pluck('id')->implode(', '), "\n";
echo $records->whereNotNull('archived_at')->pluck('id')->implode(', '), "\n";
// Output:
// 1, 3, 4
// 1
// 2, 3, 4Dot notation, one argument, and objects
The single-argument form fills in the operator '=' and the value true, so A3's integer 1 matches alongside A1's boolean. The dot path crosses from an object property into a nested array without any special handling because data_get does both. Chained where() calls are ANDed, since each filters the result of the last.
<?php
$orders = collect([
(object) ['ref' => 'A1', 'paid' => true, 'customer' => ['country' => 'FR']],
(object) ['ref' => 'A2', 'paid' => false, 'customer' => ['country' => 'FR']],
(object) ['ref' => 'A3', 'paid' => 1, 'customer' => ['country' => 'DE']],
]);
// One argument means "== true", so truthy values match.
echo $orders->where('paid')->pluck('ref')->implode(', '), "\n";
// data_get walks object properties and nested arrays alike.
echo $orders->where('customer.country', 'FR')->pluck('ref')->implode(', '), "\n";
// Chain to combine conditions.
echo $orders->where('paid')->where('customer.country', 'FR')
->pluck('ref')->implode(', '), "\n";
// Output:
// A1, A3
// A1, A2
// A1Filtering loaded models without another query
Once get() has run you hold a Collection, so where() is this method rather than the builder's — the comparison happens in PHP over rows already in memory. That is exactly what you want for slicing one result set several ways, and exactly what you do not want for narrowing a large table, where the condition belongs in the query. Needs a booted application with a database.
<?php
use App\Models\Order;
$orders = Order::with('lines')->whereMonth('created_at', 3)->get();
// These run in PHP against the already-loaded rows — no extra SQL.
$large = $orders->where('total_cents', '>=', 10000);
$refund = $orders->where('status', 'refunded');
echo $large->count().' large, '.$refund->count()." refunded\n";
// Careful: this is the collection method, not the query builder's.
// It cannot use SQL operators like 'like', and it never hits the database.
$byName = $orders->filter(fn ($o) => str_contains($o->reference, 'INV'));
Discussion