laravel-api

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Laravel API - Steve's Architecture

Laravel API - Steve架构方案

Build Laravel REST APIs with clean, stateless, resource-scoped architecture.
基于简洁、无状态、资源范围化的架构构建Laravel REST API。

Quick Start

快速开始

When user requests a Laravel API, follow this workflow:
  1. Understand requirements - What resources? What operations? Authentication needed?
  2. Initialize project structure - Set up routing, remove frontend bloat
  3. Build first resource - Complete CRUD to establish pattern
  4. Add authentication - JWT via PHP Open Source Saver
  5. Iterate on remaining resources - Follow established pattern
当用户请求Laravel API相关服务时,请遵循以下工作流程:
  1. 理解需求 - 涉及哪些资源?需要哪些操作?是否需要认证?
  2. 初始化项目结构 - 配置路由,移除前端冗余内容
  3. 构建首个资源 - 完成CRUD操作以确立规范
  4. 添加认证功能 - 通过PHP Open Source Saver实现JWT认证
  5. 迭代剩余资源 - 遵循已确立的规范

Core Architecture Principles

核心架构原则

Read
references/architecture.md
for comprehensive details. Key principles:
  1. Stateless by design - No hidden dependencies, explicit data flow
  2. Boundary-first - Clear separation of HTTP, business logic, data layers
  3. Resource-scoped - Routes, controllers organized by resource
  4. Version discipline - Namespace-based versioning, HTTP Sunset headers
详细内容请查阅
references/architecture.md
。核心原则:
  1. 无状态设计 - 无隐藏依赖,数据流清晰明确
  2. 边界优先 - HTTP层、业务逻辑层、数据层清晰分离
  3. 资源范围化 - 路由、控制器按资源分类组织
  4. 版本规范 - 基于命名空间的版本控制,搭配HTTP Sunset响应头

Code Quality Standards

代码质量标准

All code must follow Laravel best practices and PSR-12 standards:
  1. Preserve Functionality - Refactorings change HOW code works, never WHAT it does
  2. Explicit Over Implicit - Prefer clear, readable code over clever shortcuts
  3. Type Declarations - Always use return types on methods, parameter types where beneficial
  4. Avoid Nested Ternaries - Use match expressions, switch, or if/else for clarity
  5. Consistent Naming - Follow PSR-12 and Laravel conventions strictly
  6. Proper Namespacing - Organize imports logically, use full type hints
When reviewing or refactoring code:
  • Focus on clarity and maintainability over cleverness
  • Simplify complex nested logic into readable structures
  • Extract magic values into named constants or config
  • Remove unnecessary complexity while preserving exact behavior
所有代码必须遵循Laravel最佳实践与PSR-12标准:
  1. 保留功能完整性 - 重构仅改变代码实现方式,绝不改变功能逻辑
  2. 显式优于隐式 - 优先选择清晰、可读的代码,而非巧妙的捷径
  3. 类型声明 - 方法必须添加返回类型,参数按需添加类型声明
  4. 避免嵌套三元运算符 - 使用match表达式、switch或if/else提升可读性
  5. 命名一致性 - 严格遵循PSR-12与Laravel命名规范
  6. 正确命名空间 - 逻辑组织导入,使用完整类型提示
评审或重构代码时:
  • 优先关注代码清晰度与可维护性,而非技巧性
  • 将复杂嵌套逻辑简化为可读结构
  • 将魔法值提取为命名常量或配置项
  • 在保留原有功能的前提下移除不必要的复杂度

Project Structure

项目结构

routes/api/
  routes.php              # Main entry point, version grouping
  tasks.php               # All task routes, all versions
  projects.php            # All project routes, all versions

app/Http/
  Controllers/{Resource}/V1/
    StoreController.php   # Always invokable
    IndexController.php
    ShowController.php
  Requests/{Resource}/V1/
    StoreTaskRequest.php  # Validation + payload() method
  Payloads/{Resource}/
    StoreTaskPayload.php  # Simple DTOs with toArray()
  Responses/
    JsonDataResponse.php  # Implements Responsable
    JsonErrorResponse.php
  Middleware/
    HttpSunset.php

app/Actions/{Resource}/
  CreateTask.php          # Single-purpose business logic

app/Services/             # Only when logic too complex for Actions

app/Models/
  Task.php                # HasUlids trait, simple data access
routes/api/
  routes.php              # 主入口,版本分组
  tasks.php               # 所有任务路由,包含所有版本
  projects.php            # 所有项目路由,包含所有版本

app/Http/
  Controllers/{Resource}/V1/
    StoreController.php   # 始终为可调用控制器
    IndexController.php
    ShowController.php
  Requests/{Resource}/V1/
    StoreTaskRequest.php  # 验证 + payload()方法
  Payloads/{Resource}/
    StoreTaskPayload.php  # 带toArray()的简单DTO
  Responses/
    JsonDataResponse.php  # 实现Responsable接口
    JsonErrorResponse.php
  Middleware/
    HttpSunset.php

