iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0
IT Operation

AI 輔助開發下,測試如何保住品質防線系列 第 16

Day 16:跨版本相容,不是刻意做出來的,是「沒用到新語法」換來的

  • 分享至 

  • xImage
  •  

前言:跨版本相容,是刻意做出來的嗎?

「支援 PHP 7.1 到 8.3,程式碼裡是不是寫了一堆 if (version_compare(...)) 這種版本分支?」

翻遍 src/ 目錄,找不到任何一處版本判斷。今天來看這個套件是怎麼在完全不寫版本分支的情況下,撐住橫跨 8 個 PHP 版本的 CI 矩陣。

今日目標

  • 確認 src/ 裡沒有用到任何 PHP 8 限定語法
  • 理解「沒用新語法」跟「刻意寫相容層」是兩種完全不同的相容性策略
  • 看一個真實案例:相容性問題不是只看自己的程式碼,依賴鏈裡的其他套件也會影響你
  • 知道這種「靠不用新東西換相容性」的策略,代價會在什麼時候浮現

核對過的事實:沒有一行 PHP 8 限定語法

PHP 8.0 帶來了 union return type、建構子屬性提升;PHP 8.0 的 nullsafe operator ?->;PHP 8.0 的 match 表達式。核對 src/ 目錄底下全部檔案,這三種語法一次都沒出現過。

這代表這個套件的程式碼寫法,本身就停留在一個「PHP 7 時代也寫得出來」的風格——不是刻意為了相容性去避開新語法,比較像是這批程式碼從 2021 年寫下來之後,維護時多半是加欄位、加付款方式,沒有機會去動用到那些新語法能帶來的好處。相容性在這裡不是設計出來的成果,是程式碼風格長期沒有更新的副作用。

❌ vs ✅:兩種完全不同的相容性策略

❌ 刻意寫相容層:程式碼裡混雜版本判斷
public function getPaymentType()
{
    if (PHP_VERSION_ID >= 80000) {
        return match ($this->getChoosePayment()) {
            'Credit' => 'credit_card',
            'ATM' => 'atm',
            default => 'unknown',
        };
    }

    switch ($this->getChoosePayment()) {
        case 'Credit':
            return 'credit_card';
        case 'ATM':
            return 'atm';
        default:
            return 'unknown';
    }
}
✅ 現況:從頭到尾只寫一種寫法,天生就通吃全版本
public function getPaymentType()
{
    switch ($this->getChoosePayment()) {
        case 'Credit':
            return 'credit_card';
        case 'ATM':
            return 'atm';
        default:
            return 'unknown';
    }
}

正例(也就是 omnipay-ecpay 目前的實際寫法邏輯)不是「更聰明」,而是還沒有機會走到需要用新語法解決問題的地步。這種相容性策略的優點是零維護成本——沒有版本分支要顧,也不用擔心某個版本的分支寫錯;但代價是新語法帶來的表達力、型別安全(例如 union return type 能讓函式簽章更精確)完全沒有被用上。

相容性不是只看自己的程式碼:guzzlehttp/promises 的案例

這裡有一個值得注意的真實發現:在本機用比 CI 矩陣宣告的最高版本(8.3)更新的 PHP 8.5.10 環境跑這個套件的測試時,跑 vendor/bin/phpunit --coverage-text 會跳出這樣的訊息:

Deprecated: GuzzleHttp\Promise\queue(): Implicitly marking parameter $assign as
nullable is deprecated, the explicit nullable type must be used instead in
.../vendor/guzzlehttp/promises/src/functions.php on line 24

這個警告不是 omnipay-ecpay 自己的程式碼造成的——它是這個套件依賴鏈裡的間接套件 guzzlehttp/promises,內部某個函式參數用了「隱式標記為 nullable」的舊寫法(例如 function foo(SomeType $x = null) 而不是明確寫 ?SomeType $x = null),這種寫法從 PHP 8.4 開始被標記為已棄用。

跨版本相容性從來不是只看你自己寫的那幾個檔案,是整條依賴鏈裡,只要有任何一個套件先倒下,你的使用者體驗就會先受影響——即使 omnipay-ecpay 自己的程式碼完全沒有相容性問題,使用者在更新的 PHP 版本上執行時,還是會先看到來自別人程式碼的警告。這也是為什麼「這個套件相容性很好」這句話,永遠只能講到「以目前這個依賴版本組合來說」為止,而不是一個永遠成立的保證。

今日思考題

如果你的套件也依賴其他第三方函式庫,你有沒有定期用比你 CI 矩陣宣告的版本更新的 PHP(或其他語言的執行環境)跑一次測試,看看依賴鏈裡有沒有人先開始出現棄用警告?

今日重點回顧

  • src/ 目錄完全沒有用到 PHP 8 限定語法,相容性是「沒用新東西」換來的副作用,不是刻意設計的相容層
  • 這種策略零維護成本,但也放棄了新語法能帶來的表達力跟型別安全
  • 依賴鏈裡的間接套件(例如 guzzlehttp/promises)先出現棄用警告,會比你自己的程式碼更早讓使用者感受到相容性問題
  • 「相容性很好」永遠只是相對於目前這個依賴版本組合而言的暫時結論

明日預告

明天看另一種「承諾沒跟上現況」的樣子:README 上的徽章,連到的還是幾年前用過的 CI 服務,跟現在真正在跑的 GitHub Actions 已經是兩條不相干的線。


上一篇
Day 15:composer.json 沒寫最低 PHP 版本,相容性靠誰承諾?
下一篇
Day 17:徽章連到的 CI 服務,幾年前就沒人在用了
系列文
AI 輔助開發下,測試如何保住品質防線19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言