skip to content

In Laravel, how do you define and call a local Eloquent scope with #[Scope] or the scope prefix, including one that takes parameters?

level: juniorimportance: must knowfreq 58%

answer

  1. protected method, Builder first
  2. #[Scope] or scopeName prefix
  3. call without the prefix, chainable
  4. extra arguments follow $query
  5. orWhere-> higher-order chaining

basics

~20 s

Mark 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 s

A 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
<?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

for a junior

Recall that a scope is a model method taking the Builder, marked #[Scope] or prefixed with scope, and called without the prefix.

for a middle

Explain how Eloquent resolves scope calls, how parameters follow $query, and how scopes chain on models and relation queries.

for a senior

Watch OR grouping bugs, keep scopes composable and side-effect free, and push security filters into global scopes rather than habits.

for a principal

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