多數人以為主機把 PHP 升到 8.x 之後網站當機,是外掛本身壞掉了;真正常見的情況卻是那支外掛程式碼一行都沒變,只是 PHP 8 不再放行它原本慣用的寫法。同一支購物車外掛、同一個聯絡表單,在 PHP 7.4 上跑了五年都沒事,主機一把版本切到 8.1,首頁立刻變成一片空白,或者前台正常、後台某個功能一按就跳出「發生嚴重錯誤」。
這種現象背後是一道相容性斷層:PHP 8 把過去只會提示警告、甚至完全放行的寫法,明確劃進不相容變更清單,程式一執行到那一行就直接中斷。PHP 8 相容要處理的不是把外掛整批換新這麼簡單,而是先分清楚哪一段程式碼是被完全禁止、哪一段只是被降級成警告,才不會把力氣花在不會讓網站掛掉的雜訊上。
PHP 8 升級後的當機範圍,以及復原提示與白畫面的差別
升級後看到的畫面不只一種,範圍不同代表要查的方向也不同。第一種是整站前後台都打不開,通常一打開首頁就直接顯示嚴重錯誤或整片空白;第二種是後台正常、前台掛掉,或反過來前台正常後台故障,代表問題出在只在某一端執行的那段程式碼,像某個小工具只在前台輸出內容、某個外掛的設定頁只在後台載入;第三種是平常都正常,只有某個特定頁面或動作觸發時才出錯,例如結帳頁、聯絡表單送出、報表匯出,代表出問題的函式只在那個情境才會被呼叫到。
WordPress 從 5.2 版起加入一套嚴重錯誤保護機制,一旦頁面載入過程中偵測到 PHP 致命錯誤,前台不會直接壞掉,而是顯示一句「網站發生技術問題」的說明畫面,同時系統會寄一封信到網站管理員信箱,信裡附一組進入「復原模式」的專屬連結。用這個連結登入後台,觸發錯誤的外掛或佈景主題會被暫時停用,而且只影響進入復原模式的這個瀏覽器工作階段,不會連帶讓其他訪客也看到後台被動過手腳,管理員可以先不動 FTP、不改任何程式碼就把問題定位出來。
5.2 之前的版本,或者這層保護被關閉,又或者錯誤發生在保護機制生效之前,看到的就是俗稱「白畫面死機」的完全空白頁,畫面上沒有任何錯誤文字,也沒有復原連結,只剩一片空白。這時候前面提到的三種情境分類同樣適用,只是要靠下一節的 debug.log,而不能依賴畫面上的提示。
PHP 8 把舊版縱容的寫法直接判定為致命錯誤
問題不是外掛在升級的瞬間突然壞掉,而是 PHP 8 把過去只會警告、甚至完全放行的寫法,改成直接丟出無法忽略的錯誤。這個相容性斷層可以拆成兩層來看,第一層是「以前能跑、現在完全不存在」的函式與語法,程式一執行到就是未定義函式或剖析階段就失敗,下一節會列出具體清單;第二層是「以前只是警告、現在變成擋下執行的例外」,原本停留在 E_WARNING、E_NOTICE 等級、頁面照樣能跑完的問題,PHP 8 把其中一批直接升等成會中斷程式的 TypeError、ArgumentCountError。
PHP 官方手冊《Backward Incompatible Changes》把 8.0 的變更整理成幾個分類,其中「警告升級為例外」與「通知升級為警告」是最容易讓老外掛中彈的兩類。前者代表原本只是印一行警告、頁面照跑的狀況,現在會直接中斷整個請求;後者代表原本連警告都不太明顯的 E_NOTICE(例如讀取未定義變數、未定義的陣列鍵值),現在至少會被看見(E_WARNING),但還不到中斷程式的地步。
這類變更會集中出現在 8.0 這種大版本,而不是散在每個小版本,原因是 PHP 官方把破壞相容性的變更集中在大版本發布時一次處理,讓開發者有機會提前準備。外掛作者當年是照著 PHP 5、PHP 7 的規則寫程式,這些規則後來被官方明確列為不相容變更並廢除,外掛沒跟上不代表它壞了,只是它遵守的規則本身已經被改掉,跟外掛品質好壞是兩回事。
定位犯規外掛靠 debug.log 與逐一停用比對
確認問題出在相容性斷層之後,接下來要處理更實際的問題,網站真的當了,該怎麼找出是哪一支外掛在搞鬼。最直接的線索是 debug.log,其次是即使沒有 log 存取權限也還能用的兩套排查法。

