> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Eloquent 的 Scope

> 解說本地 Scope 與 Global Scope 的機制。透過解讀 SoftDeletingScope 等框架內部實作，介紹多租戶、公開/非公開篩選等實用範例。

## 什麼是 Scope

Eloquent 的 Scope 是能將查詢的約束彙整並重複使用的機制。Scope 分為兩種。

| 種類               | 套用時機      | 用途                  |
| ---------------- | --------- | ------------------- |
| **Global Scope** | 恆自動套用     | 軟刪除、多租戶、公開篩選        |
| **本地 Scope**     | 僅在明確呼叫時套用 | 「熱門文章」、「活躍使用者」等共同篩選 |

## 本地 Scope

### 定義

本地 Scope 透過在模型的方法上加註 `#[Scope]` attribute 來定義。

```php theme={null}
<?php

namespace App\Models;

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

class Post extends Model
{
    /**
     * 限定為已公開的文章
     */
    #[Scope]
    protected function published(Builder $query): void
    {
        $query->where('status', 'published');
    }

    /**
     * 限定為熱門文章
     */
    #[Scope]
    protected function popular(Builder $query): void
    {
        $query->where('views', '>', 1000);
    }
}
```

<Info>
  `#[Scope]` attribute 位於 `Illuminate\Database\Eloquent\Attributes` 命名空間，是 PHP 8.0 以後的原生語法。
</Info>

### 使用方式

已定義的 Scope 可作為方法呼叫。也能鏈式使用。

```php theme={null}
use App\Models\Post;

// 僅取得已公開的文章
$posts = Post::published()->get();

// 取得已公開且熱門的文章，以最新順序
$posts = Post::published()->popular()->latest()->get();
```

### 傳遞參數

可於 Scope 方法的第 2 個引數以後定義額外參數。

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

呼叫時直接傳入引數即可。

```php theme={null}
$posts = Post::ofStatus('draft')->get();
$posts = Post::ofStatus('published')->get();
```

### 與 `orWhere` 的組合

以 `orWhere` 串接 Scope 時，有時需要邏輯群組。

```php theme={null}
// 使用 closure 的方式（確實但冗長）
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

// 使用高階方法可寫得更簡潔
$users = User::popular()->orWhere->active()->get();
```

## Global Scope

### 機制

Global Scope 是實作 `Illuminate\Database\Eloquent\Scope` 介面的類別。此介面僅要求一個 `apply` 方法。

```php theme={null}
// 框架本體的介面定義
// src/Illuminate/Database/Eloquent/Scope.php

interface Scope
{
    public function apply(Builder $builder, Model $model);
}
```

於 `apply` 方法中對查詢建構器加入約束。

### 建立 Global Scope 類別

以 `make:scope` 指令產生範本。

```bash theme={null}
php artisan make:scope ActiveScope
```

會產生 `app/Models/Scopes/ActiveScope.php`。

```php theme={null}
<?php

namespace App\Models\Scopes;

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

class ActiveScope implements Scope
{
    /**
     * 將 Scope 套用至查詢建構器
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('is_active', true);
    }
}
```

### 套用至模型

<Steps>
  <Step title="以 #[ScopedBy] attribute 註冊（推薦）">
    Laravel 13 中使用 `#[ScopedBy]` attribute 最為簡潔。

    ```php theme={null}
    <?php

    namespace App\Models;

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

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

    可以陣列指定多個 Scope。

    ```php theme={null}
    #[ScopedBy([ActiveScope::class, TenantScope::class])]
    class User extends Model
    {
        //
    }
    ```
  </Step>

  <Step title="於 booted() 方法手動註冊">
    也可以覆寫 `booted` 方法並呼叫 `addGlobalScope`。

    ```php theme={null}
    <?php

    namespace App\Models;

    use App\Models\Scopes\ActiveScope;
    use Illuminate\Database\Eloquent\Model;

    class User extends Model
    {
        protected static function booted(): void
        {
            static::addGlobalScope(new ActiveScope);
        }
    }
    ```
  </Step>
</Steps>

加入 Global Scope 後，`User::all()` 等所有查詢都會自動附加 `WHERE is_active = 1`。

### 以匿名 Closure 定義 Global Scope

單純且不至於為其建立獨立檔案的 Scope，可以 closure 定義。

```php theme={null}
protected static function booted(): void
{
    static::addGlobalScope('active', function (Builder $builder) {
        $builder->where('is_active', true);
    });
}
```

<Warning>
  要排除以 closure 定義的 Scope，需使用 Scope 名稱（字串）而非類別名稱。
</Warning>

### Global Scope 的排除

有時希望於特定查詢停用 Scope。

```php theme={null}
use App\Models\Scopes\ActiveScope;

// 排除特定 Scope
User::withoutGlobalScope(ActiveScope::class)->get();

// 排除以 closure 定義的 Scope
User::withoutGlobalScope('active')->get();

// 排除所有 Global Scope
User::withoutGlobalScopes()->get();

// 排除多個 Scope
User::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();

