> ## 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 13 的事件註冊與 event:cache 實作，解說套件的明確註冊、與自動偵測的重複註冊、相依性解析，以及更新時的驗證方法。

想為套件提供事件驅動的擴充點時，要將**事件類別的提供**與**監聽器的註冊**分開設計。若採用即使使用者未將 `vendor` 內的目錄加入自動偵測也能運作的結構，就能減少安裝、更新與建立快取時的前提條件。

本頁會確認 Laravel Framework `v13.36.0` 的實作，將[事件](/zh-TW/events)的基本操作套用到套件的長期維護上。提供者的載入請參閱[套件自動偵測](/zh-TW/advanced/package-discovery)，測試環境請參閱[套件測試](/zh-TW/advanced/package-testing)。

## 明確註冊套件的監聽器

套件預設執行的處理，可以在一般 `Illuminate\Support\ServiceProvider` 的 `boot()` 中以 `Event::listen()` 註冊。Laravel 預設的監聽器自動偵測位置是應用程式的 `app/Listeners`，並不會自動掃描到套件內的 `src/Listeners`。

以下以將配送完成事件記錄到日誌的套件為例。事件只是一個持有通知所需值的類別。

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

namespace Acme\Courier\Events;

final class ShipmentDelivered
{
    public function __construct(public readonly string $shipmentId)
    {
    }
}
```

監聽器在建構子中接收 logger，並在 `handle()` 中處理事件。

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

namespace Acme\Courier\Listeners;

use Acme\Courier\Events\ShipmentDelivered;
use Psr\Log\LoggerInterface;

final class LogDeliveredShipment
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function handle(ShipmentDelivered $event): void
    {
        $this->logger->info('Shipment delivered', [
            'shipment_id' => $event->shipmentId,
        ]);
    }
}
```

在提供者中註冊事件與監聽器的類別名稱。

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

namespace Acme\Courier;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Listeners\LogDeliveredShipment;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;

class CourierServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Event::listen(
            ShipmentDelivered::class,
            LogDeliveredShipment::class,
        );
    }
}
```

此結構的前提是提供者本身會被載入。請準備 Composer 的 `extra.laravel.providers`，或由使用者明確註冊。若事件在 HTTP 處理與佇列 worker 中都需要，就不要以 `runningInConsole()` 限制註冊。

<Warning>
  請勿僅因為要註冊監聽器，就讓此提供者實作 `DeferrableProvider`。若延遲對象的服務未被解析，監聽器也不會被註冊。較重的服務繫結請視需要拆分到另一個[延遲提供者](/zh-TW/advanced/deferred-provider)。
</Warning>

## 註冊與實例建立發生在不同時機

`Dispatcher::listen()` 會將針對一般事件名稱的監聽器加入內部陣列。像上面那樣傳入類別名稱時，此時並不會建立監聽器的實例。

對於同步監聽器，事件觸發後 `createClassCallable()` 會使用容器的 `make()` 解析類別。因此可以利用建構子的相依性注入。

```mermaid theme={null}
flowchart TD
    A["提供者的 boot()"] --> B["以 Event::listen() 儲存類別名稱"]
    B --> C["觸發 ShipmentDelivered"]
    C --> D["Dispatcher 選取監聽器"]
    D --> E["由容器解析同步監聽器"]
    E --> F["將事件傳給 handle()"]
