响应

未匹配的标注
本文档最新版为 11.x,旧版本可能放弃维护,推荐阅读最新版!

HTTP 响应

创建响应

字符串和数组

所有路由和控制器都应该返回一个响应,以便发送回用户的浏览器。Laravel 提供了几种不同的方式来返回响应。最基本的响应就是从路由或控制器返回一个字符串。框架会自动将字符串转换为完整的 HTTP 响应:

Route::get('/', function () {
    return 'Hello World';
});

除了可以从路由和控制器返回字符串之外,你还可以返回数组。框架会自动将数组转换为 JSON 响应:

Route::get('/', function () {
    return [1, 2, 3];
});

[!注意]
你知道吗?你也可以直接从路由或控制器返回 Eloquent 集合。它们会自动被转换成 JSON。试试看吧!

响应对象

通常情况下,你不会只从路由动作返回简单的字符串或数组。相反,你会返回完整的 Illuminate\Http\Response 实例,或者 视图

返回一个完整的 Response 实例可以让你自定义响应的 HTTP 状态码和 Header。
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;
});

给响应附加 Headers

请记住,大多数响应方法都是可链式调用的,可以让你以流式的方式构建响应实例。
例如,你可以使用 header 方法,在将响应返回给用户之前添加一系列 Header:

return response($content)
    ->header('Content-Type', $type)
    ->header('X-Header-One', 'Header Value')
    ->header('X-Header-Two', 'Header Value');

或者,你也可以使用 withHeaders 方法,一次性指定一个数组来添加多个 Header:

return response($content)
    ->withHeaders([
        'Content-Type' => $type,
        'X-Header-One' => 'Header Value',
        'X-Header-Two' => 'Header Value',
    ]);

缓存控制中间件

Laravel 内置了一个 cache.headers 中间件,可以用来快速为一组路由设置 Cache-Control Header。
指令应使用对应的 snake case(蛇形命名) 形式,并且用分号隔开。
如果在指令中包含 etag,Laravel 会自动将响应内容的 MD5 哈希值作为 ETag 标识:

Route::middleware('cache.headers:public;max_age=2628000;etag')->group(function () {
    Route::get('/privacy', function () {
        // ...
    });

    Route::get('/terms', function () {
        // ...
    });
});

在响应中附加 Cookie 信息

你可以使用 cookie 方法将 cookie 附加到传出的 illumize\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 facade 将 cookie 加入队列,以便在发送响应时附加到响应中。queue 方法接受创建 cookie 实例所需的参数。在发送到浏览器之前,这些 cookies 将附加到传出的响应中:

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 facade 的 expire 方法让某个 Cookie 过期:

Cookie::expire('name');

Cookie 与加密

ay use the encryptCookies method in your application's bootstrap/app.php file:
默认情况下,多亏了 Illuminate\Cookie\Middleware\EncryptCookies 中间件,Laravel 生成的所有 Cookie 都会被 加密并签名,因此客户端无法修改或读取。
如果你想在应用中为部分 Cookie 关闭加密,可以在 bootstrap/app.php 文件里通过 encryptCookies 方法来配置:

->withMiddleware(function (Middleware $middleware) {
    $middleware->encryptCookies(except: [
        'cookie_name',
    ]);
})

重定向

重定向响应是 Illuminate\Http\RedirectResponse 类的实例,包含了正确的 Header,用于将用户重定向到另一个 URL。
有多种方式可以生成 RedirectResponse 实例,最简单的方法是使用全局的 redirect 辅助函数:

Route::get('/dashboard', function () {
    return redirect('/home/dashboard');
});

有时你可能希望把用户重定向回他们之前的位置,比如在表单提交无效时。
你可以使用全局的 back 辅助函数来实现。由于这个功能依赖 session,因此要确保调用 back 的路由使用的是 web 中间件组:

Route::post('/user/profile', function () {
    // 验证请求...

    return back()->withInput();
});

重定向到命名路由

当你在调用 redirect 辅助函数时不带任何参数,会返回一个 Illuminate\Routing\Redirector 实例,你可以在该实例上调用任意方法。
例如,要生成一个指向命名路由的 RedirectResponse,你可以使用 route 方法:

return redirect()->route('login');

如果你的路由带有参数,可以把参数作为第二个参数传给 route 方法:

// 对应的路由 URI: /profile/{id}

return redirect()->route('profile', ['id' => 1]);

通过 Eloquent 模型填充参数

如果你要重定向到一个带有 “ID” 参数的路由,并且该参数值来自一个 Eloquent 模型,你可以直接传递模型对象。
Laravel 会自动提取该模型的 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']);

如果控制器方法的路由需要参数,你可以将参数作为第二个参数传给 action 方法:

return redirect()->action(
    [UserController::class, 'profile'], ['id' => 1]
);

重定向到外部域名

有时候你可能需要重定向到你应用程序之外的某个域名。
你可以调用 away 方法来实现,这会创建一个 RedirectResponse,而不会附加任何额外的 URL 编码、验证或校验:

return redirect()->away('https://www.google.com');

带闪存session数据的重定向

