CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Laravel 实战入门/课时/控制器
RawBlame
课时目录12 节

Laravel 实战入门

  1. 01Laravel 概览
  2. 02目录结构
  3. 03路由
  4. 04控制器
  5. 05Blade 模板
  6. 06Eloquent ORM
  7. 07认证与授权
  8. 08验证与异常
  9. 09队列与事件
  10. 10运维与工具
  11. 11测试
  12. 12综合练习

控制器

Resource 控制器与响应。

30 分钟

控制器、中间件与请求响应

这一课你将能做到

  1. 用 artisan 创建控制器,在方法中通过依赖注入拿到 Request,并返回 JSON / 重定向 / 视图
  2. 自己编写一个带参数的中间件并注册别名,理解前置、后置、终止三个阶段
  3. 用 API Resource 统一响应字段,用状态码、Header、Cookie 正确表达 HTTP 语义

为什么要学

控制器是路由的落点,是「这一个请求要做什么」的入口。Spring Boot 里你写 @RestController + @RequestBody,Laravel 控制器做的事一模一样:解析输入、调用业务、组装响应。差别在于 Laravel 通过服务容器自动注入方法参数——你需要什么就声明什么,没有的一个不多给。

中间件则解决横切问题:认证、限流、CORS、日志、请求 ID 注入。这些逻辑如果散落在每个控制器方法里,会出现大量复制粘贴且容易漏写。把它们抽成中间件后,一条路由声明就能挂上,这与 Servlet Filter 是同一个思想。

先建立图像

Request
   │
   ▼
Middleware::handle() 前置
   │
   ▼
Controller::method(Request $request, Order $order)   ← 容器自动注入
   │       ├─ $request->input() / query() / header()
   │       ├─ 调用 Service / Model
   │       └─ 返回 Resource / JsonResponse / Redirect / View
   ▼
Middleware::handle() 后置
   │
   ▼
Middleware::terminate()(响应发出后,适合慢日志)

HTTP 语义对照:

场景状态码Laravel 写法Spring 写法
创建成功201response()->json($data, 201)ResponseEntity.status(201)
参数错误422验证器自动抛BindException 处理
未登录401中间件拦截认证入口点
无权限403abort(403)AccessDeniedException
资源不存在404abort(404) / 绑定自动ResponseStatusException
限流429throttle 中间件网关层

跟着做

步骤 1:创建控制器

# 普通控制器
php artisan make:controller Api/OrderController

# 带 CRUD 方法骨架的资源控制器
php artisan make:controller Api/ProductController --resource
# 纯 API(不含 create/edit)
php artisan make:controller Api/ProductController --api

# 嵌套控制器(处理某资源下的子资源)
php artisan make:controller Api/OrderItemController --parent=Order

控制器基类 App\Http\Controllers\Controller 在 Laravel 11 是极简空类,需要统一能力(如统一响应方法)时自己加。

步骤 2:读取请求数据

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function store(Request $request): JsonResponse
    {
        // 单个字段(带默认值)
        $remark = $request->input('remark', '');

        // 只取白名单字段,防止 mass assignment 之外的脏数据
        $payload = $request->only(['items', 'address_id', 'channel']);

        // 查询字符串 / JSON Body / 路由参数
        $page    = $request->query('page', '1');
        $channel = $request->input('channel');           // application/json 也能读
        $traceId = $request->header('X-Trace-Id');

        // 判断
        if ($request->isMethod('post') && $request->expectsJson()) {
            // ...
        }

        // 数组式输入:items[0][sku]
        $firstSku = $request->input('items.0.sku');

        return response()->json([
            'received' => $payload,
            'page'     => (int) $page,
            'trace_id' => $traceId,
            'first_sku'=> $firstSku,
        ], 201);
    }
}

注册路由并验证:

curl -X POST http://127.0.0.1:8000/api/orders \
  -H "Content-Type: application/json" \
  -H "X-Trace-Id: trace-001" \
  -d '{"items":[{"sku":"SKU-1"}],"address_id":3,"channel":"alipay","remark":"尽快"}'

步骤 3:依赖注入与构造注入

容器按类型提示自动解析,业务服务直接声明:

<?php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Services\OrderService;
use Illuminate\Http\Request;

class OrderController extends Controller
{
    public function __construct(
        private readonly OrderService $orders,
    ) {}

    public function store(Request $request)
    {
        $order = $this->orders->create(
            userId: $request->user()->id,
            payload: $request->only(['items', 'address_id', 'channel']),
        );

        return response()->json($order, 201);
    }
}

