> ## 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 Observer 與模型事件

> 解說 Eloquent 模型觸發事件的機制，以及使用 Observer 類別統一管理事件的方法。也包含 `#[ObservedBy]` attribute 等 Laravel 13 的最新功能。

## 什麼是模型事件

Eloquent 模型會在生命週期的各時機自動觸發事件。透過 hook 這些事件，可以在模型的儲存、刪除等前後插入處理。

Eloquent 所觸發的事件如下。

| 事件              | 觸發時機               |
| --------------- | ------------------ |
| `retrieved`     | 從 DB 取得模型時         |
| `creating`      | 儲存新模型的前一刻          |
| `created`       | 儲存新模型後             |
| `updating`      | 更新既有模型的前一刻         |
| `updated`       | 更新既有模型後            |
| `saving`        | 新建立或更新任一者儲存的前一刻    |
| `saved`         | 新建立或更新任一者儲存後       |
| `deleting`      | 刪除模型的前一刻           |
| `deleted`       | 刪除模型後              |
| `trashed`       | 軟刪除後               |
| `forceDeleting` | 實體刪除的前一刻           |
| `forceDeleted`  | 實體刪除後              |
| `restoring`     | 復原軟刪除的前一刻          |
| `restored`      | 復原軟刪除後             |
| `replicating`   | 呼叫 `replicate()` 時 |

以 `-ing` 結尾的事件在變更**寫入 DB 前**觸發，以 `-ed` 結尾則是**寫入後**觸發。

<Warning>
  批次更新或批次刪除（如 `User::where(...)->update(...)`）不會觸發 `saving`、`saved`、`updating`、`updated`、`deleting`、`deleted` 事件，因為模型實際上並未被取得。
</Warning>

## 使用 Closure 的事件監聽器

若想簡單處理事件，可於模型的 `booted` 方法中註冊 closure。

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

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    protected static function booted(): void
    {
        static::created(function (User $user) {
            // 使用者建立後執行的處理
        });

        static::deleting(function (User $user) {
            // 使用者刪除前執行的處理
        });
    }
}
```

若希望以 Queue 非同步執行處理，可使用 `queueable` helper。

```php theme={null}
use function Illuminate\Events\queueable;

static::created(queueable(function (User $user) {
    // 於 Queue 中非同步執行
}));
```

## `$dispatchesEvents` 屬性

若要與 Laravel 的事件系統整合，可透過 `$dispatchesEvents` 屬性將模型事件對應至自訂事件類別。

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

namespace App\Models;

use App\Events\UserDeleted;
use App\Events\UserSaved;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    /**
     * 模型事件與事件類別的對應
     *
     * @var array<string, string>
     */
    protected $dispatchesEvents = [
        'saved' => UserSaved::class,
        'deleted' => UserDeleted::class,
    ];
}
```

對應的事件類別會在建構函式接收模型實例。

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

namespace App\Events;

use App\Models\User;

class UserSaved
{
    public function __construct(
        public readonly User $user,
    ) {}
}
```

## 建立 Observer 類別

當要對單一模型處理多個事件時，比起列舉 closure，將其彙整為 Observer 類別更為簡潔。

<Steps>
  <Step title="以 Artisan 指令產生類別">
    以 `make:observer` 指令產生範本。加上 `--model` 選項指定模型，則會自動加入對應的方法。

    ```bash theme={null}
    php artisan make:observer UserObserver --model=User
    ```

    會產生 `app/Observers/UserObserver.php`。
  </Step>

  <Step title="實作各事件的方法">
    方法名稱對應事件名稱。引數會傳入模型實例。

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

    namespace App\Observers;

    use App\Models\User;

    class UserObserver
    {
        public function created(User $user): void
        {
            // 使用者建立後的處理
        }

        public function updated(User $user): void
        {
            // 使用者更新後的處理
        }

        public function deleted(User $user): void
        {
            // 使用者刪除後的處理
        }

        public function restored(User $user): void
        {
            // 軟刪除復原後的處理
        }

        public function forceDeleted(User $user): void
        {
            // 實體刪除後的處理
        }
    }
    ```
  </Step>

  <Step title="於 Model 註冊 Observer">
    註冊方式有兩種。在 Laravel 13 中推薦使用 `#[ObservedBy]` attribute。

    **方式 1：`#[ObservedBy]` attribute（推薦）**

    僅需於模型類別加上 attribute 即可完成註冊，無需變更 `AppServiceProvider`。

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

    namespace App\Models;

    use App\Observers\UserObserver;
    use Illuminate\Database\Eloquent\Attributes\ObservedBy;
    use Illuminate\Foundation\Auth\User as Authenticatable;

    #[ObservedBy(UserObserver::class)]
    class User extends Authenticatable
    {
        //
    }
    ```

    要註冊多個 Observer，可重複加 attribute 或以陣列傳入。

    ```php theme={null}
    #[ObservedBy(UserObserver::class)]
    #[ObservedBy(AuditObserver::class)]
    class User extends Authenticatable
    {
        //
    }
    ```

    **方式 2：於 `AppServiceProvider` 註冊**

    在 `AppServiceProvider` 的 `boot` 方法中呼叫 `observe`。

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

    namespace App\Providers;

    use App\Models\User;
    use App\Observers\UserObserver;
    use Illuminate\Support\ServiceProvider;

    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            User::observe(UserObserver::class);
        }
    }
    ```
  </Step>