debug.log 錯誤路徑直接點名犯規的外掛
要看到 debug.log,得先在 wp-config.php 裡打開除錯設定。WordPress 官方開發者文件給出的標準做法,是在 /* That's all, stop editing! */ 這行之前加入三行常數:
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );Code language: PHP (php)
WP_DEBUG 是總開關,WP_DEBUG_LOG 設為 true 之後,所有錯誤、警告、通知都會被寫進 wp-content/debug.log(也可以另外指定路徑);WP_DEBUG_DISPLAY 設為 false 代表這些訊息不會直接印在前台頁面上,避免嚇到訪客、也避免把伺服器路徑等資訊洩漏出去,但訊息仍然會寫進 log 檔裡。官方文件也提醒,這組設定是給開發、測試環境用的偵錯工具,不建議長期留在正式站上。
打開 debug.log 之後,同一份檔案裡常常同時塞了好幾種等級的訊息,真正要找的是以 PHP Fatal error 開頭、或者訊息本身寫成 Uncaught Error、Uncaught TypeError、Uncaught ArgumentCountError 這幾種。訊息裡通常會帶一段檔案路徑,長得像 /wp-content/plugins/外掛資料夾名稱/某個檔案.php,那個資料夾名稱就是要找的兇手。至於同一份 log 裡夾雜的 Deprecated、Notice 等級訊息,先跳過不用理會,它們多半不是讓網站掛掉的原因,第四節最後會把這個分界講清楚。
沒有 debug.log 存取權限時,復原模式與逐一停用法還能用
不是每個人都有 FTP、SSH 或主機控制台權限去改 wp-config.php,這時候要看後台還進不進得去,分兩種做法。後台還能登入的話,直接到外掛列表全選,用 Bulk actions 批次停用,接著一個一個重新啟用,每啟用一個就檢查一次前台,出錯的那一個就是兇手;動手前先記下原本啟用的外掛清單,這是暫時性操作,排查完要記得把該用的外掛全部裝回去。
如果後台完全進不去,就要靠前面提到的復原模式。WordPress 5.2 之後寄到管理員信箱的那封信裡有登入連結,用它登入後畫面會直接列出是哪一個外掛或主題觸發了錯誤,管理員可以直接到外掛列表或外觀底下的佈景主題頁面停用該項目。萬一連這封信都沒收到,例如寄信功能本身被卡在故障的那段程式碼之前執行,或者信被主機判定成垃圾郵件,才需要退回用 FTP、主機檔案總管把整個 wp-content/plugins 資料夾改名,例如加上 _disabled 後綴,強迫 WordPress 判定所有外掛都不存在、全部停用,前台恢復之後再逐一把外掛資料夾改回原名,一個個測試。
老外掛最常撞上的移除函式與錯誤訊息對照
PHP 8.0、8.1、8.2 真正會讓網站掛掉的移除與收緊項目,可以逐條對應到 debug.log 或畫面上實際會看到的錯誤文字,不用自己去啃 PHP 官方那份落落長的變更清單。完全被移除、呼叫就報錯的函式是一類,語法本身在剖析階段就不合法的寫法是另一類,命名參數這個新功能意外撞出的錯誤又是一類,原本的警告被轉成中斷例外的則是最後一類;四類各自對應不同的錯誤訊息,其中最容易讓人原地打轉的是 deprecated 等級警告,那其實不是真正讓網站掛掉的原因。

