In Laravel, how do you define and call a local Eloquent scope with #[Scope] or the scope prefix, including one that takes parameters?
answer
- protected method, Builder first
- #[Scope] or scopeName prefix
- call without the prefix, chainable
- extra arguments follow $query
- orWhere-> higher-order chaining
basics
~20 sMark a protected model method with #[Scope] (or name it scopeOpen) whose first parameter is the Eloquent Builder; extra parameters follow it. Call it as Matter::open()->assignedTo($lawyer)->get(), without any prefix, and chain it like any query method.
solid answer
~40 sA local scope is a named, reusable query constraint on the model. The current form is a `protected` method marked with `#[Scope]`, such as `protected function open(Builder $query): void { $query->whereNull('closed_at'); }`. The older form, `scopeOpen(Builder $query)`, still works: Eloquent resolves a scope call by checking for a `scope`-prefixed method or a non-private method carrying the attribute. Either way you call it without the prefix — `Matter::open()` — and it returns the builder, so scopes chain with each other and with `where()`. Parameters come after `$query`: `assignedTo(Builder $query, User $lawyer)` is called as `Matter::assignedTo($lawyer)`. Scopes also work on relation queries, as in `$client->matters()->open()->get()`. To OR two scopes, use `orWhere(fn ($q) => $q->urgent())` or the higher-order `orWhere->urgent()`. Inside the model, call an attributed scope through a builder, `static::query()->open()`.
code
php · 15 lines<?php
use App\Models\Client;
use App\Models\Matter;
$mine = Matter::open()
->assignedTo($request->user())
->orderBy('next_hearing_at')
->get();
$urgentOrOverdue = Matter::open()
->where(fn ($q) => $q->urgent()->orWhere->overdue())
->count();
$clientOpen = Client::findOrFail($id)->matters()->open()->get();go deeper
Recall that a scope is a model method taking the Builder, marked #[Scope] or prefixed with scope, and called without the prefix.
Explain how Eloquent resolves scope calls, how parameters follow $query, and how scopes chain on models and relation queries.
Watch OR grouping bugs, keep scopes composable and side-effect free, and push security filters into global scopes rather than habits.
Set conventions for which constraints become named scopes, query objects or global scopes, so queries stay consistent across teams.
## What a local scope is A **local scope** gives a name to a query constraint that the application repeats. In a legal-case manager, "open matters", "matters assigned to this lawyer" and "matters with a hearing this week" appear in controllers, reports and jobs. Writing the same `where` clauses in each place invites drift; a scope keeps the definition on the model. ## Defining a scope: two spellings ```php use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Builder; class Matter extends Model { #[Scope] protected function open(Builder $query): void { $query->whereNull('closed_at'); } #[Scope] protected function assignedTo(Builder $query, User $lawyer): void { $query->where('lead_lawyer_id', $lawyer->id); } // older spelling, still supported public function scopeUrgent(Builder $query): void { $query->where('priority', 'urgent'); } } ``` Reading the framework's model code, a call such as `Matter::open()` is resolved by `hasNamedScope()`, which accepts either a method called `scope` + the capitalised name, or a method of that name that is **not private** and carries the `#[Scope]` attribute. The documentation recommends `protected` for attributed scopes: a public method would be called directly on the model instead of being routed through the builder. | Aspect | `#[Scope]` method | `scope` prefix | |---|---|---| | Method name | `open()` | `scopeOpen()` | | Called as | `Matter::open()` | `Matter::open()` | | Visibility | `protected` (not private) | usually `public` | | First parameter | `Builder $query` | `Builder $query` | ## Parameters Anything after `$query` is a scope parameter, passed positionally at the call site: - `Matter::assignedTo($lawyer)` passes the user. - Several parameters are passed in order: `Matter::hearingBetween($from, $to)` for a scope declared as `hearingBetween(Builder $query, Carbon $from, Carbon $to)`. - Defaults work as in any PHP method, so a `hearingWithin(Builder $query, int $days = 7)` scope may be called with no argument. ## Calling and composing 1. Scopes return the builder, so they chain: `Matter::open()->assignedTo($lawyer)->latest('opened_at')->paginate()`. 2. They work on **relation queries** too: `$client->matters()->open()->count()`. 3. They compose inside other scopes: a `needsAttention()` scope can call `$query->open()->urgent()`. 4. To combine scopes with OR, group them: `Matter::urgent()->orWhere(fn (Builder $q) => $q->overdue())`, or use the higher-order form `Matter::urgent()->orWhere->overdue()`. 5. Inside the model, call an attributed scope through a builder, `static::query()->open()`, so the call goes through Eloquent's scope handling. ## A scope that also sets attributes A scope may call `$query->withAttributes(['status' => 'draft'])`. That adds the matching `where` and also fills those attributes on models created through the scoped query, so `Matter::drafts()->create([...])` produces a draft. Pass `asConditions: false` to set the attributes without adding the condition. ## Common mistakes - **Returning a new query.** A scope must modify the `$query` it receives (and return it or nothing). Building `static::where(...)` inside the scope and returning it discards the caller's earlier constraints. - **Forgetting that scopes stack with AND.** `Matter::open()->urgent()` means open **and** urgent; OR needs explicit grouping. - **Hiding expensive work.** A scope that joins three tables looks as cheap as `open()` at the call site; name it so readers can tell. - **Putting request data inside the scope.** Reading `request()` or `auth()` inside a local scope ties the model to HTTP; pass the value in as a parameter instead. ## Local versus global A local scope applies **only when called**. A constraint that must apply to every query of the model, such as the current law firm, is a **global scope**, registered once and removed explicitly. Choosing a local scope for a security boundary means every developer must remember to call it.
- Why does the documentation ask for attributed scope methods to be protected?Because the method is `protected`, PHP cannot call `Matter::open()` directly from outside the class and invokes the model's `__callStatic` instead, which sends attributed scopes to `static::query()`. A `public` method would be called directly, as a static call to an instance method, and fail. The source also ignores `private` methods, since it only treats non-private attributed methods as scopes.
- Why group scopes in a closure when mixing them with OR?Each scope adds its `where` clauses to the same level of the query. `Matter::open()->orWhere(...)` without grouping produces `closed_at is null or ...`, which lets closed matters in through the OR branch. Wrapping the OR pair in `where(fn ($q) => ...)` puts parentheses around it.
saying these in an interview costs you the question
- Calling the scope with its prefix, as Matter::scopeOpen()
- Forgetting the Builder parameter and putting arguments first
- Declaring an attributed scope method private
- Believing a local scope applies to every query automatically
- Mixing scopes with orWhere without grouping them