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

# 事件與監聽器

> 學習如何運用 Laravel 的事件系統，讓應用程式的元件保持鬆散耦合。

## 事件是什麼

Laravel 的事件系統是簡潔的觀察者模式實作。
你可以透過在應用程式中發生「事件」時觸發，並定義能對其做出反應的監聽器，將元件之間的相依性降到最低。

例如，發起「訂單成立」事件後，「發送確認信」、「扣減庫存」、「通知 Slack」等多個監聽器都能各自獨立地執行。
訂單處理程式碼完全不需要知道 email 或 Slack 通知的實作細節。

<Info>
  事件類別存放於 `app/Events`，監聽器類別存放於 `app/Listeners`。
  若不存在則 Artisan 指令會自動建立。
</Info>

```mermaid theme={null}
flowchart TD
    A["事件觸發<br>dispatch()"] --> B["事件派發器"]
    B --> C["監聽器 1<br>(同步)"]
    B --> D["監聽器 2<br>(同步)"]
    B --> E["監聽器 3<br>實作 ShouldQueue"]
    C --> F["立即執行"]
    D --> G["立即執行"]
    E --> H["進入佇列"]
    H --> I["worker 非同步執行"]
```

## 建立事件與監聽器

以 `make:event` 與 `make:listener` Artisan 指令產生類別骨架。

```bash theme={null}
php artisan make:event UserRegistered

php artisan make:listener SendWelcomeEmail --event=UserRegistered
```

不帶參數執行時會進入互動模式。

```bash theme={null}
php artisan make:event

php artisan make:listener
```

## 註冊事件

### 事件自動探索

預設情況下，Laravel 會掃描 `app/Listeners` 目錄自動註冊監聽器。
它會依 `handle` 或 `__invoke` 方法的引數型別推論與事件的對應。

```php theme={null}
use App\Events\UserRegistered;

class SendWelcomeEmail
{
    public function handle(UserRegistered $event): void
    {
        // 寄送歡迎信
    }
}
```

透過 PHP 的聯合型別，可讓單一方法接收多種事件。

```php theme={null}
public function handle(UserRegistered|UserUpdated $event): void
{
    // ...
}
```

若把監聽器放在其他目錄，可於 `bootstrap/app.php` 指定額外掃描位置。

```php theme={null}
->withEvents(discover: [
    __DIR__.'/../app/Domain/Orders/Listeners',
])
```

也可用 wildcard 指定多個目錄。

```php theme={null}
->withEvents(discover: [
    __DIR__.'/../app/Domain/*/Listeners',
])
```

要確認已註冊的監聽器可執行下列指令。

```bash theme={null}
php artisan event:list
```

<Tip>
  在正式環境快取監聽器 manifest 可提升效能。
  部署時可執行 `php artisan optimize` 或 `php artisan event:cache`。
  要清除快取則使用 `php artisan event:clear`。
</Tip>

### 手動註冊

也可以在 `AppServiceProvider` 的 `boot` 方法透過 `Event` Facade 手動註冊。

```php theme={null}
use App\Events\UserRegistered;
use App\Listeners\SendWelcomeEmail;
use Illuminate\Support\Facades\Event;

public function boot(): void
{
    Event::listen(
        UserRegistered::class,
        SendWelcomeEmail::class,
    );
}
```

也可以透過閉包註冊。

```php theme={null}
use App\Events\UserRegistered;
use Illuminate\Support\Facades\Event;

public function boot(): void
{
    Event::listen(function (UserRegistered $event) {
        // ...
    });
}
```

## 定義事件

事件類別是資料的容器。它不含邏輯，只以屬性儲存與事件相關的資訊。

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

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UserRegistered
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

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

透過 `SerializesModels` trait，可在佇列化的監聽器序列化事件時正確處理 Eloquent 模型。

## 觸發事件

透過 `dispatch` 靜態方法或 `event()` 輔助函式觸發事件。

```php theme={null}
use App\Events\UserRegistered;

// 靜態方法
UserRegistered::dispatch($user);

// 輔助函式
event(new UserRegistered($user));
```

也有依條件觸發的方法。

```php theme={null}
UserRegistered::dispatchIf($condition, $user);

UserRegistered::dispatchUnless($condition, $user);
```

### 資料庫 Transaction 之後才觸發

若要在 transaction commit 之後才觸發事件，可在事件類別實作 `ShouldDispatchAfterCommit` 介面。
若 transaction 失敗則事件會被丟棄。

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

namespace App\Events;

use App\Models\User;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Events\ShouldDispatchAfterCommit;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class UserRegistered implements ShouldDispatchAfterCommit
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

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

## 監聽器的實作

監聽器透過 `handle` 方法接收事件。
建構子中的相依會由服務容器自動注入。

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

namespace App\Listeners;

use App\Events\UserRegistered;
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail
{
    public function __construct() {}

    public function handle(UserRegistered $event): void
    {
        Mail::to($event->user->email)
            ->send(new WelcomeMail($event->user));
    }
}
```

若監聽器的 `handle` 方法回傳 `false`，可以中斷事件傳遞到後續監聽器。

## 佇列化的監聽器

寄信或 HTTP 請求等耗時處理，可作為佇列化的監聽器非同步執行。
只需實作 `ShouldQueue` 介面，事件觸發時監聽器就會自動進入佇列。

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

namespace App\Listeners;

use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;

class SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        // 此處理由佇列 worker 非同步執行
    }
}
```

<Info>
  使用佇列化的監聽器前，需先設定佇列並啟動 worker。
  詳情請見[佇列與工作](/zh-TW/queues)頁面。
</Info>

