Question
Group AND and OR Conditions in Laravel Eloquent Queries
Question
How can I write the following grouped condition using Laravel Eloquent?
WHERE (a = 1 OR b = 1)
AND (c = 1 OR d = 1)
For more complex conditions that combine WHERE, AND, and OR, should I use raw SQL, or can Eloquent express them clearly?
Short Answer
Laravel Eloquent can express grouped AND and OR logic without raw SQL. You will learn to use closure-based where groups so the generated SQL has the intended parentheses and evaluates conditions in the correct order.
Concept
SQL gives AND a higher precedence than OR. That means a query without parentheses may produce results you did not intend.
For example, this condition:
WHERE a = 1 OR b = 1 AND c = 1 OR d = 1
is not automatically interpreted as:
WHERE (a = 1 OR b = 1) AND (c = 1 OR d = 1)
In Laravel Eloquent, use a closure passed to where() to create a parenthesized group. Inside that closure, add conditions with where() and orWhere().
$records = Record::query()
->where(function ($query) {
$query->where('a', 1)
->orWhere('b', 1);
})
->where(function ($query) {
$query->where('c', 1)
->orWhere('d', 1);
})
->get();
Conceptually, this produces:
WHERE (a = 1 OR b = 1)
AND (c = 1 OR d = 1)
This matters because query conditions often control security, permissions, search filters, visibility rules, and reports. Incorrect grouping can expose rows that should have been excluded.
Mental Model
Think of each closure as a pair of parentheses drawn around a small decision.
- The first closure asks: “Does this record pass either test
aor testb?” - The second closure asks: “Does this record pass either test
cor testd?” - The outer query joins those two decisions with
AND, so both groups must pass.
In other words:
Group 1 passes: a OR b
AND
Group 2 passes: c OR d
A closure is Laravel's way of saying, “Build this part of the query as one grouped unit.”
Syntax and Examples
Use where() with a closure whenever conditions need parentheses.
Model::query()
->where(function ($query) {
$query->where('first_column', 'value')
->orWhere('second_column', 'value');
})
->get();
The closure creates this SQL shape:
WHERE (first_column = 'value' OR second_column = 'value')
To combine two groups with AND, call where() twice:
$users = User::query()
->where(function ($query) {
$query->where('is_admin', true)
->orWhere('is_editor', true);
})
->where(function ($query) {
$query->where('is_active', true)
->orWhere('is_verified', true);
})
->get();
This selects users who are:
- an admin or an editor, and
- active or verified.
For the original condition, use:
$records = Record::query()
->where(function ($query) {
$query->where('a', 1)
->orWhere('b', 1);
})
->where(function ($query) {
$query->where('c', 1)
->orWhere('d', 1);
})
->get();
Use orWhere() with a closure when an entire group should be joined to the preceding condition using OR:
$users = User::query()
->where('is_active', true)
->orWhere(function ($query) {
$query->where('is_admin', true)
->where('is_verified', true);
})
->get();
That represents:
WHERE is_active = true
OR (is_admin = true AND is_verified = true)
Step by Step Execution
Consider this query:
$users = User::query()
->where(function ($query) {
$query->where('role', 'manager')
->orWhere('role', 'owner');
})
->where(function ($query) {
$query->where('status', 'active')
->orWhere('email_verified_at', '!=', null);
})
->get();
Laravel builds the conditions in this order:
User::query()starts a query for theuserstable.- The first closure creates a group:
(role = 'manager' OR role = 'owner') - The second
where()addsAND, followed by another group:(status = 'active' OR email_verified_at IS NOT NULL) - The final logical structure is:
WHERE (role = 'manager' OR role = 'owner') AND (status = 'active' OR email_verified_at IS NOT NULL) get()executes the query and returns an Eloquent collection of matchingUsermodels.
A user with role manager and status active matches. A user with role owner and a verified email also matches. A user who is only manager, but is neither active nor verified, does not match.
Real World Use Cases
Grouped conditions are useful whenever an application has alternative ways to satisfy each requirement.
- User access: A user can access a resource if they are an owner or an administrator, and the account is active or has a valid subscription.
- Product search: A product matches if its name or SKU matches a search term, and it is published or available to staff.
- Support tickets: Find tickets assigned to a team or a specific agent, and whose status is open or pending.
- Reporting: Include orders paid by card or bank transfer, and created within one of several reporting periods.
- Content visibility: Show articles that are public or owned by the current user, and are published or scheduled before now.
Example: public posts or a current user's drafts, but only when they are not archived:
$posts = Post::query()
->where(function ($query) {
$query->where('visibility', 'public')
->orWhere('user_id', auth()->id());
})
->where('is_archived', false)
->get();
Real Codebase Usage
In real Laravel projects, grouped conditions are usually combined with reusable query patterns rather than placed in one huge controller query.
Apply optional filters conditionally
Use when() so a group is added only when a filter exists:
$users = User::query()
->when($request->filled('role'), function ($query) use ($request) {
$query->where(function ($query) use ($request) {
$query->where('primary_role', $request->role)
->orWhere('secondary_role', $request->role);
});
})
->get();
Keep tenant or ownership restrictions grouped safely
When adding an orWhere, group it to prevent it from bypassing an earlier restriction:
$documents = Document::query()
->where('organization_id', $organizationId)
->where(function ($query) use ($userId) {
$query->where('is_public', true)
->orWhere('owner_id', $userId);
})
->get();
This keeps the organization rule required for both alternatives.
Extract repeated queries into local scopes
If a group represents a business rule used in multiple places, define a scope on the model:
// In app/Models/User.php
public function scopePrivileged($query)
{
return $query->where(function ($query) {
$query->where('is_admin', true)
->orWhere('is_editor', true);
});
}
Then use it readably:
$users = User::privileged()
->where('is_active', true)
->get();
Eloquent is generally preferable to raw SQL because it uses parameter binding for values, composes well with scopes and relationships, and remains easier to maintain. Use whereRaw() only when the database expression cannot be represented by Eloquent's normal query methods.
Common Mistakes
Forgetting the groups
This looks similar, but it changes the meaning:
// Incorrect for: (a OR b) AND (c OR d)
Record::query()
->where('a', 1)
->orWhere('b', 1)
->where('c', 1)
->orWhere('d', 1)
->get();
Because of SQL precedence, the resulting expression behaves like:
a = 1 OR (b = 1 AND c = 1) OR d = 1
Use closure groups instead.
Allowing an orWhere() to bypass an earlier condition
// Risky: records with is_admin = true may be returned from any organization.
User::query()
->where('organization_id', $organizationId)
->where('is_active', true)
->orWhere('is_admin', true)
->get();
Correct version:
User::query()
->where('organization_id', $organizationId)
->where(function ($query) {
$query->where('is_active', true)
->orWhere('is_admin', true);
})
->get();
Starting a query with orWhere()
An orWhere() needs a condition before it to join with. Start a group with where() and use orWhere() for later alternatives.
// Preferred inside a group
$query->where('a', 1)
->orWhere('b', 1);
Using raw SQL only for parentheses
You do not need this merely to group conditions:
// Unnecessary for ordinary column comparisons
->whereRaw('(a = 1 OR b = 1) AND (c = 1 OR d = 1)')
Closures are clearer and safely bind dynamic values automatically.
Comparing nullable columns with != null
For nullable columns, prefer whereNotNull() rather than a normal comparison:
$query->whereNotNull('email_verified_at');
This expresses the intent directly and avoids SQL NULL comparison pitfalls.
Comparisons
| Approach | Best for | Parentheses/grouping | Example |
|---|---|---|---|
where() | A required condition joined with AND | A closure creates a group | ->where('status', 'active') |
orWhere() | An alternative condition joined with OR | A closure groups the full alternative | ->orWhere('role', 'admin') |
where(function ...) | Conditions that must be evaluated together | Yes | ->where(fn ($q) => $q->where(...)->orWhere(...)) |
whereIn() | One column can equal any value in a list | Not a replacement for multi-column logic | ->whereIn('status', ['new', 'open']) |
whereRaw() | Database-specific expressions or unsupported SQL | You write grouping yourself | ->whereRaw('LOWER(name) = ?', [$name]) |
For a single column, this:
$query->where('status', 'new')
->orWhere('status', 'open');
can often be simplified to:
$query->whereIn('status', ['new', 'open']);
However, whereIn() cannot replace (a = 1 OR b = 1) because that condition checks two different columns.
Cheat Sheet
// Required condition (AND)
$query->where('column', 'value');
// Alternative condition (OR)
$query->orWhere('column', 'value');
// (a = 1 OR b = 1)
$query->where(function ($query) {
$query->where('a', 1)
->orWhere('b', 1);
});
// (a = 1 OR b = 1) AND (c = 1 OR d = 1)
$query->where(function ($query) {
$query->where('a', 1)
->orWhere('b', 1);
})->where(function ($query) {
$query->where('c', 1)
->orWhere('d', 1);
});
// status = 'new' OR status = 'open'
$query->whereIn('status', ['new', 'open']);
// Check for NULL correctly
$query->whereNull('deleted_at');
$query->whereNotNull('email_verified_at');
Rules to remember:
- Use a closure when an
ORbelongs inside parentheses. where()between groups meansAND.orWhere()between groups meansOR.- Never assume chained methods automatically group themselves.
- Prefer Eloquent methods and closures before reaching for
whereRaw().
FAQ
How do I add parentheses around orWhere conditions in Laravel?
Pass a closure to where() or orWhere(). The conditions inside the closure are grouped with parentheses in the generated SQL.
->where(function ($query) {
$query->where('a', 1)->orWhere('b', 1);
})
Can Eloquent handle complex AND and OR queries without raw SQL?
Yes. Nested where() and orWhere() closures handle most complex boolean logic. Use raw SQL only for expressions that Eloquent cannot represent clearly.
Why does my Laravel query return too many rows after adding orWhere()?
Your orWhere() may not be grouped, so it can apply to the entire preceding query rather than only to the conditions you intended. Put related alternatives in a closure.
Does calling where() twice create an AND condition?
Yes. Consecutive where() calls are joined with AND unless you use a different method such as orWhere().
When should I use orWhere(function ($query) {})?
Use it when a complete parenthesized group should be an alternative to a previous condition, such as active = 1 OR (admin = 1 AND verified = 1).
Can I inspect the SQL generated by Eloquent?
You can inspect the SQL template with toSql() and bindings with getBindings().
$query = User::where('is_active', true);
$sql = $query->toSql();
$bindings = $query->getBindings();
The SQL template contains placeholders (?); bindings hold the corresponding values.
Is whereRaw() unsafe?
It can be unsafe if dynamic input is inserted into the SQL string. If raw SQL is necessary, use parameter bindings rather than concatenating user input.
Mini Project
Description
Build a reusable user-directory query that finds eligible users. A user must have an allowed role and must also be active or have a verified email address. This reflects common admin dashboards and access-management screens.
Goal
Write an Eloquent query that correctly selects users matching (role is manager OR role is owner) AND (active OR verified).
Requirements
The output must be a JSON object, not an array.
Keep learning
Related questions
Are PDO Prepared Statements Enough to Prevent SQL Injection in PHP?
Learn how PDO prepared statements prevent SQL injection in PHP, what they protect, and the mistakes that still leave MySQL apps vulnerable.
Can You Bind an Array to an IN Clause in PHP PDO?
Learn how PDO handles placeholders in IN() clauses, why arrays cannot be bound directly, and the safe PHP pattern to build dynamic queries.
Choosing the Right MySQL Collation for PHP and UTF-8
Learn how MySQL character sets and collations work with PHP, and how to choose a practical UTF-8 setup for web applications.