iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Claude AI

用 AI Agent 重構一套無框架的 legacy PHP 系統系列 第 12

Day 12:package 隔離規則——為什麼廠商 package 不能依賴外層專案的 vendor

  • 分享至 

  • xImage
  •  

前言:反正都在同一個 repo,共用套件不是很正常嗎?

「廠商整合的 package 就放在專案裡面,外層專案已經裝了一堆套件,package 直接拿來用不是很方便嗎?何必自己在 composer.json 裡再裝一次?」

這個問題我自己也問過。答案是:方便的代價,是 package 從此不再是「獨立驗證的單元」,而是「外層專案的一部分,只是恰好放在不同資料夾」。今天要講的,是為什麼把 legacy 廠商接線拆成獨立 package 之後(昨天的主題),這個 package 還必須跟外層專案的 vendor/ 徹底隔離,不能圖方便直接依賴外層已經裝好的套件。

今日目標

  • 理解「package 放在專案裡」跟「package 真的獨立」之間的落差
  • 看清楚共用 vendor/ 會讓 AI 改外層專案時,怎麼在不知情的狀況下破壞 package
  • 認識 package 隔離規則的具體做法:獨立 composer.json、獨立測試、不碰 production code
  • 建立「模組邊界」這個語言無關的架構原則,跟「composer package」這個 PHP 具體實現之間的分層認知

共用 vendor 會讓 AI 改壞你沒在看的東西

假設一個廠商整合 package 直接 use 外層專案 vendor/ 裡裝好的 HTTP client 函式庫,而不是自己在 package 的 composer.json 宣告依賴。表面上省了一次安裝,但實際上這個 package 的行為,從此跟著外層專案的 composer.json 一起漂移。

危險的地方在於:升級外層專案某個共用套件版本,這個動作本身完全不會觸發任何跟 package 相關的警訊。AI 在處理外層專案的升級任務時,看到的是外層專案自己的測試套件——package 目錄下如果沒有獨立測試、或測試沒有被納入這次驗證範圍,AI 完全沒有理由知道「這個套件的某個行為變了,會連帶影響到一個它甚至不知道存在依賴關係的 package」。

這正是 Day 01 那句話的另一種樣貌:AI 對「這次升級沒有破壞任何東西」的確認,只涵蓋了它當下驗證的範圍——如果 package 沒有被獨立測試覆蓋、也沒有被明確標示出依賴關係,它自然不在這個確認範圍裡,卻可能因此靜靜壞掉。

用一組對照來看這個差異:

❌ package 依賴外層專案的 vendor:
// package 內部程式碼直接 use 外層專案 vendor/ 裝好的類別
use Vendor\Package\SomeHttpClient;
// package 自己沒有 composer.json,或有但沒宣告這個依賴
→ 外層專案升級 SomeHttpClient 大版本時,
  完全沒有訊號告訴任何人這個 package 會受影響
  ——除非剛好有人手動想起來去跑一次 package 的測試

✅ package 有自己的 composer.json、自己的依賴宣告:
// package/composer.json
{
  "require": {
    "psr/http-client": "^1.0",
    "some-vendor/http-client": "^2.0"
  }
}
// package 自己的測試套件驗證自己的行為
→ 升級外層專案的套件版本,不會動到 package 的
  composer.lock;要升級 package 自己的依賴,
  必須明確改 package 的 composer.json,
  這個改動本身就會觸發 package 自己的測試

package 有沒有自己的 composer.json,決定的不是「多裝一次套件」這種表面上的重複,而是「這個套件的版本什麼時候該變、變了會不會壞」這件事,變成一個可以被獨立追蹤、獨立驗證的問題,而不是隱藏在外層專案某次升級的副作用裡。

package 隔離規則的具體做法

除了獨立的 composer.json,隔離規則還包含幾件事:

  • package 不能碰外層專案的 production code:package 只能透過自己定義的介面(例如昨天講的 Client/Config 分層)被外層專案呼叫,不能反過來 require 外層專案的檔案。這個方向性很重要——外層專案可以依賴 package,package 不能依賴外層專案,任何一次違反這個方向的依賴,都是把「本來該是單向」的關係變成雙向耦合。
  • package 自己的測試套件必須能獨立跑:不依賴外層專案的測試資料庫、不依賴外層專案的啟動流程。這樣才能在 CI 裡把 package 的驗證跟外層專案的驗證分開執行,也才能讓「這個 package 的行為有沒有壞」這件事,有一個明確、獨立的答案。
  • 廠商回傳的原始資料格式不做語意轉換:package 只負責忠實呈現廠商 API 的原始行為,業務語意的轉換留給外層專案的呼叫端處理。這條規則某種程度上也是隔離的延伸——package 的職責邊界劃得越清楚,它需要跟外層專案共享的假設就越少。

原則語言無關,composer package 是具體實現

「一個模組不能依賴它的呼叫方已經裝好的東西,必須自己宣告完整依賴」是一個語言無關的架構原則——Java 生態的 module 系統、Node.js 專案裡 workspace 底下每個 package 各自的 package.json,講的都是同一件事:模組邊界不是資料夾邊界,而是依賴宣告的邊界。(這裡要補一個但書:npm workspace 預設會把依賴 hoist 到根目錄的 node_modules,實務上不一定真的強制這個邊界——package 可能意外 import 到自己沒宣告過、但被 hoist 上去的依賴,這種「幽靈依賴」要額外設定或改用 pnpm 這類會做嚴格隔離的工具才能真正杜絕。這不影響「依賴要靠宣告而不是資料夾位置」這個原則本身,但選工具時要知道有些預設行為沒有把這個原則落實到底。)

今天講的 composer package、composer.json 只是 PHP 生態的具體實現方式。重點不是記住 composer 特定的指令或設定寫法,而是記住這個判斷準則:任何被拆成獨立單元的程式碼,如果還在偷偷依賴外層環境已經準備好的東西,它就還不是真正獨立的單元,只是換了個資料夾位置而已。

今日思考題

回想你手上有沒有一段程式碼,名義上被拆成「獨立模組」,但實際上還在依賴外層專案已經裝好的套件、已經啟動好的環境?如果外層專案明天升級一個共用套件的大版本,你有把握這段「獨立模組」不會默默壞掉嗎?

今日重點回顧

  • package 放在專案資料夾裡,不代表它真的獨立——真正的隔離看的是依賴宣告,不是實體位置
  • 共用 vendor/ 會讓 AI 升級外層專案套件時,完全沒有訊號提醒它可能連帶破壞某個 package
  • 隔離規則具體做法:獨立 composer.json、獨立測試套件、依賴方向只能從外層專案指向 package
  • 「模組邊界靠依賴宣告,不靠資料夾」是語言無關的架構原則,composer package 只是 PHP 的實現方式

明日預告

明天要進到依賴注入容器的陷阱:一個看起來正常運作的容器單例,其實可能悄悄快取了一個不該被快取的物件,而且這個問題只會在特定情境下才會被踩到。


上一篇
Day 11:廠商 API 遷移實戰——legacy 接線如何拆成獨立 package
下一篇
Day 13:依賴注入容器的陷阱——單例快取了不該快取的物件
系列文
用 AI Agent 重構一套無框架的 legacy PHP 系統13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言