PHP
這個分類下的問題與解答。點標題可以展開答案,也可以點右上角進到單題頁面。
版本的生命週期
PHP 的每個主要版本大致遵循固定的支援週期:約兩年的主動支援(修正一般錯誤與安全性問題),接著約一年的安全性支援(僅修正安全問題),之後停止維護。
換句話說,一個版本從發布到完全停止支援,大約三年。
實際的日期會依版本而異,建議直接查閱官方的支援時程表,不要憑印象判斷。
為什麼不能停在舊版
一、安全風險
停止支援後,即使發現新的安全漏洞也不會再有官方修補。這與網站系統或外掛停止維護是同樣的問題。
更麻煩的是:漏洞公開之後,未更新的環境反而更容易成為自動化攻擊的目標。詳見更新與弱點管理。
二、主機商會強制淘汰
多數主機商不會無限期提供舊版本。當主機商公告停止支援某個版本時,你可能只有數週的時間處理。
臨時被迫升級的風險,遠高於有計畫的升級。
三、效能差異明顯
PHP 7 之後每一版都有效能改善。從很舊的版本升上來,通常能得到可觀的執行效率提升,而且不需要改任何程式碼。
四、套件不再支援
第三方套件會逐步放棄對舊版本的支援。停在舊版意味著無法使用新版套件,連帶也拿不到它們的安全修補。
升級前的相容性檢查
一、確認目前的版本與環境
php -v php -m # 已載入的擴充套件 php --ini # 設定檔位置
注意命令列與網頁環境可能使用不同的設定檔——透過網頁執行 phpinfo() 確認實際生效的版本與設定。
二、用靜態分析工具掃描
有專門檢測版本相容性的工具,可以在不執行程式的情況下找出可能有問題的語法。
常見的做法是先掃描一輪,把明確的問題列成清單,再逐項處理。
三、檢查擴充套件
某些擴充套件在新版可能已被移除或改名。升級前確認:
- 目前載入的擴充在新版是否仍存在
- 是否有替代方案
- 第三方套件的相容版本
四、檢查第三方套件
若使用套件管理工具,可先在測試環境嘗試更新,確認相依關係是否能解決。
升級的標準流程
- 完整備份——程式碼與資料庫
- 建立測試環境——與正式環境相同的版本與設定
- 在測試環境升級並開啟完整的錯誤記錄
- 逐一走過主要功能——前台、後台、表單、金流、排程
- 檢視錯誤日誌——包含警告與棄用通知
- 修正問題後重複測試
- 正式環境升級,選在流量低的時段
- 升級後持續觀察數日的日誌
第五步不要只看致命錯誤
棄用通知(Deprecated)是下一版的致命錯誤。 現在忽略,下次升級就會爆掉。
建議在測試環境設定:
error_reporting = E_ALL display_errors = Off log_errors = On
這樣所有問題都會進日誌,但不會顯示在畫面上。
跨越多個版本的升級
如果從很舊的版本升級(例如 5.6 或 7.0),建議分階段進行,而不是一次跳到最新。
理由是:每一版的破壞性變更不同,一次跳多版時,錯誤訊息會混在一起難以判斷來源。
分階段的做法是每次升一個主要版本,確認穩定後再進行下一階段。
升級後常見的狀況
| 症狀 | 常見原因 |
|---|---|
| 502 錯誤 | PHP-FPM 的 socket 路徑改變,網頁伺服器設定未同步 |
| 白畫面 | 致命錯誤但未顯示,需查日誌 |
| 部分功能失效 | 擴充套件未安裝或已移除 |
| 大量警告 | 舊寫法在新版被限縮 |
| 設定失效 | php.ini 未沿用,或路徑改變 |
第一項最常見。 升級後務必確認網頁伺服器設定中的 socket 路徑,見Nginx 與 Apache 的疑難排解。
把版本升級排入常態工作
建議的節奏:
- 每年檢視一次目前使用的版本與其支援狀態
- 在進入安全性支援階段時就開始規劃升級,而不是等到完全停止支援
- 把版本升級的責任歸屬寫進維護合約——主機商、廠商還是自己
責任歸屬常常沒有講清楚,結果各方都以為是別人在負責。
本文的設定與語法以 PHP 8.x 為例,實際的參數、預設值與支援狀態可能因版本與發行版而異。各版本的支援時程請以 PHP 官方公告為準。套用前請先在測試環境驗證,並確實備份原設定檔。
先確認設定檔在哪
php --ini
輸出會列出載入的主設定檔與額外的設定目錄。命令列與網頁環境可能使用不同的設定檔,網頁端請用 phpinfo() 確認。
使用 PHP-FPM 時,行程池的設定檔(如 www.conf)也可能覆蓋部分設定。
錯誤處理:正式與開發環境要分開
正式環境
display_errors = Off display_startup_errors = Off log_errors = On error_log = /var/log/php/error.log error_reporting = E_ALL & ~E_DEPRECATED
正式環境絕不能顯示錯誤訊息。 錯誤內容可能洩漏檔案路徑、資料庫結構等資訊,這是實際的資安風險。
開發或測試環境
display_errors = On error_reporting = E_ALL
測試環境建議包含棄用通知,因為那是下一版的致命錯誤。
錯誤日誌的位置
若 error_log 未指定,錯誤可能被寫進網頁伺服器的錯誤日誌。建議明確指定路徑,並確認 PHP 執行身分有寫入權限。
記憶體與執行時間
memory_limit = 256M max_execution_time = 60 max_input_time = 60
memory_limit 怎麼抓
- 一般企業網站——128M 到 256M 通常足夠
- 有圖片處理、報表匯出——可能需要更高
- 不要無限制地調高——它是單一請求的上限,設太高會讓失控的程式吃光主機記憶體
這個值與 PHP-FPM 的 pm.max_children 互相關聯:max_children × 單一行程的實際用量,不能超過可用記憶體。
行程池的估算見網頁伺服器的效能相關設定。
執行時間的注意事項
max_execution_time 不計入等待外部資源的時間(例如資料庫查詢、外部 API 呼叫)。所以有時候明明設了 30 秒,實際卻跑了更久。
長時間作業應改為背景排程,不要靠拉長逾時解決。
檔案上傳
file_uploads = On upload_max_filesize = 32M post_max_size = 40M max_file_uploads = 20
三個值要一起看
- post_max_size 必須大於 upload_max_filesize——因為除了檔案還有其他表單欄位
- memory_limit 通常應大於 post_max_size
- 網頁伺服器端也要調整——Nginx 的
client_max_body_size,否則會得到 413 錯誤
只改一處是最常見的錯誤,結果是改了半天上傳還是失敗。
Session
session.cookie_httponly = 1 session.cookie_secure = 1 session.cookie_samesite = "Lax" session.use_strict_mode = 1 session.gc_maxlifetime = 1440
四個安全相關的設定
- cookie_httponly——禁止前端程式讀取,降低被竊取的風險
- cookie_secure——僅在加密連線傳送。全站已是 https 才能開啟,否則登入會失效
- cookie_samesite——限制跨站傳送,降低跨站請求偽造的風險
- use_strict_mode——拒絕未經伺服器產生的 session 編號,防止 session 固定攻擊
多台伺服器的情況
預設的 session 存在本機檔案系統。若有多台伺服器負載平衡,需改用共用的儲存機制,否則使用者會頻繁被登出。
時區與語系
date.timezone = "Asia/Taipei"
務必明確設定,否則日期時間的計算可能與預期不符——排程、訂單時間、報表都會受影響。
安全相關的設定
expose_php = Off allow_url_fopen = Off allow_url_include = Off
- expose_php——關閉後回應標頭不會顯示 PHP 版本
- allow_url_fopen——關閉可降低遠端檔案讀取的風險。但部分套件會用到,關閉前需確認
- allow_url_include——應保持關閉,這是嚴重的風險來源
關於 disable_functions
可停用高風險的函式,例如系統指令執行相關的函式。但要先確認應用程式沒有使用到,否則會導致功能失效。
停用清單應依實際情況評估,不要照抄網路上的範例。
不同層級的設定方式
| 層級 | 方式 | 適用 |
|---|---|---|
| 全域 | php.ini | 整台伺服器 |
| 行程池 | FPM 設定中的 php_admin_value | 各站台不同設定 |
| 目錄 | .user.ini 或 .htaccess | 特定目錄 |
| 程式中 | ini_set() | 執行期間,部分設定無效 |
用 php_admin_value 設定的項目無法被程式中的 ini_set 覆蓋,適合用於安全相關的設定。
修改後要驗證
- 重載 PHP-FPM——
systemctl reload php8.3-fpm - 用 phpinfo() 確認實際生效的值——不要只相信改了設定檔
- 確認錯誤日誌沒有新的問題
第二項很重要——設定可能被其他層級覆蓋,或改到了不是實際載入的那個檔案。
本文的設定與語法以 PHP 8.x 為例,實際的參數、預設值與支援狀態可能因版本與發行版而異。各版本的支援時程請以 PHP 官方公告為準。套用前請先在測試環境驗證,並確實備份原設定檔。
OPcache 是效益最大的單一設定
PHP 每次執行都要把原始碼編譯成中介碼。OPcache 把編譯結果存在記憶體中,後續請求直接使用,省下重複編譯的開銷。
對多數 PHP 網站而言,啟用 OPcache 是投入最少、效益最明顯的一項調整。
但要注意:它只解決編譯的開銷,不會加速你的資料庫查詢或程式邏輯。 若瓶頸在那裡,OPcache 的幫助有限。
基本設定
opcache.enable = 1 opcache.enable_cli = 0 opcache.memory_consumption = 256 opcache.interned_strings_buffer = 16 opcache.max_accelerated_files = 20000 opcache.validate_timestamps = 1 opcache.revalidate_freq = 2 opcache.save_comments = 1
各參數的意義
- memory_consumption——分配的記憶體,單位 MB。不足時會開始清除快取,效益下降
- max_accelerated_files——可快取的檔案數上限。應大於專案的實際 PHP 檔案數
- validate_timestamps——是否檢查檔案是否被修改
- revalidate_freq——檢查的間隔秒數
- save_comments——保留註解。若有套件依賴註解形式的標註,必須開啟,否則會出現難以理解的錯誤
怎麼確認設定夠不夠
用 opcache_get_status() 查看實際狀態,重點看三個數字:
| 指標 | 意義 | 判斷 |
|---|---|---|
| 記憶體使用率 | 已用 ÷ 總量 | 接近上限就該調高 |
| 快取檔案數 | 已快取 ÷ 上限 | 接近上限就該調高 |
| 命中率 | 命中 ÷ 總請求 | 應該很高,偏低代表快取一直被清 |
如果出現重啟次數持續累加,代表記憶體或檔案數不足,快取被反覆清空重建,效益大打折扣。
正式環境的兩種策略
策略一:保留時間戳檢查
opcache.validate_timestamps = 1 opcache.revalidate_freq = 60
檔案更新後最多 60 秒生效。適合會直接在伺服器上修改檔案的情況。
策略二:關閉時間戳檢查
opcache.validate_timestamps = 0
效能最好,但檔案更新後不會自動生效,必須在部署後手動清除快取或重載 PHP-FPM。
這是很多人踩過的坑:改了程式卻沒有變化,重載 PHP-FPM 之後才生效。
建議
- 有自動化部署流程——用策略二,並在部署腳本中加入重載
- 手動更新檔案——用策略一,避免忘記清快取
檔案快取
可額外把中介碼存到磁碟,讓重啟後不必重新編譯:
opcache.file_cache = /var/cache/opcache opcache.file_cache_only = 0
對重啟頻繁或有多台伺服器的環境有幫助。注意該目錄的權限與清理。
關於 JIT
PHP 8 引入的即時編譯功能。實務上要有正確的期待:
- 對數值運算密集的程式有明顯效益
- 對典型的網頁應用效益有限——因為瓶頸通常在資料庫與輸出入,不是 CPU 運算
- 可能增加記憶體用量與除錯的複雜度
opcache.jit_buffer_size = 64M opcache.jit = tracing
建議先測量再決定。 多數企業網站啟用 JIT 的效益不明顯,而 OPcache 本身的效益就很大了。
realpath 快取
容易被忽略但實際有效的一項:
realpath_cache_size = 4096k realpath_cache_ttl = 600
它快取檔案路徑的解析結果。對有大量 include 的框架型應用,效益明顯。
其他效能相關的設定
output_buffering = 4096 zlib.output_compression = Off
壓縮建議交給網頁伺服器處理,不要在 PHP 層做——伺服器層的實作通常更有效率,也比較好統一管理。
壓縮設定見網頁伺服器的效能相關設定。
資料庫連線
PHP 端可以考慮持久連線,但要謹慎:
- 可省下連線建立的開銷
- 但連線數會隨行程數累積,可能耗盡資料庫的連線上限
- 交易或暫存表未清乾淨時,可能影響下一個使用該連線的請求
多數情況不建議開啟,除非確認資料庫端的連線上限足夠且有明確效益。
調校的正確順序
- 先測量——確認瓶頸在哪,不要憑猜測調參數
- 啟用 OPcache 並確認命中率——這是效益最大的一步
- 檢查應用層——資料庫查詢、迴圈中的重複查詢、缺少索引
- 再考慮其他調整
第三項通常才是真正的瓶頸。 參數調到極致,也救不了一個在迴圈中查一千次資料庫的程式。
整體的效能排查見網站慢的常見原因與優先處理順序。
本文的設定與語法以 PHP 8.x 為例,實際的參數、預設值與支援狀態可能因版本與發行版而異。各版本的支援時程請以 PHP 官方公告為準。套用前請先在測試環境驗證,並確實備份原設定檔。
PHP 8 對舊程式碼的影響
PHP 8 系列在錯誤處理與型別檢查上比 PHP 7 嚴格得多。過去只是警告的情況,很多在 PHP 8 變成致命錯誤。
這對維護多年的舊系統影響最大——程式一直正常運作,升級後卻出現大量錯誤。
以下整理最常遇到的幾類。
一、警告升級為錯誤
PHP 8.0 把許多原本的警告改為拋出錯誤。最常見的幾種:
- 存取不存在的陣列鍵——過去是通知,現在仍是警告但更明顯
- 對非陣列使用陣列語法——現在會拋出錯誤
- 除以零——現在拋出 DivisionByZeroError
- 參數數量或型別不符——內建函式現在會拋出 TypeError
典型的修法
// 舊寫法 $name = $data['name']; // 建議 $name = $data['name'] ?? '';
二、傳入 null 的棄用警告
PHP 8.1 起,對內建函式的非可為空參數傳入 null 會產生棄用通知。
這是舊系統升級後最常見的大量警告來源,因為過去這樣寫完全正常。
// 會產生棄用通知 $len = strlen($maybeNull); $trimmed = trim($maybeNull); $upper = strtoupper($maybeNull); // 建議 $len = strlen($maybeNull ?? ''); $trimmed = trim((string)($maybeNull ?? ''));
這類通知現在不會中斷程式,但在未來的版本可能變成錯誤。 建議逐步修正而不是忽略。
三、動態屬性的棄用
PHP 8.2 起,在未宣告的類別屬性上直接賦值會產生棄用通知。
class User {
public $name;
}
$u = new User();
$u->email = 'a@example.com'; // 未宣告,產生棄用通知
三種處理方式
// 方式一:明確宣告(建議)
class User {
public $name;
public $email;
}
// 方式二:加上屬性標註(過渡用)
#[\AllowDynamicProperties]
class User { }
// 方式三:實作魔術方法自行管理
舊框架與 CMS 大量使用動態屬性,升級到 8.2 時會產生非常多通知。若無法立即重構,可先用方式二過渡,但應排入改善計畫。
四、字串與數字的比較行為改變
PHP 8.0 調整了非嚴格比較的規則。字串與數字比較時,改為以字串方式比較(除非字串是數值形式)。
// PHP 7:true('foo' 被轉為 0)
// PHP 8:false
var_dump(0 == 'foo');
// 兩者皆為 true
var_dump('1' == '01');
這個變更通常讓行為更符合直覺,但依賴舊行為的程式可能出現邏輯錯誤——而且不會有錯誤訊息,只是結果不同。
建議在比較時使用嚴格比較:
if ($value === 'expected') { }
五、已移除的函式與擴充
各版本陸續移除了一些舊有的功能。常見的包含:
- 舊的 MySQL 擴充——早已移除,應改用 mysqli 或 PDO
- 魔術引號相關函式
- 部分字串與陣列函式的舊別名
- each()——已移除,改用 foreach
檢查方式
用靜態分析工具掃描,可以在不執行的情況下找出使用了已移除功能的位置。
六、建構子與方法簽章的檢查更嚴格
子類別覆寫父類別方法時,參數與回傳型別的相容性檢查變嚴格。不相容時會直接拋出致命錯誤。
這在繼承層級較深的舊框架中特別容易遇到。
七、字串內插與大括號語法
部分舊的字串內插寫法已被標記為棄用:
// 已棄用的寫法
echo "值是 ${var}";
// 建議
echo "值是 {$var}";
echo "值是 $var";
怎麼有系統地處理
一、先讓所有問題可見
在測試環境設定:
error_reporting = E_ALL display_errors = Off log_errors = On
然後完整走過所有功能,收集日誌。
二、分類處理
| 類型 | 優先度 | 處理 |
|---|---|---|
| Fatal error | 最高 | 必須修,否則功能壞掉 |
| Warning | 高 | 可能造成邏輯錯誤 |
| Deprecated | 中 | 下一版的致命錯誤,應排入計畫 |
| Notice | 低 | 逐步改善 |
三、從共用的程式碼開始改
同樣的問題可能散布在數十個檔案中。先找出共用的函式庫或基底類別,改一處可能解決很多地方。
四、不要在正式環境隱藏棄用通知了事
把 error_reporting 調低確實能讓日誌乾淨,但問題仍然存在,只是被藏起來了。
正式環境可以不記錄棄用通知以免日誌爆量,但測試環境必須看得到,並排入改善計畫。
老系統的現實建議
如果系統很舊、原始碼被大幅改過又沒有文件,升級的工時可能超過重建。
判斷的參考:
- 錯誤數量龐大且散布廣泛
- 使用了已無維護的框架或套件
- 沒有測試可以驗證修改是否正確
- 原始碼與官方版本差異太大,無法直接套用官方更新
這種情況下,在乾淨的環境重建,往往比持續修補更划算——見網站改版的流程。
本文的設定與語法以 PHP 8.x 為例,實際的參數、預設值與支援狀態可能因版本與發行版而異。各版本的支援時程請以 PHP 官方公告為準。套用前請先在測試環境驗證,並確實備份原設定檔。
第一步:讓錯誤可見
PHP 的問題排查,八成的時間浪費在「看不到錯誤訊息」。
正式環境的正確設定
display_errors = Off log_errors = On error_log = /var/log/php/error.log
錯誤不顯示在畫面上,但要確實記錄。 兩者都關掉是最糟的情況——出問題時完全沒有線索。
確認日誌真的寫得進去
ls -l /var/log/php/
目錄必須存在,且 PHP 執行身分要有寫入權限。若權限不足,錯誤會靜默消失。
使用 PHP-FPM 時,行程池設定中也可能指定日誌路徑,需一併確認。
白畫面的排查
頁面完全空白,通常是致命錯誤但沒有顯示。依序檢查:
- 看 PHP 錯誤日誌——多數情況答案就在這裡
- 看網頁伺服器的錯誤日誌——若 PHP 的 error_log 未設定,可能寫在這裡
- 暫時開啟顯示錯誤——僅限測試環境
- 檢查是否為記憶體耗盡——日誌中會有 Allowed memory size 相關訊息
- 檢查語法錯誤——用
php -l 檔名逐檔檢查
如果日誌也是空的
- 可能是在 PHP 啟動前就失敗——檢查
display_startup_errors - 可能是 PHP-FPM 崩潰——檢查 FPM 的日誌
- 可能是輸出被緩衝後遺失——檢查是否有
ob_start()但未正確結束
常見錯誤訊息的判讀
Allowed memory size exhausted
記憶體耗盡。常見原因:
- 一次撈取大量資料庫資料——應改為分批處理
- 處理大圖片——影像處理很吃記憶體
- 迴圈中不斷累積陣列
- 無限遞迴
調高 memory_limit 只是治標。 應該找出為什麼需要那麼多記憶體。
Maximum execution time exceeded
執行超時。要注意這個計時不包含等待外部資源的時間,所以實際耗時可能更長。
常見原因:迴圈中重複查詢資料庫、缺少索引導致查詢緩慢、呼叫外部服務無回應。
Call to undefined function
通常是擴充套件未安裝或未載入。用 php -m 確認,並注意命令列與網頁環境可能載入不同的擴充。
Cannot modify header information
在輸出內容之後才嘗試設定標頭或啟動 session。
最常見的原因是檔案開頭或結尾有多餘的空白或換行——特別是 <?php 之前有空行,或檔案結尾的 ?> 後面有換行。
建議:純 PHP 的檔案不要寫結尾的 ?>,可以避免這類問題。
Headers already sent by ... on line N
訊息會直接告訴你是哪個檔案的哪一行先產生了輸出,依此追查即可。
資料庫相關的問題
- 連線失敗——確認主機、帳號、密碼、資料庫名稱,以及資料庫服務是否正常
- Too many connections——連線數超過上限,可能是持久連線累積或未正確關閉
- 查詢緩慢——啟用慢查詢日誌,找出耗時的語句與缺少的索引
- 字元編碼異常——確認連線的字元集設定與資料庫、資料表一致
找出效能瓶頸
簡易的計時
$start = microtime(true);
// 要測量的程式
error_log('耗時: ' . (microtime(true) - $start));
記錄慢速請求
PHP-FPM 可設定記錄執行過久的請求,並附上呼叫堆疊:
request_slowlog_timeout = 5s slowlog = /var/log/php/slow.log
這是找出瓶頸最直接的方法——它會告訴你程式卡在哪一行。
正式的效能分析工具
需要更詳細的分析時,可使用專門的效能剖析擴充。但這類工具本身有開銷,不建議長期在正式環境啟用。
不要在正式環境做的三件事
- 開啟 display_errors——錯誤訊息可能洩漏路徑與結構
- 留下 phpinfo() 頁面——會完整揭露環境資訊,是很常見的資安疏失
- 用 var_dump 或 echo 除錯——會影響使用者看到的畫面,也可能洩漏資料
第二項要特別注意。 除錯時建立的 phpinfo 檔案,事後常常忘記刪除,而檔名多半容易被猜到。
排查的通用順序
- 確認範圍——全站還是特定頁面?所有人還是部分?
- 看錯誤日誌——PHP 與網頁伺服器兩邊都看
- 確認是否為近期變更造成——程式部署、設定調整、版本升級
- 檢查系統資源——磁碟、記憶體、行程數
- 在測試環境重現——正式環境不適合反覆試驗
第三項最有效。 突然出現的問題,幾乎都能對應到某個具體的變更。
建立可追溯的基礎
平常做好這幾件事,出問題時會省下大量時間:
- 錯誤日誌確實記錄且有輪替
- 記錄每次部署與設定變更的時間
- 保留變更前的備份
- 把磁碟用量納入監控——日誌塞爆磁碟是實際會發生的事故
日誌的判讀與維護見Nginx 與 Apache 的疑難排解。
本文的設定與語法以 PHP 8.x 為例,實際的參數、預設值與支援狀態可能因版本與發行版而異。各版本的支援時程請以 PHP 官方公告為準。套用前請先在測試環境驗證,並確實備份原設定檔。
準備好讓網站 開始幫你帶生意了嗎?
不論是要做新網站、救舊網站,還是只想先聊聊方向——先諮詢,不用先付錢,我們照實給你建議。
