laravel-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Laravel Development Patterns

Laravel开发模式

Production-grade Laravel architecture patterns for scalable, maintainable applications.
适用于可扩展、可维护应用的生产级Laravel架构模式。

When to Use

适用场景

  • Building Laravel web applications or APIs
  • Structuring controllers, services, and domain logic
  • Working with Eloquent models and relationships
  • Designing APIs with resources and pagination
  • Adding queues, events, caching, and background jobs
  • 构建Laravel Web应用或API
  • 规划控制器、服务与领域逻辑结构
  • 处理Eloquent模型及关联关系
  • 基于资源与分页设计API
  • 添加队列、事件、缓存及后台任务

How It Works

工作原理

  • Structure the app around clear boundaries (controllers -> services/actions -> models).
  • Use explicit bindings and scoped bindings to keep routing predictable; still enforce authorization for access control.
  • Favor typed models, casts, and scopes to keep domain logic consistent.
  • Keep IO-heavy work in queues and cache expensive reads.
  • Centralize config in
    config/*
    and keep environments explicit.
  • 围绕清晰的边界规划应用架构(控制器 -> 服务/动作 -> 模型)。
  • 使用显式绑定和作用域绑定确保路由可预测;同时通过授权机制管控访问权限。
  • 优先使用类型化模型、转换与作用域保证领域逻辑一致性。
  • 将IO密集型工作放入队列,对开销大的读取操作进行缓存。
  • 集中配置于
    config/*
    目录,明确区分不同环境配置。

Examples

示例

Project Structure

项目结构

Use a conventional Laravel layout with clear layer boundaries (HTTP, services/actions, models).
使用常规Laravel布局,确保各层边界清晰(HTTP层、服务/动作层、模型层)。

Recommended Layout

推荐布局

app/
├── Actions/            # Single-purpose use cases
├── Console/
├── Events/
├── Exceptions/
├── Http/
│   ├── Controllers/
│   ├── Middleware/
│   ├── Requests/       # Form request validation
│   └── Resources/      # API resources
├── Jobs/
├── Models/
├── Policies/
├── Providers/
├── Services/           # Coordinating domain services
└── Support/
config/
database/
├── factories/
├── migrations/
└── seeders/
resources/
├── views/
└── lang/
routes/
├── api.php
├── web.php
└── console.php
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

Controllers -> Services -> Actions

控制器 -> 服务 -> 动作

Keep controllers thin. Put orchestration in services and single-purpose logic in actions.
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);
    }
}
保持控制器轻量化,将编排逻辑放入服务,单一功能逻辑放入动作。
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);
    }
}

Routing and Controllers

路由与控制器

Prefer route-model binding and resource controllers for clarity.
php
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->group(function () {
    Route::apiResource('projects', ProjectController::class);
});
优先使用路由模型绑定和资源控制器以提升清晰度。
php
use Illuminate\Support\Facades\Route;

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

Route Model Binding (Scoped)

作用域路由模型绑定

Use scoped bindings to prevent cross-tenant access.
php
Route::scopeBindings()->group(function () {
    Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});
使用作用域绑定防止跨租户访问。
php
Route::scopeBindings()->group(function () {
    Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']);
});

Nested Routes and Binding Names

嵌套路由与绑定名称

  • Keep prefixes and paths consistent to avoid double nesting (e.g.,
    conversation
    vs
    conversations
    ).
  • Use a single parameter name that matches the bound model (e.g.,
    {conversation}
    for
    Conversation
    ).
  • Prefer scoped bindings when nesting to enforce parent-child relationships.
php
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');
    });
});
If you want a parameter to resolve to a different model class, define explicit binding. For custom binding logic, use
Route::bind()
or implement
resolveRouteBinding()
on the model.
php
use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;

Route::model('conversation', AiConversation::class);
  • 保持前缀和路径一致,避免重复嵌套(例如:
    conversation
    vs
    conversations
    )。
  • 使用与绑定模型匹配的单一参数名称(例如:
    Conversation
    模型对应
    {conversation}
    )。
  • 嵌套时优先使用作用域绑定,以强制父子关系。
php
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()
方法。
php
use App\Models\AiConversation;
use Illuminate\Support\Facades\Route;

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

Service Container Bindings

服务容器绑定

Bind interfaces to implementations in a service provider for clear dependency wiring.
php
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);
    }
}
在服务提供者中将接口绑定到实现类,实现清晰的依赖注入。
php
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 Model Patterns

Eloquent模型模式

Model Configuration

模型配置

php
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');
    }
}
php
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');
    }
}

Custom Casts and Value Objects

自定义转换与值对象

Use enums or value objects for strict typing.
php
use Illuminate\Database\Eloquent\Casts\Attribute;

protected $casts = [
    'status' => ProjectStatus::class,
];
php
protected function budgetCents(): Attribute
{
    return Attribute::make(
        get: fn (int $value) => Money::fromCents($value),
        set: fn (Money $money) => $money->toCents(),
    );
}
使用枚举或值对象实现严格类型化。
php
use Illuminate\Database\Eloquent\Casts\Attribute;

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

Eager Loading to Avoid N+1

预加载避免N+1查询

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

Query Objects for Complex Filters

复杂过滤的查询对象

php
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;
    }
}
php
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;
    }
}

Global Scopes and Soft Deletes

全局作用域与软删除

Use global scopes for default filtering and
SoftDeletes
for recoverable records. Use either a global scope or a named scope for the same filter, not both, unless you intend layered behavior.
php
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');
        });
    }
}
使用全局作用域实现默认过滤,使用
SoftDeletes
实现可恢复的记录删除。 同一过滤逻辑请使用全局作用域或命名作用域其中一种,除非需要分层行为。
php
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');
        });
    }
}

Query Scopes for Reusable Filters

可复用过滤的查询作用域

php
use Illuminate\Database\Eloquent\Builder;

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

// In service, repository etc.
$projects = Project::ownedBy($user->id)->get();
php
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();

Transactions for Multi-Step Updates

多步骤更新的事务

php
use Illuminate\Support\Facades\DB;

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

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

Migrations

迁移

Naming Convention

命名规范

  • File names use timestamps:
    YYYY_MM_DD_HHMMSS_create_users_table.php
  • Migrations use anonymous classes (no named class); the filename communicates intent
  • Table names are
    snake_case
    and plural by default
  • 文件名使用时间戳:
    YYYY_MM_DD_HHMMSS_create_users_table.php
  • 迁移使用匿名类(无需命名类);文件名需清晰表达意图
  • 表名默认使用蛇形命名法且为复数形式

Example Migration

迁移示例

php
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');
    }
};
php
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');
    }
};

Form Requests and Validation

表单请求与验证

Keep validation in form requests and transform inputs to DTOs.
php
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'),
        );
    }
}
将验证逻辑放入表单请求,并将输入转换为DTO。
php
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 Resources

API资源

Keep API responses consistent with resources and pagination.
php
$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(),
    ],
]);
通过资源与分页保持API响应一致性。
php
$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(),
    ],
]);

Events, Jobs, and Queues

事件、任务与队列

  • Emit domain events for side effects (emails, analytics)
  • Use queued jobs for slow work (reports, exports, webhooks)
  • Prefer idempotent handlers with retries and backoff
  • 触发领域事件处理副作用(如邮件发送、数据分析)
  • 使用队列任务处理耗时操作(如报表生成、导出、Webhook调用)
  • 优先使用具有重试与退避机制的幂等处理器

Caching

缓存

  • Cache read-heavy endpoints and expensive queries
  • Invalidate caches on model events (created/updated/deleted)
  • Use tags when caching related data for easy invalidation
  • 对读取密集型接口和开销大的查询进行缓存
  • 在模型事件(创建/更新/删除)时失效缓存
  • 缓存关联数据时使用标签,便于批量失效

Configuration and Environments

配置与环境

  • Keep secrets in
    .env
    and config in
    config/*.php
  • Use per-environment config overrides and
    config:cache
    in production
  • 将敏感信息存储在
    .env
    文件,配置项存储在
    config/*.php
    文件中
  • 针对不同环境使用配置覆盖,生产环境使用
    config:cache
    优化