「Eloquent Observer 不就是在 ServiceProvider 裡 Model::observe(SomeObserver::class) 一行搞定嗎?」
這是多數 Laravel 開發者對 Observer 註冊的第一印象——確實,這是最常見、文件範例裡最常出現的寫法。但今天要看的這個真實系統裡,同時存在兩種不同的 Observer 註冊方式,一個 Model 用了新式的 PHP 8 Attribute 語法,另一個 Model 用了傳統的 ServiceProvider 手動註冊。這不是誰對誰錯的問題,而是一個好機會,具體比較兩種寫法的差異跟各自適合的情境。
try/catch 包住內容轉換邏輯的理由#[ObservedBy])系統裡負責新聞內容的 Model,用的是這種寫法:
#[ObservedBy(NewsObserve::class)]
class News extends Model
{
// ...
}
註冊邏輯直接寫在 Model 類別本身的上方,作為一個 Attribute。好處很直接:看到 Model 就知道它掛了哪個 Observer,不用另外跑去 ServiceProvider 翻找。這是 Laravel 較新版本才支援的語法,把「這個 Model 有 Observer」這件事,從「藏在別的檔案裡的設定」變成「寫在 Model 自己身上的宣告」。
負責媒體檔案的 Model,用的是比較傳統的寫法,在 AppServiceProvider::boot() 裡:
Media::observe(MediaObserver::class);
這種寫法的資訊分散在兩個地方——要知道「Media 這個 Model 有沒有掛 Observer」,得先想到要去 ServiceProvider 裡找。但它也有自己的優勢:所有 Observer 的註冊集中在同一個地方,一眼就能看到整個系統掛了哪些 Observer,不用一個一個 Model 翻過去確認。
Attribute 語法讓資訊「就近可見」(打開 Model 就看到),ServiceProvider 集中註冊讓資訊「集中可見」(打開一個檔案看到全部)。這個系統裡兩種寫法並存,一個合理的解讀是:不同時期加進來的功能、或者不同開發者的習慣,並不代表故意設計成這樣的混合策略。這正是真實系統跟教學範例的差異之一——教學範例會示範一種「標準寫法」,真實系統裡往往是歷史演進留下的多種寫法並存。
如果要從中挑一個原則:一個新加入的 Model 如果只有一兩個 Observer,用 Attribute 語法直接寫在 Model 上,維護時比較不容易忘記它的存在;如果系統裡 Observer 數量開始變多,需要有一個總覽的視角,集中在 ServiceProvider 裡註冊會比較好管理。
負責新聞內容的 Observer,saving() 方法裡做的事情是:解析內容裡的圖片標籤,把外部系統回傳的圖片路徑格式,轉換成站內可以直接下載的公開網址。整段邏輯包在 try/catch 裡:
public function saving(News $news): void
{
try {
// 解析內容裡的圖片標籤,轉換路徑格式
// ...
} catch (Throwable $e) {
report($e);
}
}
這裡的設計選擇值得停下來想:內容轉換失敗,不會讓整筆資料的存檔動作跟著失敗——catch 住例外之後只是回報錯誤,讓存檔繼續進行,只是這次的圖片路徑轉換沒有生效。
這是一個明確的權衡:存檔失敗的代價(使用者的編輯內容整個遺失)遠大於「圖片路徑轉換這次沒生效」的代價(頂多是這篇內容的某張圖顯示不出來,之後還可以再修)。如果反過來讓例外往外拋,一次意料之外的內容格式,就可能讓編輯者辛苦寫好的內容整個存不進去——這種「因為一個邊角情況讓核心操作全部失敗」的設計,通常不是使用者想要的行為。
❌ 內容解析失敗,直接讓整個存檔失敗
public function saving(News $news): void
{
$crawler = new Crawler($news->content);
// 如果 $news->content 格式跑掉,這裡直接拋例外,
// 整筆存檔動作被中斷,使用者剛編輯好的內容全部遺失
}
✅ 核心動作(存檔)優先,附加邏輯失敗不影響核心
public function saving(News $news): void
{
try {
// 內容解析與轉換
} catch (Throwable $e) {
report($e);
// 存檔繼續,只是這次的圖片路徑轉換沒生效
}
}
這個判斷不是通用真理——如果 Observer 裡做的是「驗證資料合法性」這種本來就該擋下不合法存檔的邏輯,讓例外往外拋才是對的。關鍵在於分清楚:這段邏輯是「核心業務規則」還是「附加的加值處理」,兩者對失敗的容忍度應該不一樣。
你的專案裡有沒有 Observer 或類似的 hook 邏輯,裡面做的事情失敗時,會不會連累到不該被連累的核心操作?回想一下,那段邏輯的失敗容忍度,是你刻意設計的,還是寫的時候沒有特別想過?
#[ObservedBy])讓 Observer 註冊資訊就近寫在 Model 上,方便打開 Model 就看到try/catch 包住附加邏輯,是「核心動作優先於附加處理」的明確權衡,不是無腦加上去的防禦寫法明天要看一個靠 Laravel 事件自動探索機制、完全沒有手動註冊的 Listener——一筆登入事件,怎麼在沒有人明確接線的情況下,自動變成一筆審計紀錄。