Collection::where

Filters rows by a key/value pair, with an optional comparison operator.

Signature
public function where(callable|string $key, mixed $operator = null, mixed $value = null): static

Parameters

ParameterTypeDescription
$keycallable|stringThe 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.
$operatoroptionalmixed= nullOne of ===, !==, !=, ==, =, <>, >, <, >=, <=. When only two arguments are passed this is treated as the value and the operator defaults to '='.
$valueoptionalmixed= nullThe 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

Try it yourself
Loading editor…
Press Run to execute the code.

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.

Example · php
<?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, 4

Dot 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.

Example · php
<?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
// A1

Filtering 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.

Example · php
<?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

  • Be the first to comment on this lesson.