「這段程式碼用 error_log() 記了個錯誤,那段用 trigger_error(),反正都是記錄錯誤訊息,遷移的時候應該可以用同一套邏輯處理吧?」
這是我在推動這個遷移時,一開始也有的想法——直到真的去查證這兩個函式的行為,才發現它們的差異遠比「用哪個函式」更關鍵,直接影響遷移時該怎麼設計轉換規則。
error_log() 跟 trigger_error() 在攔截行為上的關鍵差異error_log() 完全沒有被任何全域機制攔截——它就是單純把訊息寫到設定好的地方(可能是檔案、可能是系統 log),沒有任何攔截點可以在它執行的當下插入額外的處理邏輯,例如加上結構化的 context、或者觸發告警。
trigger_error() 則不一樣——它會觸發 PHP 的錯誤層級機制,如果系統裡有註冊全域錯誤處理器(set_error_handler()),這個處理器可以攔截到這次呼叫,做一些額外的事情。攔截點會自動拿到 PHP 內建附帶的資訊(錯誤訊息、發生的檔案、行號),但即使被攔截到,除了這些 PHP 自動帶的欄位之外,攔截點完全拿不到任何業務層的結構化資訊——呼叫的人是誰、當下的請求是誰觸發的、相關的業務欄位是什麼,這些資訊如果沒有手動塞進那串訊息文字裡,攔截點一樣拿不到。
如果遷移時把 error_log() 跟 trigger_error() 都當成「反正都是舊式錯誤記錄,統一轉成 logger()->error(...) 就好」,看起來邏輯一致,但會漏掉一個關鍵事實:這兩種呼叫在遷移前的實際運作方式不一樣,遷移後如果沒有針對這個差異做對應處理,可能會意外改變系統行為,而不是單純换一種寫法記錄同一件事。
具體來說:如果系統裡原本就有全域 handler 攔截 trigger_error(),並且這個 handler 做了某些事情(例如把特定層級的錯誤轉發到某個告警管道),直接把這段呼叫換成 logger()->error(...) 而沒有確認新的 logger 設定是否涵蓋了原本 handler 做的事,可能會讓原本「靜默運作」的告警機制在遷移後消失,卻沒有任何錯誤訊息告訴你這件事——因為表面上「這段程式碼還是在記錄錯誤」,測試也可能還是綠燈,只是原本會觸發的下游動作不見了。
用一組對照來看這個差異:
❌ 把兩者當同一類問題,統一轉換:
「這段用 error_log(),那段用 trigger_error(),
反正都是記錄錯誤,全部改成 logger()->error() 就好。」
→ 沒有查證 trigger_error() 原本有沒有被全域 handler 攔截、
攔截後做了什麼,遷移後這些下游行為可能悄悄消失
✅ 先查證各自原本的攔截行為,再決定遷移方式:
「這段 error_log() 沒有被任何機制攔截,直接轉換風險低;
那段 trigger_error() 有被全域 handler 攔截,
攔截後會做 XX 事情,遷移時要確認新的 logger 設定
是否需要額外設定才能涵蓋這個行為,不能只是換個函式名稱。」
→ 遷移前先分清楚兩種呼叫原本的實際行為,
才能判斷遷移會不會意外改變系統行為
遷移到結構化的 PSR-3 logger(例如 logger()->error($message, $context))之後,真正的改善不只是「換了個函式名稱」,而是把原本只能塞在一串文字裡的資訊,拆成明確的結構化欄位——哪個 channel、哪個請求觸發的、相關的業務欄位是什麼,這些都可以獨立當成 context 欄位傳入,而不是全部擠壓成一段給人讀的文字訊息。
這個改變對下游處理很關鍵:結構化的 context 可以被下游系統(例如監控平台)直接查詢、過濾、統計,而不需要對著一串自然語言文字做字串比對來猜測「這個錯誤是不是我在意的那一類」。這正是「記錄一件事」跟「記錄一件事、並且讓這件事可以被後續系統理解跟利用」之間的差距。
如果你手上的 legacy 系統也有 error_log/trigger_error 這類舊式錯誤處理程式碼:你知道有哪些呼叫背後其實有全域 handler 在攔截並做額外處理嗎?如果不確定,遷移前你會怎麼查證?
error_log() 完全不會被任何全域機制攔截,trigger_error() 可能被攔截,但攔截點拿不到結構化 context明天要換一個角度:像這樣一個涉及全站數百支檔案的長期遷移計畫,該怎麼追蹤進度——尤其是進度清單本身也會過期這件事,該怎麼處理。