Laravel: Laravel请求与响应

最后更新:2026-08-26

请求与响应是 Laravel 的"进出口"——请求进来时提取数据,响应出去时打包数据,两个方向都要精确控制。

1. 你将学到


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。

📖 小节


📝 作业

  1. 基础题(⭐):为 ShopMetrics API 实现 GET /api/v1/shops 端点,支持 search/status/sort/direction 过滤参数,使用 paginate 分页返回 JSON。

  2. 进阶题(⭐⭐):创建 ApiResponse trait,封装 success/error/paginated 三个方法,在所有 API 控制器中使用,确保响应格式统一。

  3. 挑战题(⭐⭐⭐):实现 cursor 分页的订单列表 API(GET /api/v1/orders?cursor=xxx),对比 paginate 和 cursorPaginate 在 10 万条数据下的查询性能差异。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