Updated June 2026. Tested on Laravel 13 and PHP 8.4.

A query scope is a reusable piece of a query that you keep on the model. Instead of repeating the same where everywhere, you give it a name and call it by name. The query reads better and you fix it in one place.

Say you keep writing this to get active users.

$users = User::where('active', 1)->get();

If that condition shows up across the app, package it as a scope. There are three kinds: local, global and dynamic.

Local scopes

A local scope is one you opt into per query. Define a method on the model prefixed with scope, taking a query builder and returning it with your condition added.

use Illuminate\Database\Eloquent\Builder;

class User extends Model
{
    public function scopeActive(Builder $query): Builder
    {
        return $query->where('active', 1);
    }

    public function scopePopular(Builder $query): Builder
    {
        return $query->where('votes', '>', 100);
    }
}

Call them without the scope prefix, and chain them freely.

$users = User::popular()->active()->orderBy('created_at')->get();

In recent Laravel you can also mark a plain method with the #[Scope] attribute instead of the scope prefix, but the prefix style above still works and is what you will see most.

Dynamic scopes

A scope can take parameters. Add them after the query argument.

public function scopeOfType(Builder $query, string $type): Builder
{
    return $query->where('type', $type);
}

Then pass the value when you call it.

$admins = User::ofType('admin')->get();

Global scopes

A global scope applies to every query on a model automatically, without you asking for it each time. Laravel's own soft deletes work this way, quietly adding where deleted_at is null to your queries.

Write a class that implements the Scope interface and adds your condition.

namespace App\Models\Scopes;

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

class ActiveScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('active', 1);
    }
}

Attach it with the #[ScopedBy] attribute on the model.

use App\Models\Scopes\ActiveScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy(ActiveScope::class)]
class User extends Model
{
    //
}

Now User::all() only ever returns active users. For a simple one off condition you can skip the class and add an anonymous global scope in the model's booted method.

protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('active', 1);
    });
}

Removing a global scope

When you do need the rows a global scope hides, take it off for that one query.

User::withoutGlobalScope(ActiveScope::class)->get();
User::withoutGlobalScope('active')->get();   // for an anonymous scope, by name
User::withoutGlobalScopes()->get();          // remove all of them

Scopes work through relationships

Scopes are not just for top level queries. They chain onto relationships too.

$user = User::find(34);
$popularPosts = $user->posts()->popular()->get();

So: local scopes for conditions you opt into, dynamic scopes when they need a parameter, and global scopes for conditions that should always apply. They keep your queries short and your conditions in one place. Questions welcome in the comments.