### 自訂佇列連線 / 名稱 / 延遲

可用 PHP 屬性設定連線、佇列名稱與延遲時間。

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

namespace App\Listeners;

use App\Events\UserRegistered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Delay;
use Illuminate\Queue\Attributes\Queue;

#[Connection('redis')]
#[Queue('emails')]
#[Delay(10)]
class SendWelcomeEmail implements ShouldQueue
{
    public function handle(UserRegistered $event): void
    {
        // ...
    }
}
```

也可以用方法動態決定值。

```php theme={null}
public function viaConnection(): string
{
    return 'redis';
}

public function viaQueue(): string
{
    return 'emails';
}

public function withDelay(UserRegistered $event): int
{
    return 10;
}
```

### 最大重試次數與逾時

使用 `#[Tries]` 與 `#[Timeout]` 屬性控制失敗時的行為。

```php theme={null}
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Attributes\Timeout;

#[Tries(3)]
#[Timeout(30)]
class SendWelcomeEmail implements ShouldQueue
{
    // ...
}
```

### 失敗時的處理

定義 `failed` 方法可以描述監聽器超過重試次數後失敗的後續處理。

```php theme={null}
use Throwable;

public function failed(UserRegistered $event, Throwable $exception): void
{
    // 通知管理員等
}
```

## 事件訂閱者

透過事件訂閱者，可以將多個相關事件處理程式集中在同一個類別。

### 建立訂閱者

在 `subscribe` 方法回傳事件與 handler 的對應陣列。

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

namespace App\Listeners;

use Illuminate\Auth\Events\Login;
use Illuminate\Auth\Events\Logout;
use Illuminate\Events\Dispatcher;

class UserActivitySubscriber
{
    public function handleLogin(Login $event): void
    {
        // 登入處理
    }

    public function handleLogout(Logout $event): void
    {
        // 登出處理
    }

    /**
     * @return array<string, string>
     */
    public function subscribe(Dispatcher $events): array
    {
        return [
            Login::class => 'handleLogin',
            Logout::class => 'handleLogout',
        ];
    }
}
```

### 註冊訂閱者

若事件自動探索啟用，`subscribe` 方法回傳陣列的訂閱者會自動註冊。
若要手動註冊，可在 `AppServiceProvider` 的 `boot` 方法呼叫 `Event::subscribe`。

```php theme={null}
use App\Listeners\UserActivitySubscriber;
use Illuminate\Support\Facades\Event;

public function boot(): void
{
    Event::subscribe(UserActivitySubscriber::class);
}
```

## 實戰範例：使用者註冊時寄送歡迎信

<Steps>
  <Step title="建立事件類別">
    ```bash theme={null}
    php artisan make:event UserRegistered
    ```

    編輯 `app/Events/UserRegistered.php`，加入用來保存註冊使用者的屬性。

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

    namespace App\Events;

    use App\Models\User;
    use Illuminate\Broadcasting\InteractsWithSockets;
    use Illuminate\Foundation\Events\Dispatchable;
    use Illuminate\Queue\SerializesModels;

    class UserRegistered
    {
        use Dispatchable, InteractsWithSockets, SerializesModels;

        public function __construct(
            public User $user,
        ) {}
    }
    ```
  </Step>

  <Step title="建立監聽器類別">
    ```bash theme={null}
    php artisan make:listener SendWelcomeEmail --event=UserRegistered
    ```

    為了讓寄信非同步進行，實作 `ShouldQueue`。

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

    namespace App\Listeners;

    use App\Events\UserRegistered;
    use App\Mail\WelcomeMail;
    use Illuminate\Contracts\Queue\ShouldQueue;
    use Illuminate\Support\Facades\Mail;

    class SendWelcomeEmail implements ShouldQueue
    {
        public function handle(UserRegistered $event): void
        {
            Mail::to($event->user->email)
                ->send(new WelcomeMail($event->user));
        }
    }
    ```
  </Step>

  <Step title="在控制器中觸發事件">
    在使用者註冊處理後呼叫 `UserRegistered::dispatch()`。

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

    namespace App\Http\Controllers\Auth;

    use App\Events\UserRegistered;
    use App\Models\User;
    use Illuminate\Http\RedirectResponse;
    use Illuminate\Http\Request;

    class RegisterController extends Controller
    {
        public function store(Request $request): RedirectResponse
        {
            $user = User::create($request->validated());

            UserRegistered::dispatch($user);

            return redirect('/dashboard');
        }
    }
    ```

    `RegisterController` 只需要觸發 `UserRegistered` 事件，不需要知道寄信的實作。
    未來如果加入「註冊時也通知 Slack」的需求，也完全不必修改控制器。
  </Step>

  <Step title="啟動 worker">
    要處理佇列化的監聽器，請啟動 worker。

    ```bash theme={null}
    php artisan queue:work
    ```
  </Step>
</Steps>

<Tip>
  在事件自動探索啟用時，不需要在 `AppServiceProvider` 手動註冊。
  放在 `app/Listeners` 目錄的監聽器會被自動偵測。
</Tip>

<Warning>
  執行 `php artisan event:list` 可以查看已註冊的事件與監聽器清單。
  請定期檢查是否有意料外的監聽器。
</Warning>


## Related topics

- [Laravel 11 以後的新應用程式結構 FAQ](/zh-TW/advanced/app-structure-faq.md)
- [Eloquent Observer 與模型事件](/zh-TW/advanced/eloquent-observers.md)
- [廣播](/zh-TW/broadcasting.md)
- [Contracts（契約）](/zh-TW/contracts.md)
- [請求生命週期](/zh-TW/lifecycle.md)
