PHP 版本遷移指南(baserCMS)
PHP 版本升級時遭遇的棄用與破壞性變更及修正食譜集。依版本整理並持續維護。CakePHP 框架本身引起的問題請參閱另一個技能 cakephp-migration。
收錄:PHP 8.2 / 8.4 / 8.5。支援新版本時請新增「## PHP X.Y」章節。 注意:PHP 8.5 是 9.0 的前一版本。9.0 將移除所有棄用功能,因此 8.5 出現的棄用警告建議及早處理。
基本原則
- 區分「致命錯誤」與「棄用警告」。除錯模式下棄用警告會顯示在畫面底部看起來像「錯誤」,但
logs/debug.log中標示debug:的是警告(程式仍繼續執行)。Fatal/Exception 才是真正的錯誤。 - 區分 regression 與既有失敗。使用
git diff HEAD -- <file>確認該檔案是否是自己修改的對象。 - 更改父類別簽名時,子類別與介面也需一併更新。
- 全面檢測棄用項目可使用 Rector 等靜態分析工具。
單元測試執行方法、語法檢查、失敗診斷請參閱
basercms-unittest技能。php -l(語法檢查)也會同時顯示隱含 nullable 等棄用警告。
PHP 8.2
動態屬性的棄用
對未宣告的屬性賦值被棄用。會看到 Creation of dynamic property X::$prop is deprecated。
- 對策 A(建議): 在類別中宣告屬性。
※ 典型情況是在建構子中使用/** @var ThemeFoldersServiceInterface */ protected $ThemeFoldersService; protected $_tempalteTypes = [];$this->Xxx = $this->getService(...)進行賦值。 - 對策 B: 對於會影響大量子類別的基底類別(例如測試基底
BcTestCase有超過 400 個子類別),可以加上#[ AllowDynamicProperties]屬性。屬性會被子類別繼承,因此只需修改一處。#[
AllowDynamicProperties] class BcTestCase extends TestCase { ... }
---
## PHP 8.4
### 1. 隱含 nullable 參數的棄用
`類型 $x = null` 是隱含 nullable 已被棄用。請明確使用 `?類型`。
```php
// Before
function foo(int $contentId = null, BcAbstractDetector $agent = null)
// After
function foo(?int $contentId = null, ?BcAbstractDetector $agent = null)
- 檢測(正規表達式): 比對開頭沒有
?且形式為類型 $name = null的參數。 例如:([A-Za-z_\\][A-Za-z0-9_\\]*)\\s+(\\$[A-Za-z0-9_]+)\\s*=\\s*null(前面沒有?者)。 - 變更父類別簽名時,子類別/介面也需一併更新。
- 警告訊息:
Implicitly marking parameter $x as nullable is deprecated, the explicit nullable type must be used instead。
2. fgetcsv() 的 $escape 棄用
省略 $escape 參數已被棄用。請明確指定。
// Before
$head = fgetcsv($fp, 10240);
// After
$head = fgetcsv($fp, 10240, ',', '"', '\\');
- 自行實作(例如使用
fgets+正規表達式解析 CSV 的函數)沒有用到fgetcsv,因此不在此限。僅需修正實際呼叫fgetcsv()的地方。
3. ReflectionProperty::setValue() 單一參數呼叫的棄用
對於非靜態屬性,需要傳入目標物件。
// Before
$property->setValue($value);
// After
$property->setValue($object, $value);
- 警告訊息:
Calling ReflectionProperty::setValue() with a single argument is deprecated。 - 容易潛伏在測試用工具(產生請求或操作私有屬性的 helper)中。
- 對於靜態屬性,第一個參數請傳入
null:setValue(null, $value)。這在 8.1〜8.5 都有效(不會破壞 8.1 相容性)。
4. 將 null 傳給內部函數的 string 參數已被棄用(實際上從 8.1+ 開始)
parse_url(null) / version_compare($a, null) / str_replace(..., null) 等,當參數型別不是 ?string 卻傳入 null 時,會出現 Passing null to parameter #N ($x) of type string is deprecated。請使用 (string) 強制轉換。
// Before → After
parse_url($url); → parse_url((string)$url);
version_compare($current, $required); → version_compare((string)$current, (string)$required);
- 此棄用始於 8.1,但在遷移作業(特別是更新到 8.4/8.5)時會大量出現,因此也收錄於此。
- 只有在執行時期值為 null 時才會出現,無法僅靠靜態 grep 完全找出。可以彙整所有測試日誌中的
Passing null to parameter來識別發生來源。
PHP 8.5(2025-11-20 釋出)
9.0 前的最後一個次要版本。以下大部分是警告,程式仍可繼續執行,但會在 9.0 被移除,因此需要處理。依對應用程式/框架影響程度排列。※ 只有「0.」是致命錯誤(破壞性變更),請最優先處理。
⚠️ 維持 PHP 8.1 相容性期間的注意事項(
composer.json設定為"php": ">=8.1")。 如果按照「建議的取代方式」修正 8.5 的棄用項目,可能會因為該取代方式在 8.1 中不存在而導致 8.1 出現致命錯誤。以下標示 ❌ 的項目請在放棄 8.1 支援前暫緩處理(8.5 會出現警告但仍可接受)。在 8.5 執行本身不會破壞 8.1(僅出現警告)。
- ❌
str_increment()(8.3+)→ 8.1 繼續使用$s++- ❌
http_get_last_response_headers()(8.4+)→ 8.1 繼續使用$http_response_header- ❌ 帶命名空間的 PDO(如
Pdo\Mysql::ATTR_*, 8.4+)→ 8.1 繼續使用PDO::MYSQL_ATTR_*上述以外的項目(非正規轉型、
null作為偏移量→''、自動釋放函數的移除、反引號→shell_exec、case ;、__serialize)在 8.1 也可以安全套用。
0. 【破壞性變更・致命】使用 use 引用保留類別名稱(如 Array 等)會導致 Fatal
8.5 將 Array 等設為特殊類別名稱,使用 use ...\Array; 或 class Array 會導致 Fatal(不是警告)。如果在測試收集(自動載入)階段就發生錯誤,會導致整個測試套件無法啟動,因此最優先處理。
Fatal error: Cannot use BcMail\Test\TestCase\Model\Array as Array because 'Array' is a special class name
- baserCMS 實際案例:
MailMessagesTableTest.php開頭遺留 CakePHP2 時代的殘骸use ...\Array;/use ...\ClassRegistry;(兩者都是指向不存在類別的無效 import),在 8.4 之前沒有影響,但在 8.5 變成致命錯誤。刪除對應的use行即可解決(實際參照處是markTestIncomplete的未執行程式碼)。 - 檢測:
grep -rnE "use [A-Za-z0-9_\\\\]+\\\\(Array|Object|Resource|Enum|Mixed|Never|Void|Null|False|True|Iterable|Numeric)\\s*;" --include="*.php"。 - 注意: 這是致命錯誤,
php -l可以針對單一檔案檢測。但在執行所有測試前先用 grep 掃過一遍,可以避免測試套件在收集階段就崩潰。
1. 非正規型別轉換名稱的棄用(影響範圍大)
(boolean) (integer) (double) (binary) 已被棄用。請替換為正規名稱。
// Before → After
(boolean) → (bool)
(integer) → (int)
(double) → (float)
(binary) → (string)
- 檢測:
grep -rnE "\\((boolean|integer|double|binary)\\)" --include="*.php"。 - 容易殘留在舊程式碼中。可以機械化替換。
2. 使用 null 作為陣列偏移量已被棄用
$arr[null] / array_key_exists(null, $arr) 等將 null 作為鍵值已被棄用(原本會隱含轉換為 "")。請明確使用空字串 ""。
// Before
$arr[null] = $v; array_key_exists(null, $arr);
// After
$arr[''] = $v; array_key_exists('', $arr);
- 注意變數作為鍵值的情況(
$arr[$key]且$key可能為 null)。 - 難以僅靠靜態 grep 檢測(只有在執行時期值為 null 時才會出現)。比較可靠的方式是在 8.5 下執行所有測試,然後用
grep "null as an array offset" phpunit.log彙整發生來源(on line N of <file>)。 - 修正方針: ① 將賦值/存取的鍵值改為
$key ?? ''(與原本的隱含轉換相同,務必讓 set/get 使用相同的鍵值)、② 在isset()/存取之前加上$key !== null &&檢查。也可以用?? ''處理像getData('batch')等回傳 null 的情況。 - baserCMS 實際案例(共 308 處):
BcFileUploader(將uploadingFiles[$bcUploadId]的 set/get 用?? ''保持一致)、BcUploadBehavior(將oldEntity[$alias][$entity->_bc_upload_id]改為?? '')、MailMessagesTable($dists[$mailField->group_valid])、SearchIndexes/Contents/PermissionsController(getData('batch') ?? '')、BcTextHelper::arrayValue/BcUtil::decodeContent($key !== null && isset(...))。
3. 自動釋放物件之資源釋放函數的棄用
這些函數的對象已改為物件,可以由 GC 自動釋放,因此明確呼叫 close/free 已被棄用。直接刪除呼叫即可。
curl_close()/curl_share_close()(CurlHandle / CurlShareHandle)finfo_close()(finfo)imagedestroy()(GdImage)← 常在圖片處理工具中使用xml_parser_free()(XMLParser)- 檢測:
grep -rnE "\(curl_close|curl_share_close|finfo_close|imagedestroy|xml_parser_free)\\s*\\(" --include="*.php"。 - baserCMS 實際案例:
Imageresizer.php・BcFileUploader.php中有 4 處imagedestroy()。雖然直接刪除也可以,但為了保留在迴圈中立即釋放記憶體的效果,建議改為unset($img)(GdImage 是物件,解除參照後 GC 就會釋放。此方法在 >=8.1 安全)。
3-2. ReflectionProperty/Method::setAccessible() 的棄用(發生數量最多)
setAccessible() 在 PHP 8.1 之後是無作用操作(可以直接用 getValue/setValue/invoke 存取 private/protected)。在 8.5 被棄用。直接刪除該行呼叫即可(>=8.1 安全)。
// Before
$property = new ReflectionProperty($obj, 'foo');
$property->setAccessible(true); // ← 刪除這一行
$property->setValue($obj, $v);
- baserCMS 實際案例: 包含 src 和測試共 33 處(
BcUtil/BcTestCase/BaserCorePlugin/PreviewController/BcDatabaseService等)。因為位於熱點路徑(如測試基底),執行時期發生數量極高(所有測試約 6,300 件,佔 8.5 棄用警告的大多數)。優先處理這個可以大幅減少日誌量。 - 批次刪除(限於單獨陳述句):
sed -i -E "/^[[:space:]]*\\$[A-Za-z0-9_]+->setAccessible\\(true\\);[[:space:]]*$/d" <file>。 - 檢測時的注意事項: 如果 grep 模式以
-開頭(例如->setAccessible),grep 會誤認為是選項而回傳 0 筆結果。請用grep -rn "setAccessible"避免以-開頭(或使用grep -e)。
4. 非數值字串的遞增已被棄用
$str++(非數值字串)已被棄用。請改用 str_increment()。※ str_increment() 是 8.3+。在維持 8.1 相容性期間,繼續使用 $str++。
// Before
$code++; // $code 是 'AZ' 等
// After
$code = str_increment($code);
5. __sleep() / __wakeup() 的軟性棄用
請改使用 __serialize() / __unserialize()(如果不需要 PHP7 相容性的話)。
6. $http_response_header 的棄用
超級全域變數 $http_response_header 已被棄用。請改用 http_get_last_response_headers()。※ http_get_last_response_headers() 是 8.4+。在維持 8.1 相容性期間,繼續使用 $http_response_header。
7. 反引號運算子的棄用
`command`(shell_exec() 的別名)已被棄用。請直接使用 shell_exec()。
8. 使用 ; 結束 case 陳述句已被棄用
case 1; 已被棄用。請改為 case 1:。
- 檢測:
grep -rnE "case\\s+.+;\\s*$" --include="*.php"(可能會有誤判,請目視確認)。
8-2. array_key_exists(null, ...) 的棄用(因 serialize 的不合法形式而引發)
array_key_exists() 的第一個參數為 null 已被棄用(請改為空字串)。如果在框架內部(CakePHP JsonView::_dataToSerialize)出現,原因是自訂程式碼的 serialize 選項不合法。如果在 setOption('serialize', ['foo', 'message' => $message]) 中把 'key' => 值 混入變數名稱列表,當 $message 為 null 時,serialize 陣列中會包含 'message' => null,進而觸發 array_key_exists(null, ...)。
- 修正: serialize 應該是檢視變數名稱的列表。請將
['foo', 'message' => $message]改為['foo', 'message'](值已經透過$this->set()設定)。baserCMS 實際案例:MailMessagesController的 index/view 動作。 - 當發生來源在框架內部時的識別方法: 暫時在 vendor 中插入計測程式碼(如
if ($key === null) file_put_contents(..., var_export($serialize,true).getTraceAsString()))→ 執行該外掛的測試 → 從堆疊追蹤的第一個自訂控制器找出問題 → 恢復 vendor。
8-3. 將物件作為 backing array 傳給 ArrayObject 已被棄用
new ArrayObject($obj) 中傳入物件(如 json_decode() 產生的 stdClass)已被棄用。請先轉為陣列。
- 修正:
new ArrayObject(json_decode($body)->devices)→new ArrayObject((array)json_decode($body)->devices)。baserCMS 實際案例:SitesControllerTest。
9. 其他(遇到時再處理)
- PDO 驅動專屬的常數/方法已遷移至命名空間版本(
PDO::MYSQL_ATTR_*→Pdo\Mysql::ATTR_*、PDO::sqliteCreateFunction()→Pdo\Sqlite::createFunction()等)。大部分是框架內部使用,但如果自訂程式碼有使用 DB 驅動常數則需要處理。PDO 的"uri:"DSN scheme 也已被棄用。※ 命名空間版本的 PDO 類別是 8.4+。在維持 8.1 相容性期間,繼續使用PDO::MYSQL_ATTR_*。 openssl_pkey_derive()的$key_length、finfo_buffer()的$context參數已被棄用(會被忽略)。- 非 CLI SAPI 中的
register_argc_argvINI 已被棄用(請改用$_GET/$_SERVER['QUERY_STRING'])。 - 對 static 閉包進行 bind 等部分閉包 binding 已被棄用。
clone null的錯誤訊息變更(影響測試): 8.4 之前為__clone method called on non-object,8.5 為clone(): Argument #1 ($object) must be of type object, null given。如果測試是透過字串比對來驗證對null執行clone的異常情況,需要更新預期值(baserCMS 實際案例:UploaderCategoriesControllerTest的 Admin/Api 各test_copy)。
⚠️ 遇到全測試執行時的連鎖錯誤,請先重新執行以釐清問題: 本程式碼基底在全量執行時,偶爾會發生「某個前置測試污染了全域狀態(如外掛設定 Configure 等)→ 之後幾乎所有測試都在 Application bootstrap 時出現
PluginCollection::create(): $config ... null given等連鎖錯誤」的暫時性污染。如果單獨執行外掛測試套件(例如plugins/baser-core/tests/TestCase)可以通過,且重新執行後恢復正常,則問題來自程式碼本身的可能性較低,而是不穩定的測試。請務必透過隔離測試與重新執行來確認。
調查流程(檢查清單)
- 使用
php -l(語法檢查)或logs/debug.log找出發生棄用警告的檔案與行號。 - 根據對應版本的章節,分類屬於哪種模式並進行修正。可以機械化取代的項目(如非正規型別轉換)可以使用 grep 批次檢測。
- 再次使用
php -l確認警告是否消失。 - 使用
--filter單獨執行相關測試進行驗證 → 最後執行所有測試完成最終確認。
相關:由框架引起的問題(事件回傳值、Association 重複、ResultSet、FormProtection 等)請參閱 cakephp-migration 技能。