「廠商整合的 package 就放在專案裡面,外層專案已經裝了一堆套件,package 直接拿來用不是很方便嗎?何必自己在 composer.json 裡再裝一次?」
這個問題我自己也問過。答案是:方便的代價,是 package 從此不再是「獨立驗證的單元」,而是「外層專案的一部分,只是恰好放在不同資料夾」。今天要講的,是為什麼把 legacy 廠商接線拆成獨立 package 之後(昨天的主題),這個 package 還必須跟外層專案的 vendor/ 徹底隔離,不能圖方便直接依賴外層已經裝好的套件。
vendor/ 會讓 AI 改外層專案時,怎麼在不知情的狀況下破壞 packagecomposer.json、獨立測試、不碰 production code假設一個廠商整合 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,決定的不是「多裝一次套件」這種表面上的重複,而是「這個套件的版本什麼時候該變、變了會不會壞」這件事,變成一個可以被獨立追蹤、獨立驗證的問題,而不是隱藏在外層專案某次升級的副作用裡。
除了獨立的 composer.json,隔離規則還包含幾件事:
require 外層專案的檔案。這個方向性很重要——外層專案可以依賴 package,package 不能依賴外層專案,任何一次違反這個方向的依賴,都是把「本來該是單向」的關係變成雙向耦合。「一個模組不能依賴它的呼叫方已經裝好的東西,必須自己宣告完整依賴」是一個語言無關的架構原則——Java 生態的 module 系統、Node.js 專案裡 workspace 底下每個 package 各自的 package.json,講的都是同一件事:模組邊界不是資料夾邊界,而是依賴宣告的邊界。(這裡要補一個但書:npm workspace 預設會把依賴 hoist 到根目錄的 node_modules,實務上不一定真的強制這個邊界——package 可能意外 import 到自己沒宣告過、但被 hoist 上去的依賴,這種「幽靈依賴」要額外設定或改用 pnpm 這類會做嚴格隔離的工具才能真正杜絕。這不影響「依賴要靠宣告而不是資料夾位置」這個原則本身,但選工具時要知道有些預設行為沒有把這個原則落實到底。)
今天講的 composer package、composer.json 只是 PHP 生態的具體實現方式。重點不是記住 composer 特定的指令或設定寫法,而是記住這個判斷準則:任何被拆成獨立單元的程式碼,如果還在偷偷依賴外層環境已經準備好的東西,它就還不是真正獨立的單元,只是換了個資料夾位置而已。
回想你手上有沒有一段程式碼,名義上被拆成「獨立模組」,但實際上還在依賴外層專案已經裝好的套件、已經啟動好的環境?如果外層專案明天升級一個共用套件的大版本,你有把握這段「獨立模組」不會默默壞掉嗎?
vendor/ 會讓 AI 升級外層專案套件時,完全沒有訊號提醒它可能連帶破壞某個 packagecomposer.json、獨立測試套件、依賴方向只能從外層專案指向 package明天要進到依賴注入容器的陷阱:一個看起來正常運作的容器單例,其實可能悄悄快取了一個不該被快取的物件,而且這個問題只會在特定情境下才會被踩到。