iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 16

Day 16:Event/Listener——登入事件怎麼靠自動探索變成一筆審計紀錄

  • 分享至 

  • xImage
  •  

前言:這支 Listener 從來沒被手動註冊過,它是怎麼被觸發的?

「Listener 不是要在 EventServiceProvider 裡把 Event 跟 Listener 對應起來才會生效嗎?」

今天要看的這支 Listener,全專案搜尋找不到任何手動註冊它的地方——沒有 Event::listen(...),也沒有在 $listen 陣列裡登記。但它確實會在使用者登入時被觸發,寫進一筆審計紀錄。這是 Laravel 一個容易被忽略的機制:事件自動探索

今日目標

  • 認識 Laravel 內建的 Illuminate\Auth\Events\Login 事件
  • 理解 Laravel 的事件自動探索機制怎麼運作,為什麼不用手動註冊
  • 看一支 Listener 怎麼把登入行為手動組成一筆審計紀錄
  • 分辨「自動攔截的審計」跟「手動組出的審計」是兩種不同層次的用法

本文主體

一支從沒被手動接線過的 Listener

這支 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「資訊就近好維護」,這裡的自動探索機制,選的是後者。

Listener 裡在做什麼:手動組一筆審計紀錄

handle() 方法拿到 Login 事件之後,從事件裡取出登入的使用者,手動組出一筆稽核紀錄,寫進系統用的稽核套件(owen-it/laravel-auditing)的資料表。這裡值得注意的是「手動組」這三個字——這個套件原生的能力,是自動攔截 Model 的 CRUD 操作(新增、修改、刪除),只要 Model 實作了對應的介面,套件自己就會在背景記錄每一次異動。

但「使用者登入」這件事,根本不是一次 Model 的 CRUD 操作,套件原生的自動攔截機制完全碰不到它。這支 Listener 做的事情,是繞過套件的自動機制,手動組出一筆符合套件資料格式的稽核紀錄,塞進同一張稽核紀錄表——讓「登入」這種原本不在套件涵蓋範圍內的行為,也能出現在同一份審計軌跡裡。

兩種審計,兩種層次

系統裡實際上有兩種審計在同時運作:

  1. 自動攔截的審計:套件原生機制,只要 Model 實作對應介面,新增/修改/刪除操作自動被記錄,不需要手動介入
  2. 手動組出的審計:像這支登入 Listener,套件本身管不到的事件,靠額外寫一段邏輯,手動塞進同一份稽核記錄

這兩種審計解決的是不同層次的問題:前者處理「資料被怎麼動過」,後者處理「套件原生涵蓋不到、但業務上仍然重要的行為軌跡」。一個套件的能力邊界在哪裡、超出邊界的部分怎麼延伸涵蓋,是選用任何第三方套件時都該想清楚的問題——不是套件辦不到就放棄記錄,而是想辦法用套件本身的資料結構,把套件管不到的事件也塞進同一套追蹤機制裡。

對照:依賴套件原生機制 vs 手動延伸涵蓋範圍

套件原生機制管不到就放棄,登入行為完全沒有留下軌跡

// 沒有任何處理,登入這件事完全不在審計範圍內
// 出事時只能翻伺服器層級的存取 log,而且格式跟業務審計記錄不一致

手動組出符合套件格式的紀錄,延伸涵蓋範圍

class AuditLogin
{
    public function handle(Login $event): void
    {
        // 手動組出一筆跟套件原生格式一致的稽核紀錄
        // 讓「誰在什麼時候登入」也能跟其他 CRUD 審計記錄查在同一個地方
    }
}

今日思考題

你的專案裡用的第三方套件,有沒有哪些「原生涵蓋不到,但業務上你其實在乎」的邊角案例?如果要延伸套件的能力去涵蓋這些案例,你會選擇「手動組出符合套件格式的資料塞進去」,還是「另外開一套獨立的記錄機制」?兩者的維護成本差在哪裡?

今日重點回顧

  • Laravel 的事件自動探索機制,只要 Listener 的 handle() 方法有明確型別提示,不用手動註冊也會被觸發
  • 自動探索的代價是少了一份「集中查看所有事件接線」的清單,資訊分散在各個 Listener 檔案裡
  • 系統裡的稽核套件原生只會自動攔截 Model 的 CRUD 操作,「登入」這種事件完全不在它的自動涵蓋範圍
  • 這支 Listener 手動組出符合套件格式的紀錄,把套件管不到的行為也塞進同一份審計軌跡裡
  • 一個套件的能力邊界在哪裡、怎麼手動延伸涵蓋業務上在乎的邊角案例,是選用任何第三方套件都該想清楚的問題

明日預告

明天要進入這個系列的第三部:跟外部系統整合的部分。先從 Service Container 怎麼把一個 SOAP Client 用 Factory 綁定進容器開始——bindsingleton 該怎麼選,背後有清楚的理由可以推敲。


上一篇
Day 15:Observer 兩種註冊方式對照——PHP 8 Attribute vs ServiceProvider
下一篇
Day 17:Service Container 實戰——SOAP Client 怎麼用 Factory 綁定
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言