app/Actions/{Resource}/
  CreateTask.php          # 单一职责的业务逻辑

app/Services/             # 仅当逻辑复杂度超出Action承载范围时使用

app/Models/
  Task.php                # 引入HasUlids trait,仅处理数据访问

Building a New Resource Endpoint

构建新资源端点

Step 1: Model

步骤1:模型

Always use ULIDs. Keep models simple - data access only.
php
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

final class Task extends Model
{
    use HasFactory;
    use HasUlids;
    
    protected $fillable = [
        'title',
        'description',
        'status',
        'project_id',
    ];

    protected $casts = [
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    public function project(): BelongsTo
    {
        return $this->belongsTo(Project::class);
    }
}
始终使用ULID。保持模型简洁 - 仅负责数据访问。
php
<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

final class Task extends Model
{
    use HasFactory;
    use HasUlids;
    
    protected $fillable = [
        'title',
        'description',
        'status',
        'project_id',
    ];

    protected $casts = [
        'created_at' => 'datetime',
        'updated_at' => 'datetime',
    ];

    public function project(): BelongsTo
    {
        return $this->belongsTo(Project::class);
    }
}

Step 2: Routes

步骤2:路由

Create resource route file at
routes/api/{resource}.php
:
php
use App\Http\Controllers\Tasks\V1;

Route::middleware(['auth:api'])->group(function () {
    Route::get('/tasks', V1\IndexController::class);
    Route::post('/tasks', V1\StoreController::class);
    Route::get('/tasks/{task}', V1\ShowController::class);
    Route::patch('/tasks/{task}', V1\UpdateController::class);
    Route::delete('/tasks/{task}', V1\DestroyController::class);
});
Include in
routes/api/routes.php
:
php
Route::prefix('v1')->group(function () {
    require __DIR__ . '/tasks.php';
});
routes/api/{resource}.php
创建资源路由文件:
php
use App\Http\Controllers\Tasks\V1;

Route::middleware(['auth:api'])->group(function () {
    Route::get('/tasks', V1\IndexController::class);
    Route::post('/tasks', V1\StoreController::class);
    Route::get('/tasks/{task}', V1\ShowController::class);
    Route::patch('/tasks/{task}', V1\UpdateController::class);
    Route::delete('/tasks/{task}', V1\DestroyController::class);
});
routes/api/routes.php
中引入:
php
Route::prefix('v1')->group(function () {
    require __DIR__ . '/tasks.php';
});

Step 3: DTO (Payload)

步骤3:DTO(Payload)

Create at
app/Http/Payloads/{Resource}/{Operation}Payload.php
:
php
<?php

declare(strict_types=1);

namespace App\Http\Payloads\Tasks;

final readonly class StoreTaskPayload
{
    public function __construct(
        public string $title,
        public ?string $description,
        public string $status,
        public string $projectId,
    ) {}

    public function toArray(): array
    {
        return [
            'title' => $this->title,
            'description' => $this->description,
            'status' => $this->status,
            'project_id' => $this->projectId,
        ];
    }
}
app/Http/Payloads/{Resource}/{Operation}Payload.php
创建:
php
<?php

declare(strict_types=1);

namespace App\Http\Payloads\Tasks;

final readonly class StoreTaskPayload
{
    public function __construct(
        public string $title,
        public ?string $description,
        public string $status,
        public string $projectId,
    ) {}

    public function toArray(): array
    {
        return [
            'title' => $this->title,
            'description' => $this->description,
            'status' => $this->status,
            'project_id' => $this->projectId,
        ];
    }
}

Step 4: Form Request

步骤4:Form Request

Create at
app/Http/Requests/{Resource}/V1/{Operation}Request.php
:
php
<?php

declare(strict_types=1);

namespace App\Http\Requests\Tasks\V1;

use App\Http\Payloads\Tasks\StoreTaskPayload;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string', 'max:1000'],
            'status' => ['required', Rule::in(['pending', 'in_progress', 'completed'])],
            'project_id' => ['required', 'string', 'exists:projects,id'],
        ];
    }

    public function payload(): StoreTaskPayload
    {
        return new StoreTaskPayload(
            title: $this->string('title')->toString(),
            description: $this->string('description')->toString(),
            status: $this->string('status')->toString(),
            projectId: $this->string('project_id')->toString(),
        );
    }
}
app/Http/Requests/{Resource}/V1/{Operation}Request.php
创建:
php
<?php

declare(strict_types=1);

namespace App\Http\Requests\Tasks\V1;

use App\Http\Payloads\Tasks\StoreTaskPayload;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