</Steps>

<Info>
  `#[ObservedBy]` attribute 位於 `Illuminate\Database\Eloquent\Attributes` 命名空間。是 PHP 8.0 以後的原生語法，於 Laravel 13 積極被採用。
</Info>

## 資料庫交易內的 Observer

當模型於交易內建立或更新時，有時希望在交易 commit 後才執行 Observer。可透過實作 `ShouldHandleEventsAfterCommit` 介面達成此行為。

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

namespace App\Observers;

use App\Models\User;
use Illuminate\Contracts\Events\ShouldHandleEventsAfterCommit;

class UserObserver implements ShouldHandleEventsAfterCommit
{
    public function created(User $user): void
    {
        // 於交易 commit 後執行
    }
}
```

若在交易外執行，則會如往常立即執行。

## 暫時停用事件

### 以 `withoutEvents` 僅為特定處理停止事件

於傳入 `User::withoutEvents()` 的 closure 中，所有模型事件都不會觸發。

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

$user = User::withoutEvents(function () {
    User::findOrFail(1)->delete();

    return User::find(2);
});
```

### 以 `saveQuietly` 停止儲存時的事件

若希望在不觸發事件的情況下儲存模型，可使用 `saveQuietly`。

```php theme={null}
$user = User::findOrFail(1);

$user->name = 'Victoria Faith';

$user->saveQuietly();
```

刪除、復原、複製也提供了同樣的方法。

```php theme={null}
$user->deleteQuietly();
$user->restoreQuietly();
```

## 實務使用情境

### 快取的自動清除

當模型被更新或刪除時，自動清除相關的快取。

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

namespace App\Observers;

use App\Models\Post;
use Illuminate\Support\Facades\Cache;

class PostObserver
{
    public function saved(Post $post): void
    {
        Cache::forget("post:{$post->id}");
        Cache::forget('posts:latest');
    }

    public function deleted(Post $post): void
    {
        Cache::forget("post:{$post->id}");
        Cache::forget('posts:latest');
    }
}
```

### 稽核日誌的記錄

自動記錄模型的變更歷史。可用 `getDirty()` 取得變更前後的值。

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

namespace App\Observers;

use App\Models\AuditLog;
use App\Models\User;

class UserObserver
{
    public function updating(User $user): void
    {
        AuditLog::create([
            'model_type' => User::class,
            'model_id'   => $user->id,
            'changes'    => $user->getDirty(),
            'user_id'    => auth()->id(),
        ]);
    }

    public function deleted(User $user): void
    {
        AuditLog::create([
            'model_type' => User::class,
            'model_id'   => $user->id,
            'changes'    => ['deleted' => true],
            'user_id'    => auth()->id(),
        ]);
    }
}
```

<Tip>
  由於 `updating` 事件於 DB 儲存**前**觸發，可以透過 `getDirty()` 取得預定變更的值。若於 `updated` 事件後呼叫，`getDirty()` 會變為空。
</Tip>

### 關聯模型的自動更新

以下為訂單完成時自動更新庫存數量的範例。

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

namespace App\Observers;

use App\Models\Order;

class OrderObserver
{
    public function created(Order $order): void
    {
        foreach ($order->items as $item) {
            $item->product->decrement('stock', $item->quantity);
        }
    }

    public function deleted(Order $order): void
    {
        foreach ($order->items as $item) {
            $item->product->increment('stock', $item->quantity);
        }
    }
}
```

## 下一步

<Card title="進階：PHP Attributes" icon="tag" href="/zh-TW/advanced/php-attributes">
  一併學習包含 `#[ObservedBy]` 的 Laravel 13 PHP Attributes。
</Card>


## Related topics

- [Eloquent 的自訂 Cast](/zh-TW/advanced/eloquent-casts.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [Eloquent 入門](/zh-TW/eloquent.md)
- [事件與監聽器](/zh-TW/events.md)
- [Eloquent Bootable Traits](/zh-TW/advanced/eloquent-bootable-traits.md)
