Model::where
Adds a where clause to the query, inferring the = operator when you pass only a value.
public function where(\Closure|string|array|\Illuminate\Contracts\Database\Query\Expression $column, mixed $operator = null, mixed $value = null, string $boolean = 'and'): $thisParameters
| Parameter | Type | Description |
|---|---|---|
$column | \Closure|string|array|\Illuminate\Contracts\Database\Query\Expression | A column name, an array of conditions, or a Closure that opens a parenthesised constraint group. |
$operatoroptional | mixed= null | A comparison operator. When only two arguments are given this is treated as the value and the operator becomes '='. |
$valueoptional | mixed= null | The value to compare the column against. A Closure or builder here becomes a subquery. |
$booleanoptional | string= 'and' | How this clause joins the previous one — 'and' or 'or'. |
Return value
$thisThe same builder instance, so calls can be chained. Nothing is executed until you call a terminator such as get(), first(), or count().
Errors
Throws InvalidArgumentException with the message "Illegal operator and value combination." when three arguments are passed, the value is null, and the operator is a recognised one other than '=', '<=>', '<>' or '!='. An operator the grammar does not recognise does not throw: invalidOperator() causes it to be re-read as the value, with '=' substituted as the operator.
Changelog
- PHP 12.xPassing null as the value delegates to whereNull(), producing an IS NULL (or IS NOT NULL for a negating operator) clause rather than a literal = null comparison that would never match.
where() is the workhorse constraint. The full call takes three arguments — column, operator, value — and the two-argument form is a shorthand that assumes =. Chained calls join with and, and the builder is returned each time, so nothing hits the database until a terminator like get() or first() runs.
The forms worth knowing
- Two arguments:
where('votes', 100)compiles tovotes = ?. - Three arguments: any operator your database supports, including
like,<>, and>=. - An associative array: several equality checks at once.
- An array of triples:
[['status', '=', '1'], ['subscribed', '<>', '1']]. - A Closure: opens a parenthesised group, which is how you contain an
or.
What trips people up
MySQL and MariaDB typecast strings to integers in string-number comparisons, and a non-numeric string becomes 0. A row whose secret column holds 'aaa' is returned by where('secret', 0). Cast values to the right type before they reach the query.
PDO cannot bind column names. Only the value is parameterised — user input must never choose the column.
Passing null as the value does not produce = null; Laravel rewrites it to IS NULL, which is almost always what you meant but surprises anyone reading the compiled SQL.
Example
<?php
use App\Models\User;
// Two arguments imply '='.
$users = User::where('votes', 100)->get();
// select * from users where votes = ?
// Three arguments let you name the operator.
$users = User::where('votes', '>=', 100)
->where('name', 'like', 'T%')
->get();
// select * from users where votes >= ? and name like ?
// An associative array is several equality checks at once.
$users = User::where([
'first_name' => 'Jane',
'last_name' => 'Doe',
])->get();
echo $users->count(), "\n";When to use it
- Filter a listing to the current tenant's rows before paginating.
- Narrow a report to a date range using >= and < comparisons.
- Build a parenthesised group so an optional search term does not widen an authorisation check.
Frequently asked questions
What is the difference between two and three arguments in where()?
Two arguments is shorthand for equality — where('votes', 100) compiles to votes = ?. The three-argument form puts an operator in the middle, so where('votes', '>=', 100) compiles to votes >= ?. Laravel decides between them by argument count alone: prepareValueAndOperator() tests func_num_args() === 2 and, when that is true, treats the second argument as the value and forces the operator to '='. That is why where('votes', '>') does not error — it silently compiles to votes = ? bound with the string '>', which matches nothing.
How do I group OR conditions inside a where clause?
Pass a Closure as the only argument: where(function ($query) { $query->where('votes', '>', 100)->orWhere('title', 'Admin'); }). The closure receives a builder and everything set on it is wrapped in parentheses. Without the grouping the OR joins at the top level and can match rows your other conditions were meant to exclude, especially once global scopes add their own clauses.
Why does where('secret', 0) match a row containing text?
MySQL and MariaDB typecast strings to integers when comparing a string column to a number, and any non-numeric string converts to 0 — so a row whose secret column holds 'aaa' satisfies secret = 0. Cast the value to a string before querying, or validate the input type upfront. PostgreSQL raises a type error instead of silently matching.
More examples
Grouping with a Closure
The parentheses in the compiled SQL are the whole point. The commented variant has no grouping, so any row with title = 'Admin' matches regardless of name — the classic way an authorisation filter silently stops filtering.
<?php
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
$users = User::where('name', '=', 'John')
->where(function (Builder $query) {
$query->where('votes', '>', 100)
->orWhere('title', '=', 'Admin');
})
->get();
// select * from users
// where name = ? and (votes > ? or title = ?)
echo $users->count(), "\n";
// Without the closure the OR escapes and matches every admin:
// where name = ? and votes > ? or title = ?Array conditions and operator variety
The date range shows the half-open interval that avoids the end-of-month boundary bugs BETWEEN invites. The last block shows Laravel rewriting a null value into IS NULL — written literally as = null in SQL it would match nothing at all.
<?php
use App\Models\Order;
// An array of triples - same three arguments, batched.
$orders = Order::where([
['status', '=', 'paid'],
['refunded', '<>', 1],
])->get();
// Any operator the database supports.
$recent = Order::where('total_cents', '>=', 5000)
->where('placed_at', '>=', '2026-01-01')
->where('placed_at', '<', '2026-02-01')
->get();
echo $recent->count(), "\n";
// A null value becomes IS NULL, not = null.
$unassigned = Order::where('assigned_to', null)->get();
// select * from orders where assigned_to is null
echo $unassigned->count(), "\n";Never let input choose the column
The allow-list is not optional ceremony: column names are interpolated into the SQL string, so an unchecked request value is an injection point. The final query shows the safe half — the search term is bound as a parameter, so only the LIKE wildcards need escaping.
<?php
use App\Models\Product;
$sortable = ['name', 'price_cents', 'created_at'];
$requested = request('sort_by');
// PDO binds values, never identifiers - validate against a list.
$column = in_array($requested, $sortable, true) ? $requested : 'name';
$products = Product::where('active', 1)
->orderBy($column)
->get();
echo $products->count(), "\n";
// The value side IS parameterised and safe:
$search = request('q');
$hits = Product::where('name', 'like', '%'.$search.'%')->get();
echo $hits->count(), "\n";
Discussion