这与 Spring 的构造器注入(推荐 @RequiredArgsConstructor 的 final 字段)完全同构。readonly 是 PHP 8.1+ 的 promoted constructor property 修饰符,注入后不可变。

步骤 4:编写带参数的中间件

php artisan make:middleware EnsureChannel
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureChannel
{
    // 参数在 $next 之后声明,可多个
    public function handle(Request $request, Closure $next, string ...$channels): Response
    {
        if (! in_array($request->input('channel'), $channels, true)) {
            abort(403, '支付渠道不被允许');
        }

        return $next($request);
    }

    // terminate 在响应发送后执行,适合耗时统计(先注册为 terminable)
    public function terminate(Request $request, Response $response): void
    {
        // 例如记录超过 500ms 的慢请求
    }
}

在 bootstrap/app.php 注册别名:

->withMiddleware(function (Middleware $middleware): void {
    $middleware->alias([
        'channel' => \App\Http\Middleware\EnsureChannel::class,
    ]);
})

使用(路由中参数用逗号分隔):

Route::post('/orders', [OrderController::class, 'store'])
    ->middleware('channel:alipay,wechat');

步骤 5:用 API Resource 统一响应

直接返回模型会暴露全部字段(包括你不想公开的内部标记)。Resource 是响应层的整形器,等价于 VO/DTO 转换:

php artisan make:resource OrderResource
php artisan make:resource OrderItemResource
<?php

namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class OrderResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id'         => $this->id,
            'order_no'   => $this->order_no,
            'status'     => $this->status,
            'amount'     => [
                'value'     => $this->amount,            // 单位:分
                'formatted' => number_format($this->amount / 100, 2),
            ],
            'items'      => OrderItemResource::collection($this->whenLoaded('items')),
            'created_at' => $this->created_at?->toIso8601String(),
        ];
    }
}

控制器使用:

use App\Http\Resources\OrderResource;

public function show(\App\Models\Order $order)
{
    return new OrderResource($order);
}

public function index(Request $request)
{
    $orders = \App\Models\Order::query()
        ->where('user_id', $request->user()->id)
        ->with('items')
        ->orderByDesc('id')
        ->paginate(20);

    return OrderResource::collection($orders); // 自动带分页 meta
}

步骤 6:响应的其他形式

// 下载文件(等价 Spring 的 ResponseEntity<Resource>)
return response()->download(storage_path('app/reports/daily.csv'), '日报.csv');

// 内联显示(如图片、PDF 预览)
return response()->file($path);

// 流式响应(导出大文件,避免内存爆炸)
return response()->streamDownload(function () {
    $handle = fopen('php://output', 'w');
    fputcsv($handle, ['订单号', '金额']);
    \App\Models\Order::chunk(500, fn ($orders) => collect($orders)->each(
        fn ($o) => fputcsv($handle, [$o->order_no, $o->amount])
    ));
    fclose($handle);
}, 'orders.csv');

// 设置 Cookie(自动加密)
return response()->json(['ok' => true])
    ->cookie('campaign', 'double11', minutes: 60, httpOnly: true);

// 重定向并闪存数据
return redirect()->route('orders.index')->with('success', '下单成功');

// 自定义 Header(如请求追踪)
return response()->json($data)->header('X-Trace-Id', $traceId);

讲清楚要点

  1. 控制器保持薄:它的职责是「翻译 HTTP ↔ 业务」——取输入、调服务、塑响应。跨模型的业务规则放 Service,数据查询规则放模型或 Repository。
  2. Request 对象是请求信息的唯一入口:不要碰 $_GET/$_POST/getallheaders(),框架已经统一处理了 JSON、文件、代理 IP 信任(TrustProxies)等边界。
  3. 中间件参数与别名:参数通过 alias:arg1,arg2 传入;中间件默认单例复用,不要在属性里存请求相关状态。
  4. terminate 与 handle 后置的区别:handle 后置发生在响应发送之前,可以修改响应;terminate 在响应发出之后执行,PHP-FPM 下 fastcgi_finish_request 后客户端已断开,适合慢日志等收尾。
  5. Resource 是可嵌套的整形层:whenLoaded 避免 N+1——只有预加载了关联才输出它。对外契约应稳定,加字段比删字段安全。
  6. JSON 判断有两个方法:$request->isJson() 看 Content-Type;$request->expectsJson() 看 Accept 头 / X-Requested-With。决定返回 JSON 还是跳转通常用后者。
  7. CORS 已内建:Laravel 11 的全局 CORS 由框架默认处理,配置发布用 php artisan config:publish cors,无需手写中间件。

