Sitemap
Muhammad Saim Hashmi

Muhammad Saim Hashmi

Sr. Backend Software Engineer - Programmer - Developer

Stop Copy-Pasting Your where() Conditions in Laravel

7 min readNov 3, 2026

--

Press enter or click to view image in full size

Open any busy Laravel project and search for where('status', 'active'). You will probably find it again and again, in controllers, models, reports and jobs. The same few conditions, copied from place to place.

It feels harmless until the day someone forgets one line.

I lived this in a multi-tenant SaaS product. The same conditions, “status is active” and “belongs to the current organization,” were repeated in dozens of queries. One forgotten condition showed data from the wrong place and caused bugs. I moved the “active” filter into a local scope and the “current organization” rule into a global scope. For admin reports that needed everything, I removed the global scope on purpose, only where needed. This post shows you how. It is part of my LARAVEL FOUNDATIONS series.

The Netflix profile

Think about Netflix. On a kids profile, adult shows are hidden from everything you browse. You never ask for that. The profile does it for you, always.

That is a global scope. It is a rule applied automatically to every query on a model.

Now think about the “Comedy” tab. It is a shortcut. You click it when you want it, and you skip it when you do not.

That is a local scope. It is a filter you choose to apply.

What is a query scope?

A query scope is a piece of a database query with a name. You write it once inside your model, and you reuse it everywhere. Less copy-paste, fewer places to forget something.

Think of a saved search in your email app, like “unread emails from my boss.” You build it once, give it a name, and click it whenever you need it. A scope is a saved search for your database.

The problem: repeated conditions

Here is what it looks like without scopes.

// InvoiceController
$customers = Customer::where('organization_id', auth()->user()->organization_id)
->where('status', 'active')
->get();

// ReportController
$customers = Customer::where('status', 'active')
->orderBy('name')
->get(); // forgot organization_id!

Spot the bug? The second query forgot the organization rule. It runs fine. It just quietly returns the wrong customers.

Local scopes

To define a local scope in current Laravel, add the #[Scope] attribute to a method on your model. As of November 1, 2026, this is how the Laravel 13.x docs show it. The method should be protected, and it should return the builder or void.

<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
#[Scope]
protected function active(Builder $query): void
{
$query->where('status', 'active');
}

#[Scope]
protected function ofStatus(Builder $query, string $status): void
{
$query->where('status', $status);
}
}

Now you call it by its name, without the word “scope,” and you can chain it with other things.

$customers = Customer::active()->get();

$recent = Customer::active()->latest()->take(10)->get();

Each scope adds one more condition, and they all work together, like adding filters one by one in a shopping app. Pick “active,” then pick “newest first,” and you get both.

One tip from the docs. If you want to join scopes with “or” instead of “and,” the conditions need grouping, so Laravel gives you a helper called orWhere on the scope chain. The docs explain it, and I will keep this post simple.

Older projects may use the old style, a method named scopeActive. It still works, so you do not have to rewrite everything today.

Scopes with parameters

Sometimes the filter needs a value. That is the second method above, ofStatus. Add your extra parameters after $query, and pass them when you call it.

$paused = Customer::ofStatus('paused')->get();

One small tip from the docs. If you call an attributed scope from inside the model itself, call it through a query builder, like static::query()->ofStatus('paused').

Global scopes

A global scope is a small class with one method, apply. This one limits customers to the logged-in user's organization.

<?php
namespace App\Models\Scopes;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class CurrentOrganizationScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$organizationId = auth()->user()?->organization_id;

if ($organizationId) {
$builder->where($model->qualifyColumn('organization_id'), $organizationId);
}
}
}

Then attach it to the model with the #[ScopedBy] attribute.

<?php
namespace App\Models;

use App\Models\Scopes\CurrentOrganizationScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;
use Illuminate\Database\Eloquent\Model;

#[ScopedBy([CurrentOrganizationScope::class])]
class Customer extends Model
{
//
}

From now on, every Customer query gets the organization rule. A quick note on qualifyColumn. It adds the table name in front of the column, so a joined table with a column of the same name does not confuse your query.

Get Muhammad Saim Hashmi’s stories in your inbox

Join Medium for free to get updates from this writer.

Now look at the old ReportController again. This is all it needs.

$customers = Customer::active()->orderBy('name')->get();

The active filter comes from the local scope. The organization rule comes from the global scope, automatically. The forgotten line cannot be forgotten, because nobody has to write it anymore.

