laravel-patterns

laravel-patterns

熱門

Laravel 架構模式、路由/控制器、Eloquent ORM、服務層、佇列、事件、快取以及 API 資源,適用於正式環境應用程式。

23萬星標
3.5萬分支
更新於 2026/7/14
SKILL.md
readonlyread-only
name
laravel-patterns
description

Laravel 架構模式、路由/控制器、Eloquent ORM、服務層、佇列、事件、快取以及 API 資源,適用於正式環境應用程式。

Laravel 開發模式

適用於可擴展、易維護應用程式的正式環境 Laravel 架構模式。

使用時機

  • 建置 Laravel 網頁應用程式或 API
  • 組織控制器、服務與領域邏輯
  • 使用 Eloquent 模型與關聯
  • 使用資源與分頁設計 API
  • 加入佇列、事件、快取與背景工作

運作方式

  • 以明確邊界組織應用程式(控制器 -> 服務/動作 -> 模型)。
  • 使用明確綁定與範圍綁定保持路由可預測;仍須強制授權以進行存取控制。
  • 偏好型別模型、型別轉換與範圍,保持領域邏輯一致。
  • 將 I/O 密集型工作放入佇列,並快取昂貴的讀取操作。
  • 集中設定於 config/*,並保持環境明確。

範例

專案結構

使用傳統 Laravel 佈局,具有清晰的層級邊界(HTTP、服務/動作、模型)。

建議佈局

app/
├── Actions/            # 單一用途的使用案例
├── Console/
├── Events/
├── Exceptions/
├── Http/
│   ├── Controllers/
│   ├── Middleware/
│   ├── Requests/       # 表單請求驗證
│   └── Resources/      # API 資源
├── Jobs/
├── Models/
├── Policies/
├── Providers/
├── Services/           # 協調領域服務
└── Support/
config/
database/
├── factories/
├── migrations/
└── seeders/
resources/
├── views/
└── lang/
routes/
├── api.php
├── web.php
└── console.php

控制器 -> 服務 -> 動作

保持控制器輕薄。將編排邏輯放在服務中,單一用途邏輯放在動作中。

final class CreateOrderAction
{
    public function __construct(private OrderRepository $orders) {}

    public function handle(CreateOrderData $data): Order
    {
        return $this->orders->create($data);
    }
}

final class OrdersController extends Controller
{
    public function __construct(private CreateOrderAction $createOrder) {}

    public function store(StoreOrderRequest $request): JsonResponse
    {
        $order = $this->createOrder->handle($request->toDto());

        return response()->json([
            'success' => true,
            'data' => OrderResource::make($order),
            'error' => null,
            'meta' => null,
        ], 201);
    }
}

路由與控制器

偏好路由模型綁定與資源控制器以保持清晰。

use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('projects', ProjectController::class);
});

路由模型綁定(範圍綁定)

使用範圍綁定防止跨租戶存取。

Route::scopeBindings()->group(function () {
    Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});

巢狀路由與綁定名稱

  • 保持前綴與路徑一致,避免雙重巢狀(例如 conversationconversations)。
  • 使用與綁定模型相符的單一參數名稱(例如 {conversation} 對應 Conversation)。
  • 巢狀時偏好使用範圍綁定以強制父子關係。
use App\Http\Controllers\Api\ConversationController;
use App\Http\Controllers\Api\MessageController;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->prefix('conversations')->group(function () {
    Route::post('/', [ConversationController::class, 'store'])->name('conversations.store');

    Route::scopeBindings()->group(function () {
        Route::get('/{conversation}', [ConversationController::class, 'show'])
            ->name('conversations.show');

        Route::post('/{conversation}/messages', [MessageController::class, 'store'])
            ->name('conversation-messages.store');

        Route::get('/{conversation}/messages/{message}', [MessageController::class, 'show'])
            ->name('conversation-messages.show');
    });
});

如果你希望參數解析為不同的模型類別,請定義明確綁定。如需自訂綁定邏輯,請使用 Route::bind() 或在模型上實作 resolveRouteBinding()

use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;

Route::model('conversation', AiConversation::class);

服務容器綁定

在服務提供者中將介面綁定到實作,以進行清晰的依賴注入。

use App\Repositories\EloquentOrderRepository;
use App\Repositories\OrderRepository;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->bind(OrderRepository::class, EloquentOrderRepository::class);
    }
}

Eloquent 模型模式

模型設定

final class Project extends Model
{
    use HasFactory;

    protected $fillable = ['name', 'owner_id', 'status'];

    protected $casts = [
        'status' => ProjectStatus::class,
        'archived_at' => 'datetime',
    ];

    public function owner(): BelongsTo
    {
        return $this->belongsTo(User::class, 'owner_id');
    }

    public function scopeActive(Builder $query): Builder
    {
        return $query->whereNull('archived_at');
    }
}

自訂型別轉換與值物件

使用列舉或值物件以實現嚴格的型別檢查。

use Illuminate\Database\Eloquent\Casts\Attribute;

protected $casts = [
    'status' => ProjectStatus::class,
];
protected function budgetCents(): Attribute
{
    return Attribute::make(
        get: fn (int $value) => Money::fromCents($value),
        set: fn (Money $money) => $money->toCents(),
    );
}

預先載入以避免 N+1 問題

$orders = Order::query()
    ->with(['customer', 'items.product'])
    ->latest()
    ->paginate(25);

用於複雜篩選的查詢物件

final class ProjectQuery
{
    public function __construct(private Builder $query) {}

    public function ownedBy(int $userId): self
    {
        $query = clone $this->query;

        return new self($query->where('owner_id', $userId));
    }

    public function active(): self
    {
        $query = clone $this->query;

        return new self($query->whereNull('archived_at'));
    }

    public function builder(): Builder
    {
        return $this->query;
    }
}

全域範圍與軟刪除

使用全域範圍進行預設篩選,使用 SoftDeletes 處理可復原的記錄。
對於相同的篩選條件,請使用全域範圍或命名範圍其中一種,除非你打算使用分層行為。

use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Builder;

final class Project extends Model
{
    use SoftDeletes;

    protected static function booted(): void
    {
        static::addGlobalScope('active', function (Builder $builder): void {
            $builder->whereNull('archived_at');
        });
    }
}

用於可重複使用篩選的查詢範圍

use Illuminate\Database\Eloquent\Builder;

final class Project extends Model
{
    public function scopeOwnedBy(Builder $query, int $userId): Builder
    {
        return $query->where('owner_id', $userId);
    }
}

// 在服務、儲存庫等中使用
$projects = Project::ownedBy($user->id)->get();

多步驟更新的交易

use Illuminate\Support\Facades\DB;

DB::transaction(function (): void {
    $order->update(['status' => 'paid']);
    $order->items()->update(['paid_at' => now()]);
});

遷移

命名慣例

  • 檔案名稱使用時間戳記:YYYY_MM_DD_HHMMSS_create_users_table.php
  • 遷移使用匿名類別(無命名類別);檔案名稱傳達意圖
  • 資料表名稱預設為 snake_case 且為複數

遷移範例

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('orders', function (Blueprint $table): void {
            $table->id();
            $table->foreignId('customer_id')->constrained()->cascadeOnDelete();
            $table->string('status', 32)->index();
            $table->unsignedInteger('total_cents');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('orders');
    }
};

表單請求與驗證

將驗證邏輯保留在表單請求中,並將輸入轉換為 DTO。

use App\Models\Order;

final class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()?->can('create', Order::class) ?? false;
    }

    public function rules(): array
    {
        return [
            'customer_id' => ['required', 'integer', 'exists:customers,id'],
            'items' => ['required', 'array', 'min:1'],
            'items.*.sku' => ['required', 'string'],
            'items.*.quantity' => ['required', 'integer', 'min:1'],
        ];
    }

    public function toDto(): CreateOrderData
    {
        return new CreateOrderData(
            customerId: (int) $this->validated('customer_id'),
            items: $this->validated('items'),
        );
    }
}

API 資源

使用資源與分頁保持 API 回應一致。

$projects = Project::query()->active()->paginate(25);

return response()->json([
    'success' => true,
    'data' => ProjectResource::collection($projects->items()),
    'error' => null,
    'meta' => [
        'page' => $projects->currentPage(),
        'per_page' => $projects->perPage(),
        'total' => $projects->total(),
    ],
]);

事件、工作與佇列

  • 發出領域事件以處理副作用(電子郵件、分析)
  • 使用佇列工作處理耗時操作(報表、匯出、Webhook)
  • 偏好具有重試與退避機制的冪等處理器

快取

  • 快取讀取密集的端點與昂貴的查詢
  • 在模型事件(created/updated/deleted)時使快取失效
  • 快取相關資料時使用標籤以便於失效

設定與環境

  • 將機密資訊放在 .env,設定放在 config/*.php
  • 使用每個環境的設定覆蓋,並在正式環境中使用 config:cache