「Listener 不是要在 EventServiceProvider 裡把 Event 跟 Listener 對應起來才會生效嗎?」
今天要看的這支 Listener,全專案搜尋找不到任何手動註冊它的地方——沒有 Event::listen(...),也沒有在 $listen 陣列裡登記。但它確實會在使用者登入時被觸發,寫進一筆審計紀錄。這是 Laravel 一個容易被忽略的機制:事件自動探索。
Illuminate\Auth\Events\Login 事件這支 Listener 的職責很單純:監聽 Laravel 內建的登入事件,在使用者成功登入時,寫一筆審計紀錄,記下是誰、什麼時候登入的。它的 handle() 方法型別提示了 Illuminate\Auth\Events\Login 這個事件類別:
class AuditLogin
{
public function handle(Login $event): void
{
// 組出一筆審計紀錄,寫進稽核套件的資料表
}
}
如果照著多數教學文件的印象,這裡應該還要在 EventServiceProvider 的 $listen 屬性裡,把 Login::class 對應到 AuditLogin::class,事件才會真的觸發這支 Listener。但翻遍整個專案,找不到任何一行手動接線的程式碼。
Laravel 有一個容易被忽略的機制:只要 Listener 放在 app/Listeners/ 目錄底下,handle() 方法有明確的型別提示(就像上面這支 Listener 提示了 Login $event),框架在啟動時會自動掃描這個目錄,讀出每支 Listener 的方法簽章,自動把它跟對應型別的事件連結起來——完全不需要在任何地方手動寫一行 Event::listen()。
這個機制解決的問題很實際:一個系統裡如果有幾十個 Event/Listener 配對,全部塞進 EventServiceProvider 的 $listen 陣列,這份清單會變得又長又難維護,而且新增一個 Listener 時,很容易忘記回頭去登記。自動探索把這份工作交給框架,代價是少了一個「集中查看所有事件接線」的地方——想知道系統裡有哪些事件被監聽,得靠翻 app/Listeners/ 目錄底下每一支類別的型別提示,而不是看一份清單。
這跟昨天講的 Observer 兩種註冊方式,其實是同一種取捨的不同版本:「資訊集中好總覽」vs「資訊就近好維護」,這裡的自動探索機制,選的是後者。
handle() 方法拿到 Login 事件之後,從事件裡取出登入的使用者,手動組出一筆稽核紀錄,寫進系統用的稽核套件(owen-it/laravel-auditing)的資料表。這裡值得注意的是「手動組」這三個字——這個套件原生的能力,是自動攔截 Model 的 CRUD 操作(新增、修改、刪除),只要 Model 實作了對應的介面,套件自己就會在背景記錄每一次異動。
但「使用者登入」這件事,根本不是一次 Model 的 CRUD 操作,套件原生的自動攔截機制完全碰不到它。這支 Listener 做的事情,是繞過套件的自動機制,手動組出一筆符合套件資料格式的稽核紀錄,塞進同一張稽核紀錄表——讓「登入」這種原本不在套件涵蓋範圍內的行為,也能出現在同一份審計軌跡裡。
系統裡實際上有兩種審計在同時運作:
這兩種審計解決的是不同層次的問題:前者處理「資料被怎麼動過」,後者處理「套件原生涵蓋不到、但業務上仍然重要的行為軌跡」。一個套件的能力邊界在哪裡、超出邊界的部分怎麼延伸涵蓋,是選用任何第三方套件時都該想清楚的問題——不是套件辦不到就放棄記錄,而是想辦法用套件本身的資料結構,把套件管不到的事件也塞進同一套追蹤機制裡。
❌ 套件原生機制管不到就放棄,登入行為完全沒有留下軌跡
// 沒有任何處理,登入這件事完全不在審計範圍內
// 出事時只能翻伺服器層級的存取 log,而且格式跟業務審計記錄不一致
✅ 手動組出符合套件格式的紀錄,延伸涵蓋範圍
class AuditLogin
{
public function handle(Login $event): void
{
// 手動組出一筆跟套件原生格式一致的稽核紀錄
// 讓「誰在什麼時候登入」也能跟其他 CRUD 審計記錄查在同一個地方
}
}
你的專案裡用的第三方套件,有沒有哪些「原生涵蓋不到,但業務上你其實在乎」的邊角案例?如果要延伸套件的能力去涵蓋這些案例,你會選擇「手動組出符合套件格式的資料塞進去」,還是「另外開一套獨立的記錄機制」?兩者的維護成本差在哪裡?
handle() 方法有明確型別提示,不用手動註冊也會被觸發明天要進入這個系列的第三部:跟外部系統整合的部分。先從 Service Container 怎麼把一個 SOAP Client 用 Factory 綁定進容器開始——bind 跟 singleton 該怎麼選,背後有清楚的理由可以推敲。