Communitygithub.com

PHP 版本遷移指南

針對 baserCMS 專案整理的 PHP 版本遷移指南,收錄各版本(8.2/8.4/8.5)的棄用與破壞性變更及對應修正方案。

PHP 版本遷移指南 是什麼?

這是一份專為 baserCMS 開發者設計的 PHP 版本遷移技能,涵蓋 PHP 8.2、8.4 及 8.5 的棄用功能與破壞性變更,並提供詳細的修正範例與策略。技能特別強調區分「致命錯誤」與「棄用警告」,並提供實用的正則表達式檢測模式、靜態分析工具建議(如 Rector),以及針對 baserCMS 實際案例的解決方案(例如 `imagedestroy()` 取代為 `unset()`、`setAccessible()` 刪除等)。此外,技能也考慮到同時維持 PHP 8.1 相容性的需求,標示出部分僅能在 8.3+ 或 8.4+ 使用的替代方案,並提供完整的調查檢查清單。此技能支援所有主流 AI 代理工具(Claude Code、Cursor、Codex 等),能夠有效引導開發者快速遷移至新 PHP 版本。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add baserproject/baser-skills

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

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_argv INI 已被棄用(請改用 $_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)可以通過,且重新執行後恢復正常,則問題來自程式碼本身的可能性較低,而是不穩定的測試。請務必透過隔離測試與重新執行來確認。


調查流程(檢查清單)

  1. 使用 php -l(語法檢查)或 logs/debug.log 找出發生棄用警告的檔案與行號。
  2. 根據對應版本的章節,分類屬於哪種模式並進行修正。可以機械化取代的項目(如非正規型別轉換)可以使用 grep 批次檢測。
  3. 再次使用 php -l 確認警告是否消失。
  4. 使用 --filter 單獨執行相關測試進行驗證 → 最後執行所有測試完成最終確認。

相關:由框架引起的問題(事件回傳值、Association 重複、ResultSet、FormProtection 等)請參閱 cakephp-migration 技能。


參考資料

相關技能