create_function()、each() 這類函式一呼叫就跳出未定義函式錯誤
最常見、也最容易被誤認成外掛壞掉的兩個經典案例,一個是 create_function(),老外掛常拿它動態組出一個回呼函式,多半出現在舊版的 shortcode 處理或篩選器綁定裡;另一個是 each(),老外掛拿它跑舊式的陣列迴圈。這兩個函式在 PHP 8.0 被整支拿掉,程式一執行到那一行,PHP 直接回報「呼叫未定義的函式」,屬於最單純、也最好排查的一種,因為錯誤訊息裡的函式名稱本身就是關鍵字,直接拿函式名稱加上 PHP 8 去查,幾乎都能找到修法。
// 舊寫法(PHP 8 呼叫就直接報未定義函式)
$callback = create_function('$a', 'return $a * 2;');
// PHP 8 相容寫法,改用匿名函式
$callback = function ($a) {
return $a * 2;
};Code language: PHP (php)
PHP 官方手冊在《Backward Incompatible Changes》裡明白寫著 create_function() 已被移除,建議改用匿名函式取代;each() 也被移除,建議改用 foreach 或 ArrayIterator。同一份清單也提到 __autoload() 這個自動載入函式,以及 (real)、(unset) 這兩種型別轉換寫法,一併被拿掉,效果一樣,程式碼一執行到就是呼叫未定義函式,或者在剖析階段就直接失敗。
大括號存取陣列、巢狀三元不加括號,會被判定為剖析階段的 ParseError
這一類錯誤跟前一種不同,不是「執行到那一行才報錯」,而是 PHP 直譯器在還沒開始跑程式之前、剖析整份檔案語法的階段就直接判定失敗。這時候錯誤訊息通常長成 PHP Parse error,而不是 Fatal error: Uncaught,而且往往會讓整個網站,不只是那個外掛的功能都連帶掛掉,因為 PHP 連載入那個檔案都做不到。
兩種老寫法最常撞上這條界線。一種是用大括號存取陣列或字串:
// PHP 8 不再支援花括號存取(剖析階段就直接失敗)
$value = $array{0};
// 改用中括號才是合法語法
$value = $array[0];Code language: PHP (php)
另一種是巢狀三元運算子沒有明確加括號,例如 $a ? $b : $c ? $d : $e,PHP 8 起強制要求明確括號,沒加就直接判定語法錯誤。PHP 官方手冊列出「Support for deprecated curly braces for offset access has been removed」並附上修法示範,也列出「Nested ternaries now require explicit parentheses」,兩條都會讓整份檔案在剖析階段就掛掉,不是等到執行才發作。
call_user_func_array 陣列鍵值當成具名參數就炸出 Unknown named parameter
這一條是 PHP 8 新增具名參數功能之後,一個很容易被老外掛意外踩到的副作用,也是要定位是哪個外掛犯規時最不直覺的一種,外掛本身沒有主動使用具名參數語法,卻因為呼叫方式被 PHP 8 重新解讀而出錯。老外掛常見的寫法,是用一個帶有文字鍵值的關聯陣列(例如 ['id' => 5, 'title' => 'foo'])透過 call_user_func_array() 去呼叫另一個函式,原意只是想把陣列值依序當成位置參數帶進去。
PHP 7 會忽略陣列的鍵值,只按順序帶值;PHP 8 開始,這些文字鍵值會被解讀成具名參數,如果被呼叫的函式裡剛好沒有同名的參數(例如函式參數叫 $post_id,陣列鍵值卻是 id),就會直接丟出 Error: Unknown named parameter 讓程式中斷。PHP 官方手冊在 8.0 的不相容變更清單裡寫得很清楚,call_user_func_array() 的陣列鍵值現在會被解讀成參數名稱,而不是像過去一樣被靜默忽略。
以前只是警告的寫法,PHP 8 直接丟出 TypeError 或 ArgumentCountError
「警告升級為例外」具體攤開來看,最常出問題的是函式參數數量或型別不對這一大類,因為它是老外掛裡最常見、也最分散的問題來源,幾乎任何呼叫內建函式的地方都可能中招。最常見的兩種情況,一種是呼叫內建函式時傳入的參數數量不符,例如某個函式只接受 2 個參數,外掛卻多塞了一個,或者該給的參數沒給,PHP 8 起會丟出 ArgumentCountError 而不是過去的警告;另一種是把型別不對的東西當成陣列鍵值或字串位移使用,例如拿陣列或物件當鍵值,PHP 8 起會丟出 TypeError。
PHP 官方手冊列出這批警告轉例外的項目,包括傳入內建(非可變參數)函式的參數數量錯誤會丟出 ArgumentCountError,把無效型別(陣列或物件)當成陣列鍵值或字串位移使用會丟出 TypeError,對純量值的陣列索引寫入、對非陣列或非 Traversable 做展開,也都從警告改成會中斷程式的例外。這類錯誤在 debug.log 裡通常會清楚寫出是哪個函式、第幾個參數出問題,比前面函式整個消失的情況更容易對到症狀,只是因為散落在很多地方,往往要多花一點耐心逐一比對。
deprecated 等級的警告多半不是真正讓網站掛掉的原因
打開 debug.log 之後,通常會同時看到一堆 Deprecated 等級的訊息,例如 PHP 8.1 起「實作 Serializable 介面卻沒有同時實作新版序列化方法」被標為棄用,PHP 8.2 起「建立未宣告的動態屬性」被標為棄用。這些訊息數量往往比真正的 Fatal error 多很多,看起來很嚇人,但它們單獨存在時通常只是警告,不會讓頁面回傳 500 或變成白畫面。真正讓網站掛掉的,還是前面幾節講的那幾類,完全消失的函式、剖析階段的語法錯誤、被轉成例外的警告。
PHP 官方手冊《Deprecated Features》分別列出 8.1 與 8.2 這類只降級成棄用通知、暫時仍可執行的變更。例如 8.1 起,實作 Serializable 介面(許多快取、session 相關外掛用來自訂序列化行為)被標為棄用,官方建議改用 __serialize()、__unserialize(),但舊寫法在 8.1、8.2 仍可運作,只是會出現棄用通知,要等到 PHP 9.0 才會真正被移除;8.2 起,建立一個類別裡沒宣告過的動態屬性也被標為棄用,除非該類別加上 #[\AllowDynamicProperties] 屬性,這在老外掛裡很常見,同樣只是產生棄用通知,不會讓程式中斷。
PHP 官方手冊在魔術方法的說明裡也提到,__sleep()、__wakeup()、__toString() 這類魔術方法如果沒有宣告成 public,PHP 8 起會產生 E_WARNING,而不是 8.0 之前那種完全不檢查,同樣是警告等級,不是讓網站掛掉的直接原因,卻常常跟真正的致命錯誤一起出現在同一份 log 裡,容易讓人搞混優先順序。看到 log 裡一堆 Deprecated 訊息不用逐條處理,先去找 Fatal error、Uncaught Error、Uncaught TypeError、Uncaught ArgumentCountError、Parse error 這幾個關鍵字,那才是真正讓網站掛掉的那一行。
降回舊版 PHP 只是止血,真正解法是換掉不相容的外掛
定位出問題外掛之後,「先讓網站活過來」跟「把問題真正解決」是兩件不同的事,搞混了容易誤以為降版就等於修好。三種處置,各自定位不一樣:降版是暫時的做法,目的是爭取排查時間,不是長久方案,因為舊版 PHP 遲早會被主機淘汰、也會失去安全更新;更新或替換外掛才是根治;補丁式的 polyfill 則是給那些沒有替代品、開發者也不打算再更新的外掛,一個過渡做法。
PHP 版本降回原本能跑的版本是最快的止血動作
多數主機都提供的自助功能,不管是 cPanel 的 MultiPHP Manager、Plesk,還是主機自家的控制台,都可以直接把 PHP 版本切回升級前那個版本,網站通常會立刻恢復——這等於直接驗證了問題確實是 PHP 版本造成的,而不是巧合撞上其他狀況。
降版之後,正好利用網站還能正常存取的空檔,回頭做前面幾節講的 debug.log 排查與逐一停用比對,把真正的兇手定位出來,而不是降版之後就放著不管、假裝問題已經解決。等找到真正犯規的外掛、確認修法或替代方案之後,再重新把 PHP 版本切回去,才算真正走完這個流程。
外掛的更新狀態決定接下來是修還是換掉它
找到兇手外掛之後,第一步永遠是去該外掛在 wordpress.org 外掛目錄的頁面,看有沒有更新版本、開發者有沒有針對新版 PHP 做過相容性調整,同時留意頁面標示的最近更新時間,以及 Tested up to 寫的是測試到哪一版 WordPress。如果外掛已經好幾年沒更新,即使裝了目前看得到的最新版,很可能也解決不了問題,這時候該找的是還在維護、功能相近的替代外掛,而不是死守著一支已經沒人維護的外掛硬修。
這裡有一個常見誤判,跟盤點外掛時容易犯的「只看安裝數」是同一個道理,外掛「還能安裝、還能啟用」不代表它跟目前的 PHP 版本相容,wordpress.org 外掛目錄本身並不會因為外掛過舊就自動下架或跳出警告,判斷相容性得靠自己去看更新紀錄,不能只看它出現在目錄裡就以為沒問題。
保留舊外掛的暫時做法是用 polyfill 補回被移除的函式
給真的沒有替代外掛、也沒有資源馬上換掉的情況一個過渡做法。如果 debug.log 指出的問題是某個被完全移除的函式,例如 create_function(),而且改動範圍很小,具備基本 PHP 能力的人,或者委託的工程師,可以寫一個極簡的相容層,用一個同名函式,或者一支 mu-plugin,把舊函式呼叫轉接到新的寫法,例如把 create_function() 的呼叫改包成一個匿名函式。
要明講這只是拖延問題,外掛本體還是舊的,其他相容性問題可能還沒踩到,優先順序永遠低於換掉或更新外掛。前面提到 create_function()、each() 各自的官方建議替代寫法,改用匿名函式、改用 foreach,也是這裡寫相容層時可以直接照抄的修法,不用再另外查一次。
升級前,測試站與外掛相容性標示是兩道防線
從已經出事的排查,轉到下次升級前該怎麼避免重蹈覆轍。升級 PHP 版本前,不要直接在正式站上動手,先確認 PHP 8 相容的風險有沒有排除,在測試環境(多數主機都提供的 staging 或測試站功能,或者本機環境)把 PHP 版本先切過去試跑,同時搭配外掛相容性標示與掃描工具,把風險降到最低。正式站一出事,訪客與客戶立刻看得到,測試站出事則沒有任何人受影響。

