Laravel 開發模式 是做什麼的?
適用於生產環境的 Laravel 架構模式,幫助建置可擴展、可維護的應用程式。
使用時機
- 建置 Laravel Web 應用程式或 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']);
});
巢狀路由和綁定名稱
- 保持前綴和路徑一致,以避免雙重巢狀(例如,
conversation與conversations)。 - 使用與綁定模型相符的單一參數名稱(例如,
{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