For a quick, small rule, there is also the anonymous version. You give the scope a name of your choice and use a closure inside the model’s booted method.

protected static function booted(): void
{
static::addGlobalScope('current_organization', function (Builder $builder) {
$organizationId = auth()->user()?->organization_id;

if ($organizationId) {
$builder->where('organization_id', $organizationId);
}
});
}

Laravel already does this

You may already use a global scope without knowing it. When you add the SoftDeletes trait to a model, Laravel adds a global scope that hides deleted rows. You never write whereNull('deleted_at') yourself.

class Invoice extends Model
{
use SoftDeletes;
}

Taking a global scope off

Sometimes you really need everything. Say, an admin report. Use withoutGlobalScope().

// Admin report: all organizations, on purpose
$all = Customer::withoutGlobalScope(CurrentOrganizationScope::class)->get();

// For a closure scope, use the name you gave it
$all = Customer::withoutGlobalScope('current_organization')->get();

// Soft deleted rows are hidden by a built-in global scope
$withDeleted = Invoice::withTrashed()->get();

Do this only where you truly need it, and keep it easy to spot in your code. There is also withoutGlobalScopes(), which removes every global scope at once. That includes built-in ones, like the soft delete rule, so use the single version when you only mean to remove one.

The traps of global scopes

Global scopes are powerful, so they need care.

  • Hidden filters surprise people. A new teammate sees Customer::all() and wonders why some customers are missing. The rule is invisible at the call site.
  • Raw queries skip them. A scope belongs to the model. If you write DB::table('customers'), the scope does not run, because the model is not involved.
  • Jobs, commands and seeders have no logged-in user. Look at my scope again. When nobody is logged in, it adds no filter at all. In a queued job, that means all organizations. Pass the organization to the job on purpose, or decide what the scope should do when there is no user.
  • Do not use a global scope as your only security. It is a safety net for forgetting. It does not replace checking who is allowed to see what, as in my post on [LINK: Policies and Gates post].

Which one should you choose?

Start with local scopes. They are visible, because the name is right there in the query, and nothing surprises anyone. Use a global scope only for a rule that must always apply, like “this data belongs to my organization.”

Here is a simple question to ask yourself. “Is there ever a query on this model where this rule should not apply?” If the answer is yes, even once, use a local scope. If the answer is never, a global scope can be a good fit.

If you are not sure, choose local. It is easy to promote a local scope later, but a hidden global rule is hard to find when something looks wrong.

Testing both

Short tests keep both scopes honest. These assume you have factories for your models and an organization_id on your users.

<?php
namespace Tests\Feature;

use App\Models\Customer;
use App\Models\Organization;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class CustomerScopeTest extends TestCase
{
use RefreshDatabase;

public function test_active_scope_returns_only_active_customers(): void
{
Customer::factory()->create(['status' => 'active']);
Customer::factory()->create(['status' => 'paused']);

$this->assertCount(1, Customer::active()->get());
}

public function test_global_scope_hides_other_organizations(): void
{
$mine = Organization::factory()->create();
$other = Organization::factory()->create();
$user = User::factory()->create(['organization_id' => $mine->id]);

Customer::factory()->create(['organization_id' => $mine->id]);
Customer::factory()->create(['organization_id' => $other->id]);

$this->actingAs($user);

$this->assertCount(1, Customer::all());
$this->assertCount(2, Customer::withoutGlobalScopes()->get());
}

public function test_scope_does_nothing_when_nobody_is_logged_in(): void
{
Customer::factory()->count(2)->create();

$this->assertCount(2, Customer::all());
}
}

The last test is on purpose. It shows the trap, so you never forget it.

Common mistakes

  • Putting a rule in a global scope that only some queries need.
  • Forgetting that jobs and commands have no logged-in user.
  • Trusting a global scope as the only protection for private data.
  • Using DB::table() and expecting scopes to run.
  • Removing a global scope everywhere instead of only in the one place that needs it.
  • Using withoutGlobalScopes() when you only meant to remove one, which also removes the others.
  • Giving an anonymous scope a vague name, then not knowing what to pass to withoutGlobalScope().
  • Forgetting that an attributed local scope should be protected.

Conclusion

Name your filters once. Use local scopes for filters you choose. Use a global scope only for the rules that must never be forgotten, and keep a careful eye on the traps. Your queries get shorter, and a missing line stops being a quiet bug.

Which where() condition do you copy the most in your project today?

--

--

Sr. Backend Software Engineer - Programmer - Developer

See more from me