你可能会懵的地方

  • Request 里 get()、input()、query()、post() 有什么区别? input() 合并路由参数、Body、查询串(最常用);query() 只看查询串;post() 只看表单 Body;get() 是 Symfony 层方法,不含 JSON Body 解析。记住 input() 一个基本够用。

  • 为什么我返回数组/模型时中文被转成了 \uXXXX? 那是 json_encode 默认转义。Laravel 的 JsonResponse 默认会处理部分选项;需要不转义中文、斜杠时可 response()->json($data, 200, [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)。

  • 中间件顺序错了会怎样? 认证依赖 Session,所以 StartSession 必须在认证之前;CORS 必须尽早执行,预检请求才不会被业务中间件拦截。自定义中间件放在 bootstrap/app.php 回调中的位置决定全局顺序,路由中间件按声明顺序执行。

  • abort(403) 和 throw new AccessDeniedHttpException 选哪个? abort() 是助手函数,底层就是抛 HttpException。在业务代码里用哪个都行;统一风格即可,Service 层里直接抛领域异常有时更清晰,再用全局异常渲染器转 HTTP 响应(等价 @ControllerAdvice)。

  • 文件上传怎么拿、存哪? $file = $request->file('avatar'); $path = $file->store('avatars', 'public'); 存到 storage/app/public,并执行过 php artisan storage:link 才能通过 URL 访问。不要信任客户端给的文件名与 MIME,用 guessExtension()。

错法对照

错法问题正解
控制器里直接写 DB::table 拼大 SQL职责膨胀、难以测试模型查询 + Service 编排
return $order 直接吐模型字段泄露、格式不可控API Resource 整形
中间件属性里保存当前请求数据单例复用导致串请求数据放 Request attribute
所有错误都返回 200 + code 字段破坏 HTTP 语义,网关无法识别4xx/5xx 状态码 + 统一错误体
导出大报表 Excel::create 一次加载全量内存溢出streamDownload / chunk 游标
用 md5($pwd) 自己造认证安全漏洞用 Laravel 内置 Hash 与 auth(第 7 课)

卡点急救

  1. $request->input() 读不到 JSON Body:确认请求头 Content-Type: application/json;表单数组字段名要包含方括号;可用 $request->all() 查看实际解析结果。
  2. 中间件别名报 Target class [xxx] does not exist:先在 bootstrap/app.php 完成 alias;检查类名命名空间;跑过缓存则 php artisan optimize:clear。
  3. 文件下载或流式输出乱码/提前截断:确认没有中间件在响应上做 GZIP 冲突;ob 缓冲可用 ob_end_clean() 清栈;文件路径用 storage_path() 拼绝对路径。

面试常问

  • Laravel 的依赖注入是怎么解析的?循环依赖怎么办? 容器通过反射读构造函数/方法的类型提示,递归解析未显式绑定的类(autowiring);接口需在 Provider 中 bind 到实现。循环依赖会抛解析异常,应通过事件、中介者或重构打破环,而不是强行注入。

  • 中间件和过滤器(Filter)有什么异同? 同:洋葱模型、前置后置、可短路。异:Laravel 中间件是应用层组件,能方便地拿到路由信息与容器服务,支持别名传参与 terminate;Servlet Filter 更底层,作用于容器级。二者在典型项目中的使用场景几乎一一对应。

  • 如何设计统一的 API 响应结构? 成功用 Resource(列表用 collection + 分页 meta),失败通过自定义异常渲染器或在 withExceptions 中统一输出 {message, errors(字段级)},配合正确状态码;不要在每个控制器手写包装数组。再辅以 Accept: application/json 协商,网页请求与 API 请求可分别渲染。

自测三问

  1. 要从 POST 的 JSON 中取 items 数组第一个元素的 sku,用 Request 的什么写法?
  2. 自己写一个「只允许指定渠道」的中间件需要哪三步?参数在哪声明?
  3. 为什么不建议直接 return Order::find(1)?用 Resource 后对 API 版本演进有什么好处?

小结一句话

控制器是 HTTP 与业务之间的翻译层,Request 读取、容器注入、Resource 整形、中间件横切——四件事各司其职,后端接口就既有语义又可维护。

延伸

  • 控制器文档
  • 请求与输入
  • HTTP 响应
  • Eloquent API Resource
  • 中间件
  • 下一课:Blade 模板引擎与视图——学习服务端渲染、布局继承、组件与表单脚手架。
加载本节练习…
← 上一课路由下一课 →Blade 模板