```

此路徑並不會對 `handle()` 執行容器的 `call()`。請採用**將任意服務注入建構子，並在 `handle()` 中接收事件**的設計。不要將其視為與 [Artisan 指令對 `handle()` 的相依性注入](/zh-TW/advanced/package-commands)相同的機制。

這裡說明的是同步監聽器。實作 `ShouldQueue` 時會走佇列專用的另一條路徑，因此關於交易、重試與相依性解析，也請確認[事件的佇列處理](/zh-TW/events)與[佇列](/zh-TW/queues)。

## event:cache 不會停止明確註冊

`event:cache` 並不是將事件 Dispatcher 中已註冊的所有監聽器整個序列化的指令。

`EventCacheCommand::getEvents()` 會列舉 `Illuminate\Foundation\Support\Providers\EventServiceProvider` 及其衍生提供者，並儲存各提供者的自動偵測結果與 `listens()` 的對應。寫在一般 `ServiceProvider::boot()` 中的 `Event::listen()` 不在此收集範圍內。

另一方面，即使在有快取的啟動中，一般提供者的 `boot()` 仍會執行。**未包含在快取中，與有快取時不會被註冊，是兩回事。**

| 註冊方式 | `event:cache` 儲存的內容 | 有快取時的啟動 |
| - | - | - |
| 在一般提供者的 `boot()` 中使用 `Event::listen()` | 此註冊本身不在收集範圍內 | 在 `boot()` 中再次註冊 |
| Foundation `EventServiceProvider` 的 `$listen` | 依提供者的類別名稱儲存對應 | `getEvents()` 讀取快取 |
| 成為自動偵測對象的監聽器 | 將偵測結果儲存到對應中 | 使用快取，而非重新掃描目錄 |

也可以在繼承 Foundation `EventServiceProvider` 的套件中使用 `$listen`。不過，在確認過的實作中，`shouldDiscoverEvents()` 的預設值是針對該基底類別本身的判斷。**僅僅繼承，並不會讓該套件的監聽器目錄被自動偵測。**

此外，該提供者的 `$subscribe` 與 `$observers` 是在註冊 `getEvents()` 結果的迴圈之外另行處理。請勿只看 `$listen` 的快取，就判斷這些註冊也會被省略。

<Tip>
  `Event::subscribe(SomeSubscriber::class)` 會在註冊時由容器建立訂閱者並呼叫 `subscribe()`。它與以類別名稱進行的 `Event::listen()` 並不具有相同的延遲性。請避免在訂閱者的建構子或 `subscribe()` 中產生外部通訊等副作用。
</Tip>

## 避免與自動偵測重複註冊

若使用者以 `withEvents(discover: [...])` 加入套件的監聽器目錄，而套件的提供者也明確註冊了相同的監聽器，一個事件可能會讓處理執行多次。

`Dispatcher::listen()` 對一般事件會進行陣列的附加，並不會在整個 Dispatcher 中自動移除重複的類別名稱。Foundation `EventServiceProvider` 在自身對應中進行的 `array_unique()`，也不是用來合併其他提供者明確註冊的機制。

請將註冊責任交由其中一方負責。

* **由套件註冊預設監聽器**時，不要求使用者將該目錄加入自動偵測。
* **由使用者選擇所需監聽器**時，套件提供事件與監聽器類別，且不預設註冊相同的監聽器。
* 從自動偵測轉移到明確註冊時，也要將使用者端的偵測設定與舊的事件快取列為需要更新的對象並加以說明。

業務處理的冪等性也很重要，但不要在保留重複註冊的情況下僅以冪等性掩蓋問題，而應整理註冊路徑。

## 驗證更新前後的行為

要檢查註冊狀態，請在已安裝套件的驗證用應用程式中執行 `event:list`。確認在無快取與有快取兩種情況下，目標監聽器都存在。

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

這是在驗證用應用程式中的步驟。僅憑 `event:list` 的顯示，無法保證監聽器會完成處理，也無法保證只執行一次。請另外加入實際觸發事件的測試。

以下範例在 [Testbench 的基底 TestCase](/zh-TW/advanced/package-testing) 中註冊 `CourierServiceProvider` 後，驗證 logger 只被呼叫一次。

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

namespace Acme\Courier\Tests\Feature;

use Acme\Courier\Events\ShipmentDelivered;
use Acme\Courier\Tests\TestCase;
use Illuminate\Support\Facades\Event;
use Psr\Log\LoggerInterface;

class PackageEventsTest extends TestCase
{
    public function test_delivery_is_logged_once(): void
    {
        $logger = $this->createMock(LoggerInterface::class);
        $logger->expects($this->once())
            ->method('info')
            ->with('Shipment delivered', ['shipment_id' => 'shipment-123']);

        $this->app->instance(LoggerInterface::class, $logger);

        Event::dispatch(new ShipmentDelivered('shipment-123'));
    }
}
```

`Event::fake()` 便於確認事件是否被觸發，但通常會停止真正監聽器的執行。此測試要驗證註冊與處理，因此不使用它。與自動偵測的組合以及有快取的啟動，也請在另外的驗證用應用程式、新的應用程式實例中確認。

<Warning>
  即使在啟動後執行 `event:cache`，該程序內已註冊的 Dispatcher 也不會從快取重新建構。有快取時的行為確認，請在新的啟動中進行。若要讓更新後的監聽器反映到長時間運作的 worker，也需要依照一般部署流程重新啟動。
</Warning>

## 長期維護的檢查清單

* 將事件類別名稱、公開屬性與監聽器的註冊方針，作為面向使用者的約定寫入文件。
* 變更使用者所訂閱事件的名稱或 payload 時，依照[版本相容性管理](/zh-TW/advanced/package-versioning)說明變更內容。
* 對於自動偵測或 `$listen` 有變更的更新，將重新建立 `event:cache` 納入部署流程。
* 區分套件的預設監聽器與使用者的自訂監聽器，避免重複註冊相同的預設監聽器。
* 針對每個支援的 PHP 與 Laravel 版本，持續執行確認監聽器只執行一次的測試。

## 參考的第一手資料

* [Laravel 官方文件：事件註冊、自動偵測、快取、訂閱者](https://github.com/laravel/docs/blob/73e61270287e4aaa1cecd77cd66aa6ef4f499828/events.md)
* [Dispatcher: listen、subscribe、createClassCallable](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Events/Dispatcher.php)
* [Foundation EventServiceProvider: register、getEvents、shouldDiscoverEvents](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php)
* [EventCacheCommand: 收集快取對象](https://github.com/laravel/framework/blob/v13.36.0/src/Illuminate/Foundation/Console/EventCacheCommand.php)


## Related topics

- [進階主題](/zh-TW/advanced/index.md)
- [套件的路由註冊與快取](/zh-TW/advanced/package-routes.md)
- [事件與監聽器](/zh-TW/events.md)
- [請求生命週期](/zh-TW/lifecycle.md)
- [Eloquent Observer 與模型事件](/zh-TW/advanced/eloquent-observers.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.