通常情况下,重定向到一个新的 URL 时会同时 闪存数据到会话
这通常是在成功执行某个操作后,把一条成功消息写入到会话中。
为了方便,你可以通过链式调用,在一个 RedirectResponse 实例上同时完成重定向和闪存数据:

Route::post('/user/profile', function () {
    // ...

    return redirect('/dashboard')->with('status', 'Profile updated!');
});

在用户被重定向之后,你就可以从 session 中显示这条闪存消息。
例如,使用 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 方法可用于生成一个响应,从而强制用户的浏览器下载指定路径下的文件。
download 方法接受一个文件名作为第二个参数,它决定了用户下载时看到的文件名。
最后,你还可以将 HTTP 头信息数组作为第三个参数传递给该方法:

return response()->download($pathToFile);

return response()->download($pathToFile, $name, $headers);

[!警告]
管理文件下载的 Symfony HttpFoundation 要求被下载的文件必须有一个 ASCII 文件名。

文件响应

file 方法可用于直接在用户浏览器中显示文件(例如图片或 PDF),而不是触发下载。
此方法的第一个参数是文件的绝对路径,第二个参数是 HTTP 头数组:

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 会在生成器返回的每个字符串之间自动刷新输出缓冲,并禁用 Nginx 输出缓冲:

Route::get('/chat', function () {
    return response()->stream(function (): void {
        $stream = OpenAI::client()->chat()->createStreamed(...);

        foreach ($stream as $response) {
            yield $response->choices[0];
        }
    });
});

使用流式响应

流式响应可以通过 Laravel 的 stream npm 包进行消费,该包提供了方便的 API 用于处理 Laravel 响应和事件流。
开始使用前,安装对应的前端包:

npm install @laravel/stream-react
npm install @laravel/stream-vue

然后,可以使用 useStream 来消费事件流。提供你的流 URL 后,该 Hook 会自动更新 data,将从 Laravel 应用返回的内容按顺序拼接显示:

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>

通过 send 向流发送数据时,当前活跃的连接会在发送新数据前被取消。所有请求都会以 JSON POST 请求形式发送。

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>

onResponse 会在流返回初始响应成功后触发,并将原生 Response 传递给回调。onData 在每个数据块接收时触发,将当前块传递给回调。onFinish 在流结束或在 fetch/read 循环中出现错误时触发。

默认情况下,初始化时不会向流发送请求。你可以通过 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>

可以使用 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>

每次使用 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>

流式 JSON 响应

如果需要逐步流式发送 JSON 数据,可以使用 streamJson 方法。这个方法在处理需要逐步发送到浏览器的大型数据集时特别有用,数据格式便于 JavaScript 解析:

use App\Models\User;

Route::get('/users.json', function () {
    return response()->streamJson([
        'users' => User::cursor(),
    ]);
});

useJsonStream Hook 与 useStream Hook 类似,但在流结束后会尝试将数据解析为 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>

事件流(SSE)

eventStream 方法用于返回服务器推送事件(SSE)流式响应,Content-Type 为 text/event-stream。它接受一个闭包,该闭包应在响应可用时通过 yield 将数据发送到流:

Route::get('/chat', function () {
    return response()->eventStream(function () {
        $stream = OpenAI::client()->chat()->createStreamed(...);

        foreach ($stream as $response) {
            yield $response->choices[0];
        }
    });
});

如果你想自定义事件名称,可以通过 StreamedEvent 类生成事件:

use Illuminate\Http\StreamedEvent;

yield new StreamedEvent(
    event: 'update',
    data: $response->choices[0],
);

消费事件流

事件流可以通过 Laravel 的 stream npm 包来消费,它提供了方便的 API 与 Laravel 的事件流交互。首先安装相应的包:

npm install @laravel/stream-react
npm install @laravel/stream-vue

然后使用 useEventStream Hook 消费事件流。提供流 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>

useEventStream 的第二个参数是可选的配置对象,用于自定义事件流消费行为,默认值如下:

import { useEventStream } from "@laravel/stream-react";

function App() {
  const { message } = useEventStream("/stream", {
    event: "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", {
  event: "update",
  onMessage: (message) => {
    // ...
  },
  onError: (error) => {
    // ...
  },
  onComplete: () => {
    // ...
  },
  endSignal: "</stream>",
  glue: " ",
});
</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 方法。它接受一个回调、文件名,以及可选的 HTTP 头数组:

use App\Services\GitHub;

return response()->streamDownload(function () {
    echo GitHub::api('repo')
        ->contents()
        ->readme('laravel', 'laravel')['contents'];
}, 'laravel-readme.md');

响应宏

如果你想定义一个可在多个路由或控制器中复用的自定义响应,可以使用 Response facade 的 macro 方法。通常在应用的服务提供者(如 App\Providers\AppServiceProvider)的 boot 方法中调用:

<?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 方法接受宏名称和一个闭包。调用宏时,闭包会在 ResponseFactoryresponse 助手中执行:

return response()->caps('foo');

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel/12.x/re...

译文地址:https://learnku.com/docs/laravel/12.x/re...

上一篇 下一篇
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~