Laravel: Laravel请求与响应
最后更新:2026-08-26
请求与响应是 Laravel 的"进出口"——请求进来时提取数据,响应出去时打包数据,两个方向都要精确控制。
1. 你将学到
- Request 对象:input/query/has/only/except 与类型转换
- 文件上传处理:store/storedAs/S3 集成
- Response 构建器:json/view/download/redirect/stream
- API 资源分页:LengthAwarePaginator 与 cursor 分页
- 响应宏与全局响应格式标准化
2. 一个 API 开发者的真实故事
(1) 痛点:每个端点返回格式都不一样
Bob 给 ShopMetrics API 写了 10 个端点——有的返回 {data: [...]},有的返回 {shops: [...]},有的直接返回数组。Alice 前端对接时每个端点都要写不同的解析逻辑。更糟的是,分页数据有的返回 total_pages,有的返回 last_page,前端分页组件根本没法统一。
(2) 标准化响应的解法
Laravel 的 API Resource + 响应宏统一了输出格式——所有端点返回相同的 JSON 结构,分页数据格式一致。
PHP
// Standardized response — every endpoint follows this format
return ShopResource::collection($shops);
// {
// "data": [...],
// "meta": {"current_page": 1, "total": 50},
// "links": {"next": "...", "prev": "..."}
// }
(3) 收益
Alice 用统一格式后,前端解析逻辑从 10 套缩减到 1 套,分页组件复用率 100%。
3. Request 对象
(1) 获取输入
PHP
// Get single input
$name = $request->input('name');
$name = $request->input('name', 'default value');
// Get from query string only
$sort = $request->query('sort', 'created_at');
// Get multiple inputs
$filtered = $request->only(['name', 'email', 'status']);
$filtered = $request->except(['password', '_token']);
// Check existence
if ($request->has('search')) { ... }
if ($request->filled('search')) { ... } // has + not empty
if ($request->missing('search')) { ... }
// Type conversion
$page = $request->integer('page', 1);
$active = $request->boolean('active', false);
$date = $request->date('published_at', 'Y-m-d');
| 方法 | 说明 | 区别 |
|---|---|---|
input() |
所有输入(query+body) | 最常用 |
query() |
仅 query string | GET 参数 |
post() |
仅 POST body | 表单数据 |
only() |
提取指定字段 | 白名单 |
except() |
排除指定字段 | 黑名单 |
has() |
字段存在 | 包含空值 |
filled() |
字段存在且非空 | 排除空值 |
▶ 示例:ShopMetrics API 请求处理
PHP
// app/Http/Controllers/Api/ShopController.php
public function index(Request $request): JsonResponse
{
$query = Shop::where('tenant_id', tenant()->id);
// Search filter
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
// Status filter
if ($request->filled('status')) {
$query->where('status', $request->status);
}
// Sort
$sortField = $request->input('sort', 'created_at');
$sortDir = $request->input('direction', 'desc');
$query->orderBy($sortField, $sortDir);
// Paginate
$perPage = $request->integer('per_page', 15);
$shops = $query->paginate($perPage);
return ShopResource::collection($shops);
}
输出:
TEXT
📖 仅展示
// 执行成功
4. 文件上传
(1) 基本上传处理
PHP
// Validate file upload
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// Store file
$path = $request->file('logo')->store('shops/logos', 'public');
// => "shops/logos/abc123.jpg"
// Store with custom name
$path = $request->file('logo')->storeAs(
'shops/logos',
$shop->slug . '.' . $request->file('logo')->extension(),
'public',
);
// Get file info
$file = $request->file('logo');
$file->getClientOriginalName();
$file->getClientOriginalExtension();
$file->getSize(); // bytes
$file->getMimeType();
| 方法 | 说明 |
|---|---|
store() |
存储到指定目录,随机文件名 |
storeAs() |
存储到指定目录,自定义文件名 |
storePublicly() |
存储到 public 可访问目录 |
isValid() |
验证上传是否成功 |
▶ 示例:ShopMetrics 店铺 Logo 上传
PHP
// app/Http/Controllers/ShopLogoController.php
class ShopLogoController extends Controller
{
public function update(Request $request, Shop $shop): RedirectResponse
{
$validated = $request->validate([
'logo' => 'required|image|mimes:jpeg,png,webp|max:2048',
]);
// Delete old logo if exists
if ($shop->logo_path) {
Storage::disk('public')->delete($shop->logo_path);
}
// Store new logo
$path = $request->file('logo')->store("shops/{$shop->id}/logos", 'public');
$shop->update(['logo_path' => $path]);
return back()->with('success', 'Logo updated.');
}
}
输出:
TEXT
📖 仅展示
// 执行成功
5. Response 构建器
(1) 响应类型
PHP
// JSON response
return response()->json(['message' => 'Created', 'data' => $shop], 201);
// View response
return response()->view('shops.show', compact('shop'), 200);
// Redirect
return redirect()->route('shops.show', $shop);
return back()->withInput()->withErrors($errors);
return redirect()->away('https://external-site.com');
// File download
return response()->download(storage_path('app/reports/report.pdf'));
return response()->streamDownload(function () {
echo generateCsv();
}, 'orders.csv', ['Content-Type' => 'text/csv']);
// No content
return response()->noContent(); // 204
| 响应类型 | 方法 | HTTP 状态码 |
|---|---|---|
| JSON | response()->json() |
200/201/422 |
| 视图 | response()->view() |
200 |
| 重定向 | redirect() |
302 |
| 下载 | response()->download() |
200 |
| 流下载 | response()->streamDownload() |
200 |
| 无内容 | response()->noContent() |
204 |
▶ 示例:ShopMetrics CSV 导出流式响应
PHP
// app/Http/Controllers/ExportController.php
class ExportController extends Controller
{
public function exportOrders(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
$csv = fopen('php://output', 'w');
fputcsv($csv, ['Order ID', 'Customer', 'Total', 'Status', 'Date']);
$shop->orders()
->with('user')
->orderBy('created_at', 'desc')
->chunk(500, function ($orders) use ($csv) {
foreach ($orders as $order) {
fputcsv($csv, [
$order->order_number,
$order->user->name,
$order->total,
$order->status,
$order->created_at->format('Y-m-d'),
]);
}
});
fclose($csv);
}, "orders-{$shop->slug}-" . now()->format('Y-m-d') . '.csv', [
'Content-Type' => 'text/csv',
]);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
6. API 分页
(1) 分页方法对比
| 方法 | 查询数 | 返回信息 | 适用场景 |
|---|---|---|---|
paginate() |
2 (COUNT+SELECT) | total/last_page/links | 需要总页数 |
simplePaginate() |
1 (SELECT) | next/prev links | 不需要总页数 |
cursorPaginate() |
1 (SELECT+WHERE) | next/prev cursor | 大数据集 |
(2) 分页 JSON 格式
JSON
{
"data": [
{"id": 1, "name": "Alice Store"},
{"id": 2, "name": "Bob Electronics"}
],
"current_page": 1,
"per_page": 15,
"total": 50,
"last_page": 4,
"from": 1,
"to": 15,
"links": {
"first": "/api/v1/shops?page=1",
"last": "/api/v1/shops?page=4",
"next": "/api/v1/shops?page=2",
"prev": null
}
}
(3) Cursor 分页
PHP
// More efficient for large datasets
$shops = Shop::cursorPaginate(15);
// Uses "cursor" parameter instead of "page"
// URL: /api/shops?cursor=eyJpZCI6MTV9
// Next page URL
$shops->nextPageUrl();
// /api/shops?cursor=eyJpZCI6MzB9
▶ 示例:ShopMetrics API 分页与过滤
PHP
// app/Http/Controllers/Api/OrderController.php
public function index(Request $request): JsonResponse
{
$query = Order::where('tenant_id', tenant()->id)
->with(['shop', 'user', 'items.product']);
// Filters
if ($request->filled('status')) {
$query->where('status', $request->status);
}
if ($request->filled('shop_id')) {
$query->where('shop_id', $request->shop_id);
}
if ($request->filled('date_from')) {
$query->where('created_at', '>=', $request->date('date_from'));
}
if ($request->filled('date_to')) {
$query->where('created_at', '<=', $request->date('date_to'));
}
// Sort
$query->orderBy(
$request->input('sort_by', 'created_at'),
$request->input('sort_dir', 'desc'),
);
// Choose pagination strategy
$perPage = $request->integer('per_page', 15);
$orders = $request->boolean('cursor', false)
? $query->cursorPaginate($perPage)
: $query->paginate($perPage);
return OrderResource::collection($orders);
}
输出:
TEXT
📖 仅展示
// 执行成功
7. 响应宏与格式标准化
(1) 定义响应宏
PHP
// app/Providers/AppServiceProvider.php
public function boot(): void
{
Response::macro('apiSuccess', function (mixed $data, string $message = 'Success', int $status = 200) {
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $status);
});
Response::macro('apiError', function (string $message, int $status = 400, array $errors = []) {
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $status);
});
}
(2) 使用响应宏
PHP
// In controller
return response()->apiSuccess($shop, 'Shop created', 201);
return response()->apiError('Shop not found', 404);
return response()->apiError('Validation failed', 422, $validator->errors()->toArray());
▶ 示例:ShopMetrics API 标准响应格式
PHP
// app/Traits/ApiResponse.php
trait ApiResponse
{
protected function success(mixed $data, string $message = 'Success', int $status = 200): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
'timestamp' => now()->toISOString(),
], $status);
}
protected function error(string $message, int $status = 400, array $errors = []): JsonResponse
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
'timestamp' => now()->toISOString(),
], $status);
}
protected function paginated($resource, string $message = 'Success'): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $resource->resolve(),
'meta' => [
'current_page' => $resource->resource->currentPage(),
'per_page' => $resource->resource->perPage(),
'total' => $resource->resource->total(),
'last_page' => $resource->resource->lastPage(),
],
'links' => [
'first' => $resource->resource->url(1),
'last' => $resource->resource->url($resource->resource->lastPage()),
'next' => $resource->resource->nextPageUrl(),
'prev' => $resource->resource->previousPageUrl(),
],
]);
}
}
输出:
TEXT
📖 仅展示
// 执行成功
8. 综合示例:ShopMetrics API 请求处理全流程
PHP
// ============================================
// Comprehensive: ShopMetrics API Request-Response
// Covers: input handling, file upload, pagination, response format
// ============================================
// app/Http/Controllers/Api/ProductController.php
class ProductController extends Controller
{
use ApiResponse;
public function index(Request $request): JsonResponse
{
$query = Product::where('shop_id', $request->shop_id)
->with('categories');
if ($request->filled('search')) {
$query->where('name', 'like', "%{$request->search}%");
}
if ($request->filled('category')) {
$query->whereHas('categories', fn ($q) => $q->where('slug', $request->category));
}
if ($request->filled('min_price')) {
$query->where('price', '>=', $request->float('min_price'));
}
if ($request->filled('max_price')) {
$query->where('price', '<=', $request->float('max_price'));
}
if ($request->filled('in_stock') && $request->boolean('in_stock')) {
$query->where('stock', '>', 0);
}
$query->orderBy(
$request->input('sort', 'created_at'),
$request->input('direction', 'desc'),
);
$products = $query->paginate($request->integer('per_page', 15));
return $this->paginated(ProductResource::collection($products));
}
public function store(StoreProductRequest $request): JsonResponse
{
$product = Product::create($request->validated());
if ($request->hasFile('image')) {
$path = $request->file('image')->store("products/{$product->id}", 's3');
$product->update(['image_path' => $path]);
}
return $this->success(new ProductResource($product), 'Product created', 201);
}
public function export(Request $request, Shop $shop): StreamedResponse
{
$this->authorize('view', $shop);
return response()->streamDownload(function () use ($shop) {
echo ProductExporter::toCsv($shop);
}, "products-{$shop->slug}.csv", ['Content-Type' => 'text/csv']);
}
}
❓ 常见问题
Q input() 和 query() 有什么区别?
A input() 从所有输入源获取(query string + request body),query() 只从 URL query string 获取。POST 表单数据用 input(),GET 参数用 query() 更明确。
Q cursorPaginate 和 paginate 怎么选?
A 数据量小于 10 万用 paginate(需要显示总页数);数据量大于 10 万用 cursorPaginate(无需 COUNT 查询,性能更好)。无限滚动 UI 适合 cursor。
Q API 响应格式应该怎么统一?
A 创建 ApiResponse trait 或响应宏,封装 success/error/paginated 方法。所有控制器使用统一的响应方法,确保字段名、状态码、分页格式一致。
Q 大文件下载用 download 还是 streamDownload?
A 小文件用 download() 直接读入内存;大文件用 streamDownload() 流式输出,内存占用恒定。CSV 导出等场景必须用 streamDownload。
Q 如何处理 API 版本间的响应格式差异?
A 每个 API 版本使用独立的 Resource 类(V1/ShopResource vs V2/ShopResource),在 Resource 的 toArray() 中控制输出字段,路由层选择对应版本。
Q 文件上传存储到 local 和 S3 有什么区别?
A local 存在服务器磁盘(免费但不可扩展),S3 存在云存储(按量付费、CDN 加速、无限扩容)。生产环境推荐 S3。
📖 小节
- Request 对象提供 input/query/only/except 等方法获取输入
- 文件上传用 store()/storeAs() 存储到指定磁盘
- Response 支持多种类型:json/view/redirect/download/stream
- API 分页有三种策略:paginate/simplePaginate/cursorPaginate
- 响应宏统一 API 输出格式,避免每个端点格式不同
- 大文件导出用 streamDownload 流式输出,避免内存溢出
📝 作业
-
基础题(⭐):为 ShopMetrics API 实现 GET /api/v1/shops 端点,支持 search/status/sort/direction 过滤参数,使用 paginate 分页返回 JSON。
-
进阶题(⭐⭐):创建 ApiResponse trait,封装 success/error/paginated 三个方法,在所有 API 控制器中使用,确保响应格式统一。
-
挑战题(⭐⭐⭐):实现 cursor 分页的订单列表 API(GET /api/v1/orders?cursor=xxx),对比 paginate 和 cursorPaginate 在 10 万条数据下的查询性能差异。