免費 PHP 相容性掃描外掛,安裝數最高的不一定有在維護
wordpress.org 外掛目錄裡有幾款免費的 PHP 相容性掃描外掛,原理是把網站上安裝的外掛與佈景主題程式碼掃過一遍,比對要升級到的 PHP 版本,抓出使用了已移除或已棄用寫法的檔案與行號,等於把前面那份移除清單自動化比對一次,省掉手動逐一測試的時間。
這裡容易踩上跟前面「只看外掛安裝數」一樣的坑,挑掃描工具不能只看安裝數最高的那款,還要順手確認它有沒有在更新維護。目前 wordpress.org 上安裝數最高的 PHP Compatibility Checker,由 WP Engine 出品,安裝數在 20 萬以上,頁面卻明確標示已不再積極維護,不再提供更新;同類型的 Plugin Compatibility Checker,由 CompatShield 出品,安裝數 9,000 以上,查證當下仍持續更新,版本落在 7.0.7,近期才更新過,還新增了資料庫版本相容性檢查。安裝數高不代表工具本身跟得上最新的 PHP 版本變更清單,掃出來的報告反而可能不準。
測試站升級後的 debug.log 是判斷能否上線的依據
測試站要怎麼測才有意義,不是切了 PHP 版本、首頁能打開就算過關,而是要照著前面第三節的方法把 WP_DEBUG_LOG 打開,實際點過幾個關鍵頁面與功能,尤其是有互動、有表單、有金流的頁面,例如購物車結帳、聯絡表單、會員登入,看 debug.log 有沒有冒出新的 Fatal error、Uncaught Error 等級訊息。
只打開首頁看看沒出現嚴重錯誤畫面就以為過關,是最常見的漏測,很多問題只在特定情境才會觸發,例如結帳流程裡某一步呼叫到被移除的函式,平常瀏覽網站根本碰不到。乾淨過關之後,再把同樣的 PHP 版本切換動作搬到正式站執行,才是真正走完升級流程,而不是省略測試站這一步直接在正式站上賭運氣。
外掛頁面的 Requires PHP 和最近更新時間是最快的篩選依據
在真的動手掃描或試跑之前,最快、零成本的健檢方式,是直接到每支已安裝外掛在 wordpress.org 上的頁面,看 Requires PHP 這一項(也就是該外掛最低要求的 PHP 版本,等於官方寫明至少要這個版本才保證能跑),同時對照最近一次更新的時間。WordPress 官方的 Requirements 頁面目前建議主機至少支援 PHP 8.3 或更高版本,作為現代安裝的安全基準,可以拿來當對照的基準線。
WordPress 核心開發手冊《PHP Compatibility and WordPress Versions》記錄了核心對各 PHP 版本支援狀態的變化,例如核心提高最低需求版本、對新版 PHP 從 beta 支援轉為完全相容的時間點,可以用來判斷這個 PHP 版本 WordPress 核心是否已經正式支援。這兩份都是官方對核心本身的支援狀態,跟外掛頁面自己標示的 Requires PHP,也就是外掛作者自己測過的最低版本,是兩件不同的事,核心相容不代表外掛也相容,兩者要分開對照,不能混為一談。
PHP 8 不是要跟老外掛過不去,它只是把過去含糊、可以被忽略的規則寫清楚而已。真正該練的習慣,不是等網站當掉才翻 debug.log,而是每次主機通知要升版之前,先讓測試站跑過一輪,把還沒跟上規則的外掛揪出來。降版永遠只能撐到下一次升級,換掉或更新那支外掛,才是真正解決 PHP 8 相容問題、讓網站站得住的做法。
