Laravel 生产级架构模式完整指南:分层目录、Eloquent ORM、队列事件与 API 设计(ECC laravel-patterns)
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
本篇技术指南系统讲解 ECC(The agent harness performance optimization system)仓库中laravel-patterns技能文档所沉淀的 Laravel 生产级架构实践,覆盖从目录分层、控制器/服务/Action 职责划分,到路由模型绑定、Eloquent 模型模式、迁移、Form Request 校验、API Resources,以及队列事件与缓存配置的完整落地方案。读者读完将掌握一套可直接应用于 Laravel Web 应用与 API 项目的分层架构规范,并了解该技能在 ECC 项目中与 PHP 审查代理、技术栈识别及项目模板是如何联动生效的。
laravel-patterns是 ECC 为「构建或评审 Laravel 应用」提供的一等技能资产,其源文件保存在 skills/laravel-patterns/SKILL.md,并同步维护了西班牙语(docs/es/skills/laravel-patterns/SKILL.md)等多语言版本。围绕同一主题,仓库还配齐了 laravel-security、laravel-tdd、laravel-verification 与 laravel-plugin-discovery 等技能,构成完整的 Laravel 工程化闭环。
技能定位:何时使用 laravel-patterns
根据技能文档的description与When to Use一节,laravel-patterns面向四类核心场景:
- 构建 Laravel Web 应用或 API:从零搭建或重构时以此为架构基线;
- 组织控制器、服务与领域逻辑:回答「业务逻辑该放哪一层」这一高频问题;
- 使用 Eloquent 模型与关系:规范模型配置、cast、scope 与关联预加载;
- 设计基于 Resources 与分页的 API:统一响应结构与序列化形态;
- 引入队列、事件、缓存与后台 Job:为耗时的副作用逻辑选择正确的异步载体。
在 ECC 的整套工程体系中,这一技能被显式挂载到 PHP/Laravel 项目栈之上。见 config/project-stack-mappings.json 中的php-laravel映射条目:当项目目录出现composer.json、artisan、composer.lock等指示文件时,即判定为 PHP/Laravel 栈,并同时装载laravel-patterns、laravel-tdd、laravel-verification、laravel-security、tdd-workflow等技能与对应命令。换言之,当 ECC 在某个 Laravel 仓库中运行时,本节介绍的架构原则会自动成为编码与评审的默认参照。底层框架识别逻辑见 scripts/lib/project-detect.js:framework: 'laravel', language: 'php', markers: ['artisan'], packageKeys: ['laravel/framework']。
项目结构与分层边界
技能文档强调使用「带清晰层边界的常规 Laravel 布局」(HTTP、services/actions、models 三层)。推荐布局如下:
app/ ├── Actions/ # 单一用途用例 ├── Console/ ├── Events/ ├── Exceptions/ ├── Http/ │ ├── Controllers/ │ ├── Middleware/ │ ├── Requests/ # Form Request 校验 │ └── Resources/ # API resources ├── Jobs/ ├── Models/ ├── Policies/ ├── Providers/ ├── Services/ # 协调性领域服务 └── Support/ config/ database/ ├── factories/ ├── migrations/ └── seeders/ resources/ ├── views/ └── lang/ routes/ ├── api.php ├── web.php └── console.php这套结构与 ECC 附带的真实 Laravel 项目模板 examples/laravel-api-CLAUDE.md 中的 File Structure 完全一致,印证其可直接落地。其背后的核心原则是:
- Controller 只做传输层的事:鉴权、校验、序列化、状态码,不承载业务规则;
- Service 承担协调职责:编排多个 Action 或多个仓储完成一个完整业务流程(如「下单」);
- Action 封装单一目的用例:一个 Action 只回答一个问题,便于独立测试与复用。
在 rules/php/patterns.md 中,这一原则被提炼为“Thin Controllers, Explicit Services”:控制器聚焦 transport,业务规则下沉到无需 HTTP 启动即可测试的应用/领域服务中。
Controller → Service → Action 的调用链实现
技能文档给出的示例将「创建订单」拆为 Action 与 Controller 两层:
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); } }值得注意的写法细节:
- 构造器注入:Controller 与 Action 均通过构造函数注入依赖(
OrderRepository、CreateOrderAction),完全交给 Laravel 服务容器自动解析,避免app()服务定位器式调用,这与 rules/php/patterns.md 中「依赖接口/窄契约而非框架全局、通过构造器传递协作者」的 DI 约定一一对应。 - HTTP 入参先转 DTO:Controller 不直接消费
$request->all(),而是由 Form Request 的toDto()产出CreateOrderData数据传输对象。 - 返回统一信封:
success / data / error / meta四字段响应结构贯穿所有 API 响应。配套项目模板 examples/laravel-api-CLAUDE.md 给出了该信封的 JSON 形态:
{ "success": true, "data": {"...": "..."}, "error": null, "meta": {"page": 1, "per_page": 25, "total": 120} }若业务流程更复杂,可在 Action 之上再叠加一层协调性 Service。模板 examples/laravel-api-CLAUDE.md 展示了完整形态:OrderService构造注入CreateOrderAction,placeOrder()仅转发调用,Controller 注入OrderService,形成Controller → Service → Action的三段链路。
路由、隐式绑定与作用域绑定
资源路由与中间件
优先使用路由模型绑定与资源控制器提升可读性:
use Illuminate\Support\Facades\Route; Route::middleware('auth:sanctum')->group(function () { Route::apiResource('projects', ProjectController::class); });apiResource会按 REST 惯例一次性注册 index/store/show/update/destroy 五条路由;auth:sanctum中间件为整个组启用 API Token 鉴权(ECC 的 Laravel 模板默认以 Sanctum 作为 API 认证方案,见 examples/laravel-api-CLAUDE.md)。
作用域绑定(Scoped Binding)防止跨租户访问
当路由中出现父子两级模型参数时,使用scopeBindings()让 Laravel 自动把父模型的关联约束应用到子模型解析上,从而防止跨账户/跨租户访问:
Route::scopeBindings()->group(function () { Route::get('/accounts/{account}/projects/{project}', [ProjectController::class, 'show']); });上例中,{project}的解析会限定为「属于该{account}的 project」,任一参数不匹配都会直接返回 404 而非 403,从路由层就堵死越权通道。
嵌套路由与绑定命名的一致性
技能文档特别警告了两类易错点:
- 避免双重嵌套与前后缀不一致(例如
conversation与conversations混用造成 URL 语义混乱); - 参数名必须与绑定的模型类名单数形式一致:
{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::prefix('conversations')->group()统一了集合级前缀,内层scopeBindings()保证{message}必须属于{conversation};命名时统一conversations.*风格,跨控制器引用消息则用语义明确的conversation-messages.*。
显式绑定与自定义绑定解析
当 URL 参数需要解析为与参数名不同的模型类时(例如将{conversation}绑定到AiConversation),定义显式模型绑定:
use App\Models\AiConversation; use Illuminate\Support\Facades\Route; Route::model('conversation', AiConversation::class);若需要更复杂的绑定逻辑(如按 slug 而非主键查询、或叠加额外过滤),技能文档给出两条路:使用Route::bind()注册自定义解析回调,或在模型类上实现resolveRouteBinding()方法。
服务容器绑定:接口到实现的依赖装配
为保证依赖注入清晰、可替换,技能文档推荐在 Service Provider 的register()中把「接口/抽象」绑定到「具体实现」:
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); } }bind()表示每次解析都会构造新实例;若希望整个应用生命周期内复用同一实例,可将bind换成singleton。这一模式配合「依赖接口而非具体类」的约定(见 rules/php/patterns.md),使 Action/Service 可面向仓储契约编程——在测试中用内存假实现替换 Eloquent 实现即可完成单元测试,无需数据库。
Eloquent 模型模式
模型基础配置
技能文档给出的模型规范模板同时覆盖了 fillable、cast 与关系:
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'); } }几点实践要点:
$fillable白名单是安全底线:ECC 的 PHP 审查代理把「$guarded = []或直接create($request->all())造成的大规模赋值(Mass Assignment)」列为 CRITICAL 级安全问题,规范做法是显式列出可批量赋值的字段(见 agents/php-reviewer.md)。$casts保证类型一致:将status映射到 PHP 枚举类ProjectStatus,将时间戳字段声明为datetime,让领域取值在应用层始终具备正确类型。- 类声明为
final:除非设计为被继承,否则服务与模型一律final,这是 ECC PHP 规范(PSR-12 + 严格类型)的一部分。
自定义 Cast 与值对象
对枚举或带约束的值(金额、标识符、日期区间),技能文档要求使用自定义 cast 或值对象实现严格类型:
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(), ); }第二个示例演示了 Laravel 9+ 的Attribute::make访问器写法:数据库存整数「分」,应用层读出的是Money值对象,写入时自动转回整数,货币计算永远不会出现浮点误差。这也呼应 rules/php/patterns.md 中「用 DTO/值对象替代形状复杂的关联数组」的 PHP 层约定。
Eager Loading 规避 N+1
N+1 查询是 ECC PHP 审查代理明确列为 HIGH 级的问题(见 agents/php-reviewer.md:missingwith()for relationships in loops or serialization)。技能文档给出的标准解:
$orders = Order::query() ->with(['customer', 'items.product']) ->latest() ->paginate(25);用一条查询预加载嵌套两层关系(customer、items.product),避免循环内逐条查询;latest()按时间倒序,paginate(25)直接产出分页器,无需手写 LIMIT/OFFSET。
Query Objects:复杂过滤的封装
当单个模型的过滤条件组合复杂(多筛选、多排序、需复用),技能文档建议抽出 Query Object 类封装 builder:
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; } }设计要点在于每个过滤方法都克隆当前 query再返回新的 Query Object 实例,保持不可变性、支持链式组合((new ProjectQuery(Project::query()))->ownedBy($id)->active()->builder()),同时避免对传入 Builder 的意外原地修改。它比把过滤堆进 Service 更内聚,也比全局可变状态更安全。
Global Scopes 与 Soft Deletes
默认过滤(如「只看未归档」)可用全局作用域固化;需要可恢复删除的记录则启用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'); }); } }技能文档特别给出了一条反模式警告:同一过滤条件不要同时用全局作用域和命名作用域实现——两者叠加会导致过滤行为重复或难以追踪;除非刻意追求分层行为,否则二选一。SoftDeletes则让delete()变为写入deleted_at的逻辑删除,withTrashed()可恢复查询。
命名 Query Scope:可复用过滤器
轻量、单处的过滤优先用命名作用域:
use Illuminate\Database\Eloquent\Builder; final class Project extends Model { public function scopeOwnedBy(Builder $query, int $userId): Builder { return $query->where('owner_id', $userId); } } // En servicio, repositorio, etc. $projects = Project::ownedBy($user->id)->get();命名作用域通过scopeXxx方法定义、以xxx()静态调用,可继续接->where()->get()链式扩展。上例注释表明其典型调用位置是 Service、Repository 等业务层。
事务包裹多步更新
涉及多张表或多次写操作必须放进数据库事务:
use Illuminate\Support\Facades\DB; DB::transaction(function (): void { $order->update(['status' => 'paid']); $order->items()->update(['paid_at' => now()]); });闭包内任一语句抛出异常都会触发整体回滚,避免「主订单已标记已支付、明细却未更新」这类中间态。DB::transaction支持嵌套(基于保存点),也支持第二个参数传入隔离级别与重试次数,多步业务更新应默认以此收口。
迁移:命名约定与匿名类
命名约定
- 迁移文件名带时间戳前缀:
YYYY_MM_DD_HHMMSS_create_users_table.php,保证执行顺序确定; - 迁移使用匿名类(
return new class extends Migration),不写命名类,文件名即意图声明; - 表名默认
snake_case复数(orders、users)。
迁移示例
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'); } };本示例同时示范了几条数据库最佳实践:外键用foreignId()->constrained()声明式关联并以cascadeOnDelete()级联清理;status加索引以加速按状态的 where/orderBy 查询(ECC 审查代理明确要求「任何出现在 where 或 orderBy 中的列都建索引」,见 examples/laravel-api-CLAUDE.md);金额使用无符号整数分(total_cents)而非浮点。迁移文件应提交进版本控制。
Form Requests 与校验:校验留在请求层,输入转成 DTO
技能文档的核心主张是:校验逻辑放进 Form Request,而不是控制器方法体;控制器不再接触$request->all()。
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'), ); } }authorize()做授权:通过$this->user()?->can('create', Order::class)委托 Policy 完成模型级授权,未登录返回 false。rules()只声明校验:数组语法支持exists:customers,id引用完整性校验与items.*.sku嵌套数组逐项校验;校验失败由框架自动转为 422 响应。toDto()产出 DTO:只取$this->validated()的字段,显式转型后构造CreateOrderData。业务层不感知 HTTP 请求对象,派生字段也绝不信任原始 payload(见 examples/laravel-api-CLAUDE.md)。
API Resources 与统一分页响应
技能文档要求 API 响应保持「Resources + 分页」的一致形态:
$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(), ], ]);ProjectResource::collection(...)负责逐项序列化(隐藏敏感字段、格式化日期、扁平化关联),meta块携带分页信息便于客户端实现「加载更多」。Resource 本身继承JsonResource并实现toArray(Request $request): array,模板示例见 examples/laravel-api-CLAUDE.md。注意此处data传的是$projects->items()(当前页集合),分页状态单列在meta,从而在保持分页信息的同时让data字段始终是纯资源数组,便于前端直接消费。
事件、Job 与队列:异步副作用的标准载具
技能文档为异步处理给出了三条核心主张:
- 用领域事件承载副作用:邮件通知、埋点统计等不应阻塞主流程,而应在业务完成后
event(new OrderCreated($order))派发,由监听器处理; - 用队列 Job 承载慢任务:报表生成、数据导出、Webhook 推送等耗时工作放入队列异步执行;
- Handler 尽量幂等:设计可重复执行不产生重复副作用的 handler,并配置重试与退避(retries/backoff)。
配套模板中的队列 Job 展示了标准骨架(examples/laravel-api-CLAUDE.md):
use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use App\Repositories\OrderRepository; use App\Services\OrderMailer; final class SendOrderConfirmation implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; public function __construct(private int $orderId) {} public function handle(OrderRepository $orders, OrderMailer $mailer): void { $order = $orders->findOrFail($this->orderId); $mailer->sendOrderConfirmation($order); } }要点:实现ShouldQueue使 Job 默认异步;构造器只保存orderId这类标量而非整个模型(配合SerializesModels,避免把完整模型快照序列化进队列);handle()中依赖通过容器注入——即便延迟执行,解析出的仓储与邮件服务仍是最新绑定,这正是「面向接口编程 + 容器注入」在异步边界的延伸。ECC 的 PHP 审查代理也将「队列任务幂等性」列为 Laravel 专项检查项(见 agents/php-reviewer.md)。
缓存:昂贵读取的加速与失效策略
技能文档给出三条缓存纪律:
- 缓存高频读取的端点与昂贵查询:例如热点列表、聚合统计、外部 API 结果,用
Cache::remember('key', $ttl, fn () => ...)包裹; - 在模型事件上失效缓存:在模型的
created / updated / deleted事件回调中清除对应缓存键,避免读到脏数据; - 关联数据用缓存标签便于整体失效:
Cache::tags(['orders', 'user:'.$id])->put(...),失效时按标签一键清理(要求驱动支持 tags,如 Redis 或 array 驱动)。
配置与环境:secret 与 config 分离
- 密钥只放
.env(数据库口令、APP_KEY、第三方凭据),配置逻辑放config/*.php,代码中一律通过config('key')读取; - 环境差异化覆盖:通过
.env按环境注入不同值,config/*.php提供默认值与env()回退; - 生产环境执行
php artisan config:cache:将全部配置编译为单一缓存文件,既减少每次请求解析.env的开销,也能在部署后快速发现残缺配置(注意:使用config:cache后env()仅在配置文件中可调用,业务代码应改用config())。
技能在 ECC 中的配套落地
laravel-patterns并非孤立文档,它处于 ECC 完整的 Laravel 工程体系正中:
- 技能映射:config/project-stack-mappings.json 将 PHP/Laravel 栈(
composer.json/artisan/composer.lock指示文件)关联到laravel-patterns及配套技能,并为该栈预设build: composer install、test: php artisan test / phpunit / pest、lint: phpstan / pint等标准命令; - 技术栈识别:scripts/lib/project-detect.js 以
artisan文件与composer.json中的laravel/framework依赖作为 Laravel 判定信号; - 规则衔接:rules/php/patterns.md 将 PHP 通用约定(薄控制器、DTO/值对象、构造器注入、隔离 ORM 与领域决策)与本文的 Laravel 具体实现打通,并显式指向本技能;
- 审查代理:agents/php-reviewer.md 把 N+1、
$fillable/$casts缺失、控制器夹带业务逻辑、绕过 FormRequest、whereRaw拼接用户输入等本文反面场景列为评审红线,静态检查推荐phpstan analyse --level max、psalm、pint --test; - 真实模板:examples/laravel-api-CLAUDE.md 给出了 PHP 8.2+ / Laravel 11.x / PostgreSQL / Redis / Horizon / Pest 的整仓规范,将上述模式连同 API 信封、Policy、队列 Job、PHPUnit/Pest 测试模板一并呈现,可直接复制到项目根目录作为团队基线。
说明:当前仓库以该技能文档(含 docs/es/skills/laravel-patterns/SKILL.md、skills/laravel-patterns/SKILL.md 及多语言镜像)作为交付物,本身并不包含可运行的 Laravel 应用源码;文中所列 PHP 代码均为技能定义的最佳实践示例,落地前请结合目标项目的 Laravel 主版本与既有约定适配。
以上从目录分层、依赖注入、Eloquent 数据访问、迁移、校验与序列化,到异步处理与缓存配置,构成一套完整可执行的 Laravel 生产级架构基线。把本文的原则落实为团队编码规范,配合 ECC 的审查代理与模板项目,即可让 Laravel 代码在可维护性、安全性与查询性能上保持一致的高水位。
【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考