final class StoreTaskRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:255'],
            'description' => ['nullable', 'string', 'max:1000'],
            'status' => ['required', Rule::in(['pending', 'in_progress', 'completed'])],
            'project_id' => ['required', 'string', 'exists:projects,id'],
        ];
    }

    public function payload(): StoreTaskPayload
    {
        return new StoreTaskPayload(
            title: $this->string('title')->toString(),
            description: $this->string('description')->toString(),
            status: $this->string('status')->toString(),
            projectId: $this->string('project_id')->toString(),
        );
    }
}

Step 5: Action

步骤5:Action

Create at
app/Actions/{Resource}/{Operation}.php
:
php
<?php

declare(strict_types=1);

namespace App\Actions\Tasks;

use App\Http\Payloads\Tasks\StoreTaskPayload;
use App\Models\Task;

final readonly class CreateTask
{
    public function handle(StoreTaskPayload $payload): Task
    {
        return Task::create($payload->toArray());
    }
}
app/Actions/{Resource}/{Operation}.php
创建:
php
<?php

declare(strict_types=1);

namespace App\Actions\Tasks;

use App\Http\Payloads\Tasks\StoreTaskPayload;
use App\Models\Task;

final readonly class CreateTask
{
    public function handle(StoreTaskPayload $payload): Task
    {
        return Task::create($payload->toArray());
    }
}

Step 6: Controller

步骤6:控制器

Create invokable controller at
app/Http/Controllers/{Resource}/V1/{Operation}Controller.php
:
php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Tasks\V1;

use App\Actions\Tasks\CreateTask;
use App\Http\Requests\Tasks\V1\StoreTaskRequest;
use App\Http\Responses\JsonDataResponse;
use Illuminate\Http\JsonResponse;

final readonly class StoreController
{
    public function __construct(
        private CreateTask $createTask,
    ) {}

    public function __invoke(StoreTaskRequest $request): JsonResponse
    {
        $task = $this->createTask->handle(
            payload: $request->payload(),
        );

        return new JsonDataResponse(
            data: $task,
            status: 201,
        );
    }
}
app/Http/Controllers/{Resource}/V1/{Operation}Controller.php
创建可调用控制器:
php
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Tasks\V1;

use App\Actions\Tasks\CreateTask;
use App\Http\Requests\Tasks\V1\StoreTaskRequest;
use App\Http\Responses\JsonDataResponse;
use Illuminate\Http\JsonResponse;

final readonly class StoreController
{
    public function __construct(
        private CreateTask $createTask,
    ) {}

    public function __invoke(StoreTaskRequest $request): JsonResponse
    {
        $task = $this->createTask->handle(
            payload: $request->payload(),
        );

        return new JsonDataResponse(
            data: $task,
            status: 201,
        );
    }
}

Response Format

响应格式

Standard format for all responses:
Success:
json
{
    "data": {...},
    "meta": {...}
}
Error (Problem+JSON):
json
{
    "type": "about:blank",
    "title": "Validation Failed",
    "status": 422,
    "detail": "The given data was invalid",
    "errors": {...}
}
所有响应采用标准格式:
成功响应:
json
{
    "data": {...},
    "meta": {...}
}
错误响应(Problem+JSON):
json
{
    "type": "about:blank",
    "title": "Validation Failed",
    "status": 422,
    "detail": "The given data was invalid",
    "errors": {...}
}

Query Building

查询构建

Use Spatie Query Builder for filtering, sorting, includes:
php
use Spatie\QueryBuilder\QueryBuilder;

$tasks = QueryBuilder::for(Task::class)
    ->allowedFilters(['status', 'priority'])
    ->allowedSorts(['created_at', 'due_date'])
    ->allowedIncludes(['project', 'assignee'])
    ->paginate();
使用Spatie Query Builder实现过滤、排序、关联查询:
php
use Spatie\QueryBuilder\QueryBuilder;

$tasks = QueryBuilder::for(Task::class)
    ->allowedFilters(['status', 'priority'])
    ->allowedSorts(['created_at', 'due_date'])
    ->allowedIncludes(['project', 'assignee'])
    ->paginate();

Versioning Endpoints

端点版本控制

When creating V2:
  1. Create V2 namespace:
    App\Http\Controllers\Tasks\V2\
  2. Add V2 route group in resource file
  3. Add Sunset middleware to V1 routes:
php
Route::middleware(['auth:api', 'http.sunset:2025-12-31'])->group(function () {
    // V1 routes
});
创建V2版本时:
  1. 创建V2命名空间:
    App\Http\Controllers\Tasks\V2\
  2. 在资源路由文件中添加V2路由分组
  3. 为V1路由添加Sunset中间件:
php
Route::middleware(['auth:api', 'http.sunset:2025-12-31'])->group(function () {
    // V1路由
});