// 除指定 Scope 以外全數排除
User::withoutGlobalScopesExcept([TenantScope::class])->get();
```

## 框架內部：SoftDeletingScope

觀察 Laravel 標準的 `SoftDeletes` trait 如何運用 Global Scope，即可掌握實作模式。

`SoftDeletingScope` 實作了 `Scope` 介面。

```php theme={null}
// src/Illuminate/Database/Eloquent/SoftDeletingScope.php

class SoftDeletingScope implements Scope
{
    protected $extensions = [
        'Restore', 'RestoreOrCreate', 'CreateOrRestore',
        'WithTrashed', 'WithoutTrashed', 'OnlyTrashed',
    ];

    /**
     * 將 Scope 套用至查詢建構器
     * 加入僅取得 deleted_at 為 NULL 之記錄的限制
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull($model->getQualifiedDeletedAtColumn());
    }

    /**
     * 為查詢建構器擴充 macro
     * 讓 withTrashed() / onlyTrashed() 等可以使用
     */
    public function extend(Builder $builder)
    {
        foreach ($this->extensions as $extension) {
            $this->{"add{$extension}"}($builder);
        }

        // 覆寫 delete() 操作為更新 deleted_at 的處理
        $builder->onDelete(function (Builder $builder) {
            $column = $this->getDeletedAtColumn($builder);
            return $builder->update([
                $column => $builder->getModel()->freshTimestampString(),
            ]);
        });
    }
}
```

<Accordion title="來看 withTrashed() 的實作">
  `withTrashed()` 實際上是呼叫 `withoutGlobalScope($this)`。也就是排除 `SoftDeletingScope` 自身，讓已刪除記錄也可取得。

  ```php theme={null}
  protected function addWithTrashed(Builder $builder)
  {
      $builder->macro('withTrashed', function (Builder $builder, $withTrashed = true) {
          if (! $withTrashed) {
              return $builder->withoutTrashed();
          }

          return $builder->withoutGlobalScope($this);
      });
  }
  ```

  `onlyTrashed()` 同樣是排除自身後再加上 `whereNotNull('deleted_at')`。

  ```php theme={null}
  protected function addOnlyTrashed(Builder $builder)
  {
      $builder->macro('onlyTrashed', function (Builder $builder) {
          $model = $builder->getModel();

          $builder->withoutGlobalScope($this)->whereNotNull(
              $model->getQualifiedDeletedAtColumn()
          );

          return $builder;
      });
  }
  ```
</Accordion>

<Tip>
  `Scope` 介面並未定義 `extend` 方法，但 Eloquent 的 Builder 若發現 Scope 具備 `extend` 方法就會自動呼叫。可用於新增自訂 macro。
</Tip>

## 實務使用情境

### 多租戶：以租戶 ID 自動篩選

SaaS 應用程式中，對所有查詢自動套用租戶 ID 篩選相當重要。

```php theme={null}
<?php

namespace App\Models\Scopes;

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

class TenantScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        if ($tenantId = auth()->user()?->tenant_id) {
            $builder->where('tenant_id', $tenantId);
        }
    }
}
```

```php theme={null}
use App\Models\Scopes\TenantScope;
use Illuminate\Database\Eloquent\Attributes\ScopedBy;

#[ScopedBy([TenantScope::class])]
class Post extends Model
{
    //
}
```

如此只要呼叫 `Post::all()` 就會只回傳驗證中使用者的租戶資料。

### 公開/非公開篩選

於管理後台希望顯示未公開的文章，但於前端僅顯示已公開者的情境。

```php theme={null}
<?php

namespace App\Models\Scopes;

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

class PublishedScope implements Scope
{
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('status', 'published')
                ->where('published_at', '<=', now());
    }
}
```

於管理後台以 `withoutGlobalScope` 排除 Scope。

```php theme={null}
// 前端：僅顯示已公開（自動套用 PublishedScope）
$posts = Post::latest()->get();

// 管理後台：顯示所有文章
$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();
```

### 使用 `addSelect` 而非 `select`

<Warning>
  於 Global Scope 中加入欄位時，請使用 `addSelect` 而非 `select`。使用 `select` 會覆寫呼叫端查詢中 `select` 的欄位。

  ```php theme={null}
  // 錯誤範例：覆寫呼叫端的 select
  public function apply(Builder $builder, Model $model): void
  {
      $builder->select('id', 'tenant_id', 'name');
  }

  // 正確範例：在既有 select 上追加
  public function apply(Builder $builder, Model $model): void
  {
      $builder->addSelect('tenant_id');
  }
  ```
</Warning>

## 下一步

<Card title="Eloquent 自訂 Cast" icon="wand-magic-sparkles" href="/zh-TW/advanced/eloquent-casts">
  學習將屬性轉換邏輯實作為自訂 Cast，並活用 Value Object 模式的方法。
</Card>


## Related topics

- [Collection 的 Higher Order Messages](/zh-TW/advanced/higher-order-messages.md)
- [Laravel Pennant](/zh-TW/pennant.md)
- [Eloquent Factories](/zh-TW/eloquent-factories.md)
- [Laravel Sanctum（API token 認證）](/zh-TW/sanctum.md)
- [Eloquent 入門](/zh-TW/eloquent.md)
