响应
创建响应
字符串和数组
所有路由和控制器都应该返回一个响应,发回给用户的浏览器。Laravel 提供了好几种不同的返回方式。最基础的——从路由或控制器直接返回一个字符串,框架会自动把它转成完整的 HTTP 响应:
Route::get('/', function () {
return 'Hello World';
});
除了返回字符串,你也可以返回数组,框架会自动转成 JSON 响应:
Route::get('/', function () {
return [1, 2, 3];
});
信息
你知道吗,路由或控制器还可以直接返回 Eloquent 集合,它们也会自动转成 JSON。试试看!
Response 对象
实际开发中你不会只返回简单的字符串或数组,更多时候会返回完整的 Illuminate\Http\Response 实例或视图。
返回完整的 Response 实例可以让你自己设定 HTTP 状态码和响应头。Response 实例继承自 Symfony\Component\HttpFoundation\Response 类,提供了一大堆构建 HTTP 响应的好方法:
Route::get('/home', function () {
return response('Hello World', 200)
->header('Content-Type', 'text/plain');
});
Eloquent 模型和集合
你还可以直接从路由和控制器返回 Eloquent ORM 模型和集合。Laravel 会自动把它们转成 JSON 响应,同时遵循模型里定义的隐藏字段:
use App\Models\User;
Route::get('/user/{user}', function (User $user) {
return $user;
});
给响应加请求头
大多数响应方法都支持链式调用,让你可以流畅地构建响应实例。比如用 header() 方法在响应发出去之前加一系列请求头:
return response($content)
->header('Content-Type', $type)
->header('X-Header-One', 'Header Value')
->header('X-Header-Two', 'Header Value');
也可以用 withHeaders() 方法一次性传入一个请求头数组:
return response($content)
->withHeaders([
'Content-Type' => $type,
'X-Header-One' => 'Header Value',
'X-Header-Two' => 'Header Value',
]);
用 withoutHeader() 方法可以删掉某个响应头:
return response($content)->withoutHeader('X-Debug');
return response($content)->withoutHeader(['X-Debug', 'X-Powered-By']);
Cache-Control 中间件
Laravel 内置了 cache.headers 中间件,可以快速给一组路由设置 Cache-Control 头。指令用对应缓存控制指令的蛇形命名,分号分隔。如果在指令列表里指定了 etag,框架会自动把响应内容的 MD5 哈希设为 ETag 标识:
Route::middleware('cache.headers:public;max_age=30;s_maxage=300;stale_while_revalidate=600;etag')->group(function () {
Route::get('/privacy', function () {
// ...
});
Route::get('/terms', function () {
// ...
});
});
给响应加 Cookie
用 cookie() 方法可以给即将发出的 Illuminate\Http\Response 实例附加一个 Cookie。传三个参数:名称、值、有效分钟数:
return response('Hello World')->cookie(
'name', 'value', $minutes
);
cookie() 方法还接受几个不常用的额外参数,用法和 PHP 原生的 setcookie 方法一致:
return response('Hello World')->cookie(
'name', 'value', $minutes, $path, $domain, $secure, $httpOnly
);
如果你想确保某个 Cookie 跟着响应一起发出去,但手头还没有响应实例,可以用 Cookie 门面把 Cookie 先"排上队"。queue() 方法的参数和创建 Cookie 实例一样。这些 Cookie 会在响应发出之前自动附加上去:
use Illuminate\Support\Facades\Cookie;
Cookie::queue('name', 'value', $minutes);
生成 Cookie 实例
如果你想先生成一个 Symfony\Component\HttpFoundation\Cookie 实例、稍后再附到响应上,可以用全局 cookie() 辅助函数。这个 Cookie 不会自动发回客户端,除非你显式把它挂到某个响应实例上:
$cookie = cookie('name', 'value', $minutes);
return response('Hello World')->cookie($cookie);
提前过期 Cookie
用 withoutCookie() 方法可以让某个 Cookie 过期,也就是删掉它:
return response('Hello World')->withoutCookie('name');
如果手头还没有响应实例,也可以用 Cookie 门面的 expire() 方法让 Cookie 过期:
Cookie::expire('name');
Cookie 与加密
默认情况下,Laravel 生成的所有 Cookie 都会经过 Illuminate\Cookie\Middleware\EncryptCookies 中间件加密和签名,客户端没法修改或读取。如果你想让某些 Cookie 跳过加密,在 bootstrap/app.php 里用 encryptCookies() 方法排除它们:
->withMiddleware(function (Middleware $middleware): void {
$middleware->encryptCookies(except: [
'cookie_name',
]);
})
注意
一般来说,不要禁用 Cookie 加密——这会让你的 Cookie 暴露给客户端,面临数据泄露和篡改的风险。
重定向
重定向响应是 Illuminate\Http\RedirectResponse 类的实例,包含了把用户导航到另一个 URL 所需的请求头。生成 RedirectResponse 有好几种方式。最简单的是用全局 redirect() 辅助函数:
Route::get('/dashboard', function () {
return redirect('/home/dashboard');
});
有时候你需要把用户带回上一页——比如表单提交后发现数据有问题。用全局 back() 辅助函数就行。这个功能依赖会话,所以调用 back() 的路由要挂在 web 中间件组下面:
Route::post('/user/profile', function () {
// 校验请求...
return back()->withInput();
});
重定向到命名路由
不带参数调用 redirect() 时,返回的是 Illuminate\Routing\Redirector 实例,你可以接着调用它的各种方法。比如要生成一个指向命名路由的重定向,用 route() 方法:
return redirect()->route('login');
如果路由有参数,把它们作为第二个参数传给 route() 方法:
// 对应路由 URI:/profile/{id}
return redirect()->route('profile', ['id' => 1]);
用 Eloquent 模型填充路由参数
如果你要重定向到一个带 "ID" 参数的路由,而且这个 ID 来自 Eloquent 模型,直接把模型本身传进去就行了,ID 会自动提取:
// 对应路由 URI:/profile/{id}
return redirect()->route('profile', [$user]);
如果你想自定义路由参数里填的值,可以在路由参数定义里指定字段(/profile/{id:slug}),或者覆盖 Eloquent 模型的 getRouteKey() 方法:
/**
* 获取模型的路由键值。
*/
public function getRouteKey(): mixed
{
return $this->slug;
}
重定向到控制器方法
你也可以重定向到控制器的某个方法。把控制器类和方法名传给 action() 方法:
use App\Http\Controllers\UserController;
return redirect()->action([UserController::class, 'index']);
如果控制器路由需要参数,把它们作为第二个参数传进去:
return redirect()->action(
[UserController::class, 'profile'], ['id' => 1]
);
重定向到外部域名
有时候你需要重定向到应用之外的域名。用 away() 方法——它会创建一个 RedirectResponse,不做任何额外的 URL 编码、校验或验证:
return redirect()->away('https://www.google.com');
重定向时附带闪存数据
重定向到新 URL 的同时往会话里闪存数据,这两件事经常一起干。比如操作成功后,闪存一条成功提示到会话里。为方便起见,你可以一条流畅的方法链同时创建 RedirectResponse 实例和闪存数据:
Route::post('/user/profile', function () {
// ...
return redirect('/dashboard')->with('status', '个人资料已更新!');
});
用户被重定向之后,你可以在会话里取出并显示这条闪存消息。比如用 Blade 模板语法:
@if (session('status'))
<div class="alert alert-success">
{{ session('status') }}
</div>
@endif
重定向时附带输入
用 RedirectResponse 实例的 withInput() 方法,可以在重定向到新地址之前,把当前请求的输入数据闪存到会话里。这通常用在用户表单校验失败后。输入数据一旦闪存到会话,下一次请求就能轻松取出旧输入来回填表单:
return back()->withInput();
其他响应类型
response() 辅助函数还能生成其他类型的响应实例。不带参数调用 response() 时,返回的是 Illuminate\Contracts\Routing\ResponseFactory 契约的实现,提供了好几个生成响应的好方法。
视图响应
如果你想控制响应的状态码和请求头,同时又需要返回一个视图作为响应内容,用 view() 方法:
return response()
->view('hello', $data, 200)
->header('Content-Type', $type);
当然,如果你不需要自定义 HTTP 状态码或请求头,直接用全局 view() 辅助函数就行。
JSON 响应
json() 方法会自动把 Content-Type 请求头设为 application/json,并用 PHP 的 json_encode() 函数把给定数组转成 JSON:
return response()->json([
'name' => 'Abigail',
'state' => 'CA',
]);
如果你想创建 JSONP 响应,把 json() 方法和 withCallback() 方法组合使用:
return response()
->json(['name' => 'Abigail', 'state' => 'CA'])
->withCallback($request->input('callback'));
文件下载
download() 方法可以生成一个强制用户浏览器下载文件的响应。第一个参数是文件路径,第二个参数(可选)是用户看到的下载文件名,第三个参数(可选)是 HTTP 请求头数组:
return response()->download($pathToFile);
return response()->download($pathToFile, $name, $headers);
注意
Symfony HttpFoundation(负责文件下载的底层组件)要求被下载的文件名必须是 ASCII 字符。
文件响应
file() 方法可以直接在用户浏览器里展示文件(比如图片或 PDF),而不是触发下载。第一个参数是文件绝对路径,第二个参数是请求头数组:
return response()->file($pathToFile);
return response()->file($pathToFile, $headers);
流式响应
把数据边生成边发送给客户端,可以大幅降低内存占用、提升性能,尤其是对于大体积的响应。流式响应让客户端可以在服务器还没全部发完之前就开始处理数据:
Route::get('/stream', function () {
return response()->stream(function (): void {
foreach (['developer', 'admin'] as $string) {
echo $string;
ob_flush();
flush();
sleep(2); // 模拟分块之间的延迟...
}
}, 200, ['X-Accel-Buffering' => 'no']);
});
方便的是,如果你传给 stream() 方法的闭包返回一个 Generator,Laravel 会自动在生成器每次 yield 之后刷新输出缓冲区,同时禁用 Nginx 的输出缓冲:
Route::post('/chat', function () {
return response()->stream(function (): Generator {
$stream = OpenAI::client()->chat()->createStreamed(...);
foreach ($stream as $response) {
yield $response->choices[0];
}
});
});
消费流式响应
你可以用 Laravel 的 stream npm 包来消费流式响应,它提供了与 Laravel 响应流和事件流交互的便捷 API。先安装对应的包:
npm install @laravel/stream-react
npm install @laravel/stream-vue
npm install @laravel/stream-svelte
然后就可以用 useStream 来消费事件流。传入你的流 URL 后,这个 hook 会在 Laravel 应用返回内容时自动把 data 更新为拼接好的响应:
import { useStream } from "@laravel/stream-react";
function App() {
const { data, isFetching, isStreaming, send } = useStream("chat");
const sendMessage = () => {
send({
message: `Current timestamp: ${Date.now()}`,
});
};
return (
<div>
<div>{data}</div>
{isFetching && <div>Connecting...</div>}
{isStreaming && <div>Generating...</div>}
<button onClick={sendMessage}>Send Message</button>
</div>
);
}
<script setup lang="ts">
import { useStream } from "@laravel/stream-vue";
const { data, isFetching, isStreaming, send } = useStream("chat");
const sendMessage = () => {
send({
message: `Current timestamp: ${Date.now()}`,
});
};
</script>
<template>
<div>
<div>{{ data }}</div>
<div v-if="isFetching">Connecting...</div>
<div v-if="isStreaming">Generating...</div>
<button @click="sendMessage">Send Message</button>
</div>
</template>
<script>
import { useStream } from "@laravel/stream-svelte";
const stream = useStream("chat");
const sendMessage = () => {
stream.send({
message: `Current timestamp: ${Date.now()}`,
});
};
</script>
<div>
<div>{$stream.data}</div>
{#if $stream.isFetching}
<div>Connecting...</div>
{/if}
{#if $stream.isStreaming}
<div>Generating...</div>
{/if}
<button onclick={sendMessage}>Send Message</button>
</div>
通过 send 往流里回传数据时,当前活跃的流连接会先取消再发送新数据。所有请求都以 JSON POST 请求形式发送。
注意
因为 useStream hook 会向你的应用发 POST 请求,所以需要有效的 CSRF 令牌。最简单的提供方式是在你的应用布局 head 里通过 meta 标签引入。
useStream 的第二个参数是一个选项对象,用来定制流的消费行为。下面列出了各项默认值:
import { useStream } from "@laravel/stream-react";
function App() {
const { data } = useStream("chat", {
id: undefined,
initialInput: undefined,
headers: undefined,
csrfToken: undefined,
onResponse: (response: Response) => void,
onData: (data: string) => void,
onCancel: () => void,
onFinish: () => void,
onError: (error: Error) => void,
});
return <div>{data}</div>;
}
<script setup lang="ts">
import { useStream } from "@laravel/stream-vue";
const { data } = useStream("chat", {
id: undefined,
initialInput: undefined,
headers: undefined,
csrfToken: undefined,
onResponse: (response: Response) => void,
onData: (data: string) => void,
onCancel: () => void,
onFinish: () => void,
onError: (error: Error) => void,
});
</script>
<template>
<div>{{ data }}</div>
</template>
<script>
import { useStream } from "@laravel/stream-svelte";
const stream = useStream("chat", {
id: undefined,
initialInput: undefined,
headers: undefined,
csrfToken: undefined,
onResponse: (response) => {},
onData: (data) => {},
onCancel: () => {},
onFinish: () => {},
onError: (error) => {},
});
</script>
<div>{$stream.data}</div>
onResponse 在流首次成功返回后被触发,原始 Response 对象会传给回调。onData 在收到每个数据块时调用,当前数据块会传给回调。onFinish 在流结束或 fetch/读取过程中出错时调用。
默认情况下,初始化时不会自动向流发请求。用 initialInput 选项可以传入初始负载:
import { useStream } from "@laravel/stream-react";
function App() {
const { data } = useStream("chat", {
initialInput: {
message: "Introduce yourself.",
},
});
return <div>{data}</div>;
}
<script setup lang="ts">
import { useStream } from "@laravel/stream-vue";
const { data } = useStream("chat", {
initialInput: {
message: "Introduce yourself.",
},
});
</script>
<template>
<div>{{ data }}</div>
</template>
<script>
import { useStream } from "@laravel/stream-svelte";
const stream = useStream("chat", {
initialInput: {
message: "Introduce yourself.",
},
});
</script>
<div>{$stream.data}</div>
要手动取消一个流,用 hook 返回的 cancel 方法:
import { useStream } from "@laravel/stream-react";
function App() {
const { data, cancel } = useStream("chat");
return (
<div>
<div>{data}</div>
<button onClick={cancel}>Cancel</button>
</div>
);
}
<script setup lang="ts">
import { useStream } from "@laravel/stream-vue";
const { data, cancel } = useStream("chat");
</script>
<template>
<div>
<div>{{ data }}</div>
<button @click="cancel">Cancel</button>
</div>
</template>
<script>
import { useStream } from "@laravel/stream-svelte";
const stream = useStream("chat");
</script>
<div>
<div>{$stream.data}</div>
<button onclick={() => stream.cancel()}>Cancel</button>
</div>
每次使用 useStream hook 时,都会生成一个随机的 id 来标识这个流。每次请求都会通过 X-STREAM-ID 请求头发回给服务端。如果你要在多个组件里消费同一个流,可以自己提供 id 来读写同一个流:
// App.tsx
import { useStream } from "@laravel/stream-react";
function App() {
const { data, id } = useStream("chat");
return (
<div>
<div>{data}</div>
<StreamStatus id={id} />
</div>
);
}
// StreamStatus.tsx
import { useStream } from "@laravel/stream-react";
function StreamStatus({ id }) {
const { isFetching, isStreaming } = useStream("chat", { id });
return (
<div>
{isFetching && <div>Connecting...</div>}
{isStreaming && <div>Generating...</div>}
</div>
);
}
<!-- App.vue -->
<script setup lang="ts">
import { useStream } from "@laravel/stream-vue";
import StreamStatus from "./StreamStatus.vue";
const { data, id } = useStream("chat");
</script>
<template>
<div>
<div>{{ data }}</div>
<StreamStatus :id="id" />
</div>
</template>
<!-- StreamStatus.vue -->
<script setup lang="ts">
import { useStream } from "@laravel/stream-vue";
const props = defineProps<{
id: string;
}>();
const { isFetching, isStreaming } = useStream("chat", { id: props.id });
</script>
<template>
<div>
<div v-if="isFetching">Connecting...</div>
<div v-if="isStreaming">Generating...</div>
</div>
</template>
<!-- App.svelte -->
<script>
import { useStream } from "@laravel/stream-svelte";
import StreamStatus from "./StreamStatus.svelte";
const stream = useStream("chat");
</script>
<div>
<div>{$stream.data}</div>
<StreamStatus id={stream.id} />
</div>
<!-- StreamStatus.svelte -->
<script>
import { useStream } from "@laravel/stream-svelte";
let { id } = $props();
const stream = useStream("chat", { id });
</script>
<div>
{#if $stream.isFetching}
<div>Connecting...</div>
{/if}
{#if $stream.isStreaming}
<div>Generating...</div>
{/if}
</div>
流式 JSON 响应
如果你需要渐进式地发送 JSON 数据,可以用 streamJson() 方法。这个方法特别适合需要逐步推送给浏览器的大数据集,格式方便 JavaScript 解析:
use App\Models\User;
Route::get('/users.json', function () {
return response()->streamJson([
'users' => User::cursor(),
]);
});
useJsonStream hook 和上面的 useStream 几乎一样,区别在于流结束后它会尝试把数据解析为 JSON:
import { useJsonStream } from "@laravel/stream-react";
type User = {
id: number;
name: string;
email: string;
};
function App() {
const { data, send } = useJsonStream<{ users: User[] }>("users");
const loadUsers = () => {
send({
query: "taylor",
});
};
return (
<div>
<ul>
{data?.users.map((user) => (
<li>
{user.id}: {user.name}
</li>
))}
</ul>
<button onClick={loadUsers}>Load Users</button>
</div>
);
}
<script setup lang="ts">
import { useJsonStream } from "@laravel/stream-vue";
type User = {
id: number;
name: string;
email: string;
};
const { data, send } = useJsonStream<{ users: User[] }>("users");
const loadUsers = () => {
send({
query: "taylor",
});
};
</script>
<template>
<div>
<ul>
<li v-for="user in data?.users" :key="user.id">
{{ user.id }}: {{ user.name }}
</li>
</ul>
<button @click="loadUsers">Load Users</button>
</div>
</template>
<script>
import { useJsonStream } from "@laravel/stream-svelte";
const stream = useJsonStream("users");
const loadUsers = () => {
stream.send({
query: "taylor",
});
};
</script>
<div>
<ul>
{#if $stream.data?.users}
{#each $stream.data.users as user (user.id)}
<li>{user.id}: {user.name}</li>
{/each}
{/if}
</ul>
<button onclick={loadUsers}>Load Users</button>
</div>
事件流(SSE)
eventStream() 方法可以返回一个服务器发送事件(SSE)流式响应,Content-Type 是 text/event-stream。eventStream() 接受一个闭包,闭包里应该逐个 yield 返回给流的响应:
Route::get('/chat', function () {
return response()->eventStream(function () {
$stream = OpenAI::client()->chat()->createStreamed(...);
foreach ($stream as $response) {
yield $response->choices[0];
}
});
});
如果你想自定义事件名,可以 yield 一个 StreamedEvent 实例:
use Illuminate\Http\StreamedEvent;
yield new StreamedEvent(
event: 'update',
data: $response->choices[0],
);
消费事件流
事件流同样可以用 Laravel 的 stream npm 包来消费。先安装对应的包:
npm install @laravel/stream-react
npm install @laravel/stream-vue
npm install @laravel/stream-svelte
然后用 useEventStream 来消费事件流。传入你的流 URL 后,这个 hook 会在 Laravel 应用返回消息时自动把 message 更新为拼接好的内容:
import { useEventStream } from "@laravel/stream-react";
function App() {
const { message } = useEventStream("/chat");
return <div>{message}</div>;
}
<script setup lang="ts">
import { useEventStream } from "@laravel/stream-vue";
const { message } = useEventStream("/chat");
</script>
<template>
<div>{{ message }}</div>
</template>
<script>
import { useEventStream } from "@laravel/stream-svelte";
const eventStream = useEventStream("/chat");
</script>
<div>{$eventStream.message}</div>
useEventStream 的第二个参数是一个选项对象,用来定制事件流的消费行为。下面列出了各项默认值:
import { useEventStream } from "@laravel/stream-react";
function App() {
const { message } = useEventStream("/stream", {
eventName: "update",
onMessage: (message) => {
//
},
onError: (error) => {
//
},
onComplete: () => {
//
},
endSignal: "</stream>",
glue: " ",
});
return <div>{message}</div>;
}
<script setup lang="ts">
import { useEventStream } from "@laravel/stream-vue";
const { message } = useEventStream("/chat", {
eventName: "update",
onMessage: (message) => {
// ...
},
onError: (error) => {
// ...
},
onComplete: () => {
// ...
},
endSignal: "</stream>",
glue: " ",
});
</script>
<script>
import { useEventStream } from "@laravel/stream-svelte";
const eventStream = useEventStream("/chat", {
eventName: "update",
onMessage: (event) => {
//
},
onError: (error) => {
//
},
onComplete: () => {
//
},
endSignal: "</stream>",
glue: " ",
replace: false,
});
</script>
事件流也可以通过前端的原生 EventSource 对象手动消费。eventStream() 方法在流结束时会自动发送一条 </stream> 更新给事件流:
const source = new EventSource('/chat');
source.addEventListener('update', (event) => {
if (event.data === '</stream>') {
source.close();
return;
}
console.log(event.data);
});
要自定义发送给事件流的结尾事件,给 eventStream() 方法的 endStreamWith 参数传一个 StreamedEvent 实例:
return response()->eventStream(function () {
// ...
}, endStreamWith: new StreamedEvent(event: 'update', data: '</stream>'));
流式下载
有时候你想把某段操作的字符串结果直接变成下载响应,而不是先写到磁盘上。用 streamDownload() 方法就行。它接受三个参数:回调函数、文件名、以及可选的请求头数组:
use App\Services\GitHub;
return response()->streamDownload(function () {
echo GitHub::api('repo')
->contents()
->readme('laravel', 'laravel')['contents'];
}, 'laravel-readme.md');
Response 宏
如果你想定义可复用的自定义响应,在 Response 门面上调用 macro() 方法。通常你应该在某个服务提供者的 boot() 方法里调用——比如 App\Providers\AppServiceProvider:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* 启动应用服务。
*/
public function boot(): void
{
Response::macro('caps', function (string $value) {
return Response::make(strtoupper($value));
});
}
}
macro() 方法的第一个参数是名称,第二个参数是闭包。之后你就可以从 ResponseFactory 实现或者 response() 辅助函数上调用这个宏名:
return response()->caps('foo');