--- trigger: always_on --- --- name: 跨模組調用與通訊規範 (Cross-Module Communication) description: 規範 Laravel Modular Monolith 架構下,不同業務模組中如何彼此調用資料與邏輯,包含禁止項目、Interface 實作、與 Service 綁定規則。 --- # 跨模組調用與通訊規範 (Cross-Module Communication) 為了確保專案的「模組化單體架構 (Modular Monolith)」的獨立性與可維護性,當遇到**需要跨越不同業務模組存取資料或調用功能**的情境時,請嚴格遵守以下規範。 ## 🚫 絕對禁止的行為 (Strict Prohibitions) * **禁止跨模組 Eloquent 關聯(例外除外)** * **錯誤**:在 `app/Modules/Sales/Models/Order.php` 中撰寫 `public function product() { return $this->belongsTo(\App\Modules\Inventory\Models\Product::class); }`。 * **原因**:這會造成資料庫查詢的強耦合。如果 `Inventory` 模組修改了 Schema,`Sales` 模組會無預警崩壞。 * **禁止跨模組直接引入 (use) Model** * **錯誤**:在 `app/Modules/Procurement/Controllers/PurchaseOrderController.php` 頂端寫 `use App\Modules\Inventory\Models\Warehouse;`。 * **禁止跨模組直接實例化 (new) Service** * **錯誤**:`$inventoryService = new \App\Modules\Inventory\Services\InventoryService();` --- ## 🌟 允許的全域例外 (Global Exceptions) 雖然我們嚴格禁止跨模組直接相依,但為了開發效率與框架機制的完整性,**`Core` 模組下的特定基礎設施模型 (Infrastructure Models) 被視為全域例外**。 其他業務模組 **可以** 透過 Eloquent (`belongsTo` / `hasMany`) 直接關聯以下 Model: 1. **`App\Modules\Core\Models\User`**:因為幾乎所有表都有 `created_by` / `updated_by`,直接關聯可保留 `with('creator')` 等便利性。 2. **`App\Modules\Core\Models\Role`**:權限判定已深度整合至系統底層。 3. **`App\Modules\Core\Models\Tenant`**:多租戶架構 (Tenancy) 的核心基石,底層查詢會頻繁依賴。 > **⚠️ 注意**:這項例外是單向的。`Core` 模組內的業務邏輯(如 `DashboardController`)**絕對不能**反過來直接 `use` 外部業務模組的 Model,仍必須透過外部模組的 Service Interface 來索取資料。 --- ## ✅ 正確的跨模組調用流程:合約與依賴反轉 所有的跨模組資料交換與功能調用,必須透過**介面化通訊 (Contracts)** 進行。 ### Step 1: 在被調用的模組定義合約 (Interface) 如果 `Inventory` 模組需要提供功能給外部使用,請在 `app/Modules/Inventory/Contracts/` 建立 Interface 檔案。 ```php // app/Modules/Inventory/Contracts/InventoryServiceInterface.php namespace App\Modules\Inventory\Contracts; use Illuminate\Support\Collection; interface InventoryServiceInterface { /** * 取得可用的倉庫清單 * * @return Collection 包含每個倉庫的 id, name, code 等基本資料 */ public function getActiveWarehouses(): Collection; } ``` ### Step 2: 實作介面並在自己模組的 ServiceProvider 註冊 由 `Inventory` 模組自己的 Service 來實作上述介面。 ```php // app/Modules/Inventory/Services/InventoryService.php namespace App\Modules\Inventory\Services; use App\Modules\Inventory\Contracts\InventoryServiceInterface; use App\Modules\Inventory\Models\Warehouse; use Illuminate\Support\Collection; class InventoryService implements InventoryServiceInterface { public function getActiveWarehouses(): Collection { // 建議只取出需要的欄位,或者轉換為 DTO / 陣列 // 避免將完整的 Eloquent Model 實例拋出模組外 return Warehouse::where('is_active', true) ->select(['id', 'name', 'code']) ->get(); } } ``` 然後進入 `app/Modules/Inventory/InventoryServiceProvider.php` 完成綁定: ```php // app/Modules/Inventory/InventoryServiceProvider.php namespace App\Modules\Inventory; use Illuminate\Support\ServiceProvider; use App\Modules\Inventory\Contracts\InventoryServiceInterface; use App\Modules\Inventory\Services\InventoryService; class InventoryServiceProvider extends ServiceProvider { public function register(): void { // 綁定介面與實體 $this->app->bind(InventoryServiceInterface::class, InventoryService::class); } } ``` ### Step 3: 調用方透過依賴注入 (DI) 使用服務 當 `Procurement` 模組需要取得倉庫資料時,禁止直接 new 服務或呼叫倉庫 Model。必須透過**建構子注入**或**方法注入**取得 `InventoryServiceInterface`。 ```php // app/Modules/Procurement/Controllers/PurchaseOrderController.php namespace App\Modules\Procurement\Controllers; use App\Http\Controllers\Controller; use App\Modules\Inventory\Contracts\InventoryServiceInterface; use Inertia\Inertia; class PurchaseOrderController extends Controller { // 透過建構子注入介面 public function __construct( protected InventoryServiceInterface $inventoryService ) {} public function create() { // 僅能呼叫介面有定義的方法 $warehouses = $this->inventoryService->getActiveWarehouses(); return Inertia::render('Procurement/PurchaseOrder/Create', [ 'warehouses' => $warehouses ]); } } ``` --- ## ⚠️ 跨模組資料回傳的注意事項 (Data Hydration) * **回傳純粹資料**:為了防止其他模組意外觸發 Lazy Loading (`$item->product->name`),請盡量在 Service 中就用 `with()` 載入好關聯,或者直接轉為原生的 Array、`stdClass`、或具體的 DTO。 * **手動組合 (Manual Hydration)**:若某個頁面需要合併兩個模組的資料,這也是被允許的,但必須在 Controller 層級呼叫兩個不同的 Service Interface 後,手動合併。 ### 範例:手動合併資料 ```php // 錯誤示範(禁止在 OrderService 中去查使用者的關聯) $orders = Order::with('user')->get(); // 如果 user 表在 Core 模組,這是不允許的 // 正確示範:在各自模組取資料,並手動組裝 $orders = $this->orderService->getOrders(); $userIds = $orders->pluck('user_id')->unique()->toArray(); $users = $this->coreUserService->getUsersByIds($userIds)->keyBy('id'); $mergedData = $orders->map(function ($order) use ($users) { // 將使用者資料手動附加上去 $order->user_name = $users->get($order->user_id)->name ?? 'Unknown'; return $order; }); ```