> ## 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 作用域

> 讲解本地作用域与全局作用域的机制。结合 SoftDeletingScope 等框架内部实现的解读，介绍多租户、公开/非公开筛选等实战用例。

## 什么是作用域

Eloquent 的作用域是将查询约束打包并可复用的机制。作用域分两类。

| 类型        | 应用时机     | 用途                |
| --------- | -------- | ----------------- |
| **全局作用域** | 始终自动应用   | 软删除、多租户、公开筛选      |
| **本地作用域** | 显式调用时才应用 | 「热门文章」「活跃用户」等通用筛选 |

## 本地作用域

### 定义

本地作用域通过给模型方法加上 `#[Scope]` 属性来定义。

```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]` 属性位于 `Illuminate\Database\Eloquent\Attributes` 命名空间，是 PHP 8.0 之后的原生语法。
</Info>

### 使用

已定义的作用域可以像方法一样调用，也可以链式使用。

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

// 仅获取已发布的文章
$posts = Post::published()->get();

// 获取已发布且热门的文章，按最新排序
$posts = Post::published()->popular()->latest()->get();
```

### 传递参数

可以在作用域方法的第 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` 连接作用域时，有时需要逻辑分组。

```php theme={null}
// 使用闭包（可靠但冗长）
$users = User::popular()->orWhere(function (Builder $query) {
    $query->active();
})->get();

// 使用高阶方法可以写得更简洁
$users = User::popular()->orWhere->active()->get();
```

## 全局作用域

### 机制

全局作用域是实现了 `Illuminate\Database\Eloquent\Scope` 接口的类。该接口仅要求实现 `apply` 一个方法。

```php theme={null}
// 框架本体的接口定义
// src/Illuminate/Database/Eloquent/Scope.php

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

在 `apply` 方法中给查询构造器附加约束。

### 创建全局作用域类

使用 `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
{
    /**
     * 将作用域应用到查询构造器
     */
    public function apply(Builder $builder, Model $model): void
    {
        $builder->where('is_active', true);
    }
}
```

### 应用到模型

<Steps>
  <Step title="使用 #[ScopedBy] 属性注册（推荐）">
    Laravel 13 中使用 `#[ScopedBy]` 属性最简洁。

    ```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
    {
        //
    }
    ```

    可以将多个作用域以数组形式指定。

    ```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>

添加全局作用域后，`User::all()` 等所有查询都会自动附加 `WHERE is_active = 1`。

### 匿名闭包形式的全局作用域

对于不需要单独放在文件里的简单作用域，可以用闭包定义。

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

<Warning>
  要移除以闭包定义的作用域，必须使用作用域名（字符串），而不是类名。
</Warning>

### 移除全局作用域

在特定查询中你可能希望禁用作用域。

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

// 移除特定作用域
User::withoutGlobalScope(ActiveScope::class)->get();

// 移除以闭包定义的作用域
User::withoutGlobalScope('active')->get();

// 移除所有全局作用域
User::withoutGlobalScopes()->get();

// 移除多个作用域
User::withoutGlobalScopes([ActiveScope::class, TenantScope::class])->get();

// 除指定作用域外全部移除
User::withoutGlobalScopesExcept([TenantScope::class])->get();
```

## 框架内部：SoftDeletingScope

看看 Laravel 标准的 `SoftDeletes` trait 如何使用全局作用域，就能了解实现模式。

`SoftDeletingScope` 实现了 `Scope` 接口。

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

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

    /**
     * 将作用域应用到查询构造器
     * 添加只取 deleted_at 为 NULL 的记录的约束
     */
    public function apply(Builder $builder, Model $model)
    {
        $builder->whereNull($model->getQualifiedDeletedAtColumn());
    }

    /**
     * 向查询构造器扩展宏
     * 让 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 会自动调用作用域上存在的 `extend` 方法。要添加自定义宏时可以利用它。
</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` 移除作用域。

```php theme={null}
// 前台：仅显示已发布（自动应用 PublishedScope）
$posts = Post::latest()->get();

// 管理后台：显示全部文章
$posts = Post::withoutGlobalScope(PublishedScope::class)->latest()->get();
```

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

<Warning>
  在全局作用域中添加列时请使用 `addSelect` 而不是 `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 自定义类型转换" icon="wand-magic-sparkles" href="/zh/advanced/eloquent-casts">
  将属性转换逻辑实现为自定义 Cast，并结合 Value Object 模式使用。
</Card>


## Related topics

- [PHP 属性](/zh/advanced/php-attributes.md)
- [Laravel Socialite（社交登录）](/zh/socialite.md)
- [Laravel Pennant](/zh/pennant.md)
- [Context（上下文）](/zh/context.md)
- [Eloquent Bootable Traits](/zh/advanced/eloquent-bootable-traits.md)