Authentication Setup

认证配置

Use PHP Open Source Saver JWT package:
bash
composer require php-open-source-saver/jwt-auth
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
Configure in
config/auth.php
:
php
'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],
使用PHP Open Source Saver JWT包:
bash
composer require php-open-source-saver/jwt-auth
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
config/auth.php
中配置:
php
'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

Essential Setup

基础配置

Add to
app/Providers/AppServiceProvider.php
:
php
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::shouldBeStrict(); // Prevent N+1 queries
}
Register HttpSunset middleware in
app/Http/Kernel.php
:
php
protected $middlewareAliases = [
    'http.sunset' => \App\Http\Middleware\HttpSunset::class,
];
app/Providers/AppServiceProvider.php
中添加:
php
use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::shouldBeStrict(); // 防止N+1查询
}
app/Http/Kernel.php
中注册HttpSunset中间件:
php
protected $middlewareAliases = [
    'http.sunset' => \App\Http\Middleware\HttpSunset::class,
];

Anti-Patterns to Avoid

需避免的反模式

  • Using auto-increment IDs instead of ULIDs
  • Business logic in models
  • Multiple actions per controller
  • Accessing request data directly in controllers/actions
  • Hidden query scopes
  • Service classes when an Action would suffice
  • Breaking changes without versioning
  • Inconsistent response formats
  • Nested ternary operators (use match expressions instead)
  • Missing type declarations on methods and parameters
  • Overly compact "clever" code that sacrifices readability
  • 使用自增ID而非ULID
  • 在模型中编写业务逻辑
  • 单个控制器处理多个操作
  • 在控制器/Action中直接访问请求数据
  • 隐藏查询作用域
  • 可用Action实现却使用Service类
  • 无版本控制的破坏性变更
  • 不一致的响应格式
  • 嵌套三元运算符(改用match表达式)
  • 方法和参数缺少类型声明
  • 过度追求简洁的“技巧性”代码而牺牲可读性

Code Review & Refactoring

代码评审与重构

When reviewing or refactoring Laravel API code, apply these principles:
评审或重构Laravel API代码时,遵循以下原则:

Simplification Checklist

简化检查清单

  1. Preserve Functionality - Ensure refactorings don't change behavior
  2. Check Type Safety - Add missing return types and parameter types
  3. Simplify Logic - Replace nested ternaries with match expressions
  4. Extract Complexity - Move complex conditions into named methods
  5. Verify Standards - Ensure PSR-12 compliance with declare(strict_types=1)
  6. Improve Naming - Use descriptive names that reveal intent
  1. 保留功能完整性 - 确保重构不改变原有功能
  2. 检查类型安全性 - 补充缺失的返回类型与参数类型
  3. 简化逻辑 - 用match表达式替代嵌套三元运算符
  4. 提取复杂逻辑 - 将复杂条件提取为命名方法
  5. 验证标准合规性 - 确保符合PSR-12规范,添加declare(strict_types=1)
  6. 优化命名 - 使用能体现意图的描述性命名

Match Expression Pattern

Match表达式模式

Replace nested ternaries with match for clarity:
php
// ❌ Avoid: Nested ternary
$status = $task->completed_at 
    ? ($task->verified ? 'verified' : 'completed')
    : ($task->started_at ? 'in_progress' : 'pending');

// ✅ Prefer: Match expression
$status = match (true) {
    $task->completed_at && $task->verified => 'verified',
    $task->completed_at => 'completed',
    $task->started_at => 'in_progress',
    default => 'pending',
};
用match表达式替代嵌套三元运算符以提升可读性:
php
// ❌ 避免:嵌套三元运算符
$status = $task->completed_at 
    ? ($task->verified ? 'verified' : 'completed')
    : ($task->started_at ? 'in_progress' : 'pending');

// ✅ 推荐:Match表达式
$status = match (true) {
    $task->completed_at && $task->verified => 'verified',
    $task->completed_at => 'completed',
    $task->started_at => 'in_progress',
    default => 'pending',
};

References

参考文档

  • architecture.md - Comprehensive architectural patterns and principles
  • code-examples.md - Complete working examples for every component
  • code-quality.md - Laravel best practices, refactoring patterns, and PSR-12 standards
  • architecture.md - 完整的架构模式与原则
  • code-examples.md - 所有组件的完整可运行示例
  • code-quality.md - Laravel最佳实践、重构模式与PSR-12标准

Templates

模板

Template files in
assets/templates/
for quick scaffolding:
  • Controller.php
  • FormRequest.php
  • Payload.php
  • Action.php
  • Model.php
assets/templates/
目录下的模板文件可用于快速脚手架搭建:
  • Controller.php
  • FormRequest.php
  • Payload.php
  • Action.php
  • Model.php