Files
star-erp/.agent/skills/cross-module-communication/SKILL.md
sky121113 ad91b08dbc
All checks were successful
ERP-Deploy-Production / deploy-production (push) Successful in 1m9s
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m10s
refactor: 重構 VendorProduct API 與新增進貨單重複檢查前端邏輯
1. 將 VendorProductController 中的 Eloquent 關聯操作改為透過 ProcurementService 使用 DB 操作,解除跨模組 Model 直接依賴。
2. ProcurementService 加入 vendor product 的資料存取方法。
3. 進貨單建立前端 (GoodsReceipt/Create.tsx) 新增重複進貨檢查與警告對話框邏輯。
2026-02-25 11:11:28 +08:00

5.4 KiB
Raw Blame History

name, description
name description
跨模組調用與通訊規範 (Cross-Module Communication) 規範 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 模組修改了 SchemaSales 模組會無預警崩壞。
  • 禁止跨模組直接引入 (use) Model
    • 錯誤:在 app/Modules/Procurement/Controllers/PurchaseOrderController.php 頂端寫 use App\Modules\Inventory\Models\Warehouse;
  • 禁止跨模組直接實例化 (new) Service
    • 錯誤$inventoryService = new \App\Modules\Inventory\Services\InventoryService();

正確的跨模組調用流程:合約與依賴反轉

所有的跨模組資料交換與功能調用,必須透過介面化通訊 (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;
});