All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 57s
- 依循跨模組通訊規範,將 Sales 與 Production 模組中對 Inventory 的直接模型關聯改為透過 InventoryServiceInterface 取得 - 於 InventoryService 實作獲取最高庫存價值、即將過期商品等方法,供儀表板使用 - 確保所有跨模組調用皆採用手動水和(Manual Hydration)方式組合資料 - 移除本地已歸檔的 .agent 規範檔案
6.4 KiB
6.4 KiB
trigger
| 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:
App\Modules\Core\Models\User:因為幾乎所有表都有created_by/updated_by,直接關聯可保留with('creator')等便利性。App\Modules\Core\Models\Role:權限判定已深度整合至系統底層。App\Modules\Core\Models\Tenant:多租戶架構 (Tenancy) 的核心基石,底層查詢會頻繁依賴。
⚠️ 注意:這項例外是單向的。
Core模組內的業務邏輯(如DashboardController)絕對不能反過來直接use外部業務模組的 Model,仍必須透過外部模組的 Service Interface 來索取資料。
✅ 正確的跨模組調用流程:合約與依賴反轉
所有的跨模組資料交換與功能調用,必須透過介面化通訊 (Contracts) 進行。
Step 1: 在被調用的模組定義合約 (Interface)
如果 Inventory 模組需要提供功能給外部使用,請在 app/Modules/Inventory/Contracts/ 建立 Interface 檔案。
// 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 來實作上述介面。
// 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 完成綁定:
// 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。
// 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 後,手動合併。
範例:手動合併資料
// 錯誤示範(禁止在 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;
});