Global scopes in Laravel are one of those features that often go unnoticed—until you really need them. They provide a way to ensure certain query constraints are always applied, no matter how complex or messy your query logic becomes later on.
A global scope attaches additional conditions to all queries for a given Eloquent model. Common examples include filtering out soft-deleted records, limiting data to a specific tenant, or restricting access to a subset of records based on type.
Consider a multi-tenant setup where every record should belong to a specific tenant, or where certain models should always represent an “internal” user type. You might define a global scope like this:
class InternalUserScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('type', 'internal');
}
}
You can attach it to your model:
class User extends Model
{
use SoftDeletes;
protected static function booted()
{
static::addGlobalScope(new InternalUserScope);
}
}
Now, every query on the User
model will automatically include where type = 'internal'
— without you ever needing to remember to add it manually.
Here’s the beauty of global scopes: they are always applied, even when you accidentally (or intentionally) try to work around them with complex conditions.
Take the following example:
User::query()->ddRawSql();
This outputs:
select * from `users` where `users`.`deleted_at` is null and `type` = 'internal'
Now let’s try to be clever and add an orWhere
:
User::query()->orWhere('type', 'external')->ddRawSql();
You might expect this to give you both internal and external users.
But the actual SQL is:
select * from `users` where ((`type` = 'external') and `users`.`deleted_at` is null) and `type` = 'internal'
Laravel has correctly grouped your conditions so the global scope still applies.
Even though you used an orWhere
, the global scope ensures that the type = 'internal'
condition remains enforced. The same goes for any other global scope, such as a tenant_id
filter in a multi-tenant application.
In a multi-tenant setup, global scopes can prevent serious data leaks. For example, if every model includes a tenant scope:
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('tenant_id', Auth::user()->tenant_id);
}
}
Then no matter what query your developers write — even those involving joins, subqueries, or orWhere
conditions — the tenant filter is always applied. This prevents cross-tenant data exposure without relying on developers to remember to add where('tenant_id', ...)
everywhere.
Global scopes aren’t a silver bullet. They can hide complexity when debugging queries or make certain administrative queries harder to write. Laravel provides an escape hatch for these cases:
User::withoutGlobalScopes()->get();
User::withoutGlobalScope(InternalUserScope::class)->get();
Use these sparingly — they’re the equivalent of “root” access for your model.
Global scopes are one of the most reliable ways to enforce consistent data filtering across your Laravel models.
They keep your queries safe, predictable, and aligned with your application’s rules — even when someone accidentally writes an orWhere
that would otherwise bypass your constraints.
In environments like multi-tenant apps, they’re not just a convenience — they’re a security feature.
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts, subscribe use the RSS feed.