Response 对象详解

Response 对象是 Viswoole 框架对 Swoole HTTP 响应的面向对象封装,提供了状态码设置、响应头操作、内容输出、Cookie 管理、文件发送等完整功能,并支持链式调用。它实现了 ResponseInterface 接口。

获取 Response 对象

在控制器中注入

php
use App\Response;  // 自定义 Response 类(继承框架 Response)

public static function index(Response $response): ResponseInterface
{
    return $response->html('<h1>Hello</h1>');
}

使用 Facade

php
use Viswoole\HttpServer\Facade\Response;

public static function data(): array
{
    // 直接返回数组,框架自动转为 JSON
    return ['status' => 'ok'];
}

// 或手动操作 Response 对象
public static function custom(): ResponseInterface
{
    return Response::json(['message' => 'success']);
}

快速响应方法

html() - HTML 响应

返回 HTML 内容,自动设置 Content-Type: text/html

php
/**
 * @param string $html HTML 内容
 * @return ResponseInterface 支持链式调用
 */
Response::html(string $html): ResponseInterface

示例:

php
public static function home(Response $response): ResponseInterface
{
    return $response->html('
        <!DOCTYPE html>
        <html>
        <head><title>首页</title></head>
        <body><h1>欢迎访问</h1></body>
        </html>
    ');
}

json() - JSON 响应

将数据编码为 JSON 并返回,自动设置 Content-Type: application/json

php
/**
 * @param mixed $data 可 JSON 编码的数据
 * @return ResponseInterface 支持链式调用
 * @throws RuntimeException 编码失败时抛出
 */
Response::json(mixed $data): ResponseInterface

示例:

php
public static function userList(Response $response): ResponseInterface
{
    $users = [
        ['id' => 1, 'name' => '张三', 'email' => 'zhangsan@example.com'],
        ['id' => 2, 'name' => '李四', 'email' => 'lisi@example.com'],
    ];

    return $response->json([
        'code' => 200,
        'message' => 'success',
        'data' => $users,
        'total' => count($users)
    ]);
}

默认 JSON 编码选项:

  • JSON_UNESCAPED_UNICODE:不转义 Unicode 字符(中文正常显示)
  • JSON_UNESCAPED_SLASHES:不转义斜杠

纯文本响应

返回纯文本内容,通过 setContentType 设置 Content-Type: text/plain

php
public static function text(Response $response): ResponseInterface
{
    return $response->setContentType('text/plain')->setContent('这是纯文本内容');
}

redirect() - 重定向响应

发送 HTTP 重定向,默认使用 302 状态码:

php
/**
 * @param string $uri 目标 URI
 * @param int $http_code 状态码,默认 302
 * @return bool 成功返回 true
 */
Response::redirect(string $uri, int $http_code = 302): bool

示例:

php
// 临时重定向(302)
public static function oldUrl(Response $response): bool
{
    return $response->redirect('/new-url');
}

// 永久重定向(301)
public static function movedPermanently(Response $response): bool
{
    return $response->redirect('/new-location', 301);
}

// 重定向到外部 URL
public static function externalRedirect(Response $response): bool
{
    return $response->redirect('https://example.com');
}

响应状态码

status() - 设置状态码

php
/**
 * @param int $http_status_code 状态码 (100-599)
 * @param string $reasonPhrase 状态描述,为空时自动获取
 * @return ResponseInterface 支持链式调用
 */
Response::status(int $http_status_code, string $reasonPhrase = ''): ResponseInterface

常用状态码常量(Status 类):

常量说明
Status::OK200成功
Status::CREATED201创建成功
Status::NO_CONTENT204无内容
Status::BAD_REQUEST400请求错误
Status::UNAUTHORIZED401未认证
Status::FORBIDDEN403禁止访问
Status::NOT_FOUND404未找到
Status::METHOD_NOT_ALLOWED405方法不允许
Status::UNPROCESSABLE_ENTITY422无法处理
Status::INTERNAL_SERVER_ERROR500服务器错误

示例:

php
use Viswoole\HttpServer\Status;

// 成功创建资源
public static function create(Response $response): ResponseInterface
{
    // 业务逻辑...
    return $response
        ->status(Status::CREATED)
        ->json(['id' => 123, 'message' => '创建成功']);
}

// 资源未找到
public static function notFound(Response $response): ResponseInterface
{
    return $response
        ->status(Status::NOT_FOUND)
        ->json(['error' => '资源不存在']);
}

// 参数验证失败
public static function validationError(Response $response): ResponseInterface
{
    return $response
        ->status(Status::UNPROCESSABLE_ENTITY)
        ->json([
            'code' => 422,
            'message' => '参数验证失败',
            'errors' => [
                'name' => '姓名不能为空',
                'email' => '邮箱格式不正确'
            ]
        ]);
}

// 自定义状态描述
public static function customStatus(Response $response): ResponseInterface
{
    return $response
        ->status(418, "I'm a teapot") // HTTP 418 俏皮状态码
        ->setContentType('text/plain')->setContent("☕ 茶壶拒绝冲泡咖啡");
}

响应头操作

header() - 设置单个响应头

php
/**
 * @param string $key 标头名称
 * @param string $value 标头值
 * @param bool $format 是否按 HTTP 规范格式化键名
 * @return ResponseInterface 支持链式调用
 */
Response::header(string $key, string $value, bool $format = true): ResponseInterface

示例:

php
return $response
    ->header('X-Custom-Header', 'custom-value')
    ->header('X-Request-Time', (string)microtime(true))
    ->header('Cache-Control', 'no-cache, no-store, must-revalidate')
    ->json($data);

setHeaders() - 批量设置响应头

php
/**
 * @param array<string, string|string[]> $headers 标头键值对
 * @return ResponseInterface 支持链式调用
 */
Response::setHeaders(array $headers): ResponseInterface

示例:

php
$headers = [
    'Cache-Control' => 'public, max-age=3600',
    'X-Content-Type-Options' => 'nosniff',
    'X-Frame-Options' => 'DENY',
    'X-XSS-Protection' => '1; mode=block',
];

return $response
    ->setHeaders($headers)
    ->json($data);

setContentType() - 快捷设置 Content-Type

php
/**
 * @param string $contentType MIME 类型
 * @param string $charset 字符集,默认 utf-8
 * @return ResponseInterface 支持链式调用
 */
Response::setContentType(string $contentType, string $charset = 'utf-8'): ResponseInterface

常用 Content-Type:

php
// JSON
$response->setContentType('application/json');

// HTML
$response->setContentType('text/html');

// XML
$response->setContentType('application/xml');

// 纯文本
$response->setContentType('text/plain');

// PDF
$response->setContentType('application/pdf');

// 图片
$response->setContentType('image/png');
$response->setContentType('image/jpeg');

// 文件下载
$response->setContentType('application/octet-stream');

getHeader() - 获取已设置的响应头

php
/**
 * @return array<string, string> 所有已设置的标头
 */
Response::getHeader(): array

值会经过 urlencode 处理:

php
/**
 * @param string $key Cookie 名称
 * @param string $value Cookie 值
 * @param int $expire 过期时间戳(0 = 会话结束)
 * @param string $path 有效路径
 * @param string $domain 有效域名
 * @param bool $secure 仅 HTTPS
 * @param bool $httponly 禁止 JS 访问
 * @param string $samesite SameSite 策略
 * @param string $priority 优先级
 * @return ResponseInterface 支持链式调用
 */
Response::cookie(
    string $key,
    string $value = '',
    int $expire = 0,
    string $path = '/',
    string $domain = '',
    bool $secure = false,
    bool $httponly = false,
    string $samesite = '',
    string $priority = ''
): ResponseInterface

示例:

php
// 设置会话 Cookie(浏览器关闭后失效)
return $response->cookie('session_id', 'abc123');

// 设置持久化 Cookie(7 天有效)
return $response->cookie(
    key: 'remember_token',
    value: 'xyz789',
    expire: time() + (86400 * 7), // 7 天
    httponly: true,                 // 防止 XSS 窃取
    secure: true,                   // 仅 HTTPS
    samesite: 'Strict'              // 严格同站策略
);

// 删除 Cookie(设置过期时间为过去的时间)
return $response->cookie(
    key: 'old_cookie',
    value: '',
    expire: time() - 3600
);

rawCookie() - 设置原始 Cookie(不编码)

适用于需要存储原始值的场景(如 JWT Token):

php
// 存储 JWT Token(避免 URL 编码破坏 Token 格式)
return $response->rawCookie(
    key: 'auth_token',
    value: 'eyJhbGciOiJIUzI1NiIs...',
    httponly: true,
    secure: true,
    samesite: 'Strict'
);

发送文件

sendfile() - 发送本地文件

使用 Swoole 的零拷贝技术高效发送文件,适合大文件下载:

php
/**
 * @param string $filePath 文件绝对路径
 * @param int $offset 起始偏移量(字节)
 * @param int $length 发送长度(0 = 全部)
 * @param string|null $fileMimeType MIME 类型(null 则自动检测)
 * @return bool 发送成功返回 true
 * @throws InvalidArgumentException 文件不存在时抛出
 */
Response::sendfile(
    string $filePath,
    int $offset = 0,
    int $length = 0,
    ?string $fileMimeType = null
): bool

示例:

发送文件下载

php
/**
 * 下载文件
 *
 * GET /download/file?id=123
 */
public static function downloadFile(#[InjectGet] int $id, Response $response): bool
{
    // 从数据库获取文件路径
    $file = FileService::getFileById($id);
    $filePath = app()->getRootPath() . '/storage/files/' . $file['path'];

    // 检查文件是否存在
    if (!file_exists($filePath)) {
        $response->status(Status::NOT_FOUND)->json(['error' => '文件不存在']);
        return false;
    }

    // 设置下载响应头
    $fileName = urlencode($file['original_name']);
    return $response
        ->header('Content-Disposition', "attachment; filename=\"{$fileName}\"")
        ->sendfile($filePath);
}

发送图片/视频等媒体文件

php
/**
 * 显示图片
 *
 * GET /image/show?id=100
 */
public static function showImage(#[InjectGet] int $id, Response $response): bool
{
    $image = ImageService::getImageById($id);
    $filePath = app()->getRootPath() . '/storage/images/' . $image['path'];

    // 自动检测 MIME 类型并发送
    return $response->sendfile($filePath);
}

断点续传

php
/**
 * 视频流式播放(支持断点续传)
 *
 * GET /video/play?id=50
 */
public static function playVideo(#[InjectGet] int $id, Response $response): bool
{
    $video = VideoService::getVideoById($id);
    $filePath = app()->getRootPath() . '/storage/videos/' . $video['path'];

    // 指定 MIME 类型为 video/mp4
    return $response
        ->header('Accept-Ranges', 'bytes')
        ->sendfile($filePath, fileMimeType: 'video/mp4');
}

输出响应

end() / send() - 结束响应

php
/**
 * 发送响应内容并结束请求
 *
 * @param string|null $content 响应内容(null 使用已设置的内容)
 * @return bool 成功返回 true
 */
Response::end(?string $content = null): bool

// send() 是 end() 的别名
Response::send(?string $content = null): bool

注意: 通常不需要手动调用 end(),控制器方法返回 ResponseInterface 时框架会自动调用。

write() - 分段写入(Chunked)

用于流式传输大体积数据或 SSE(Server-Sent Events):

php
/**
 * 分段写入数据
 *
 * @param string $data 数据内容
 * @return ResponseInterface 支持链式调用
 * @throws RuntimeException 写入失败时抛出
 */
Response::write(string $data): ResponseInterface

示例 - SSE 推送:

php
/**
 * 实时日志推送(SSE)
 *
 * GET /stream/logs
 */
public static function streamLogs(Response $response): ResponseInterface
{
    // 设置 SSE 响应头
    return $response
        ->header('Content-Type', 'text/event-stream')
        ->header('Cache-Control', 'no-cache')
        ->header('Connection', 'keep-alive')
        ->write("data: {\"type\":\"connected\"}\n\n");

    // 后续通过 Task 异步推送数据...
}

示例 - 大文件生成:

php
/**
 * 导出 CSV(流式写入)
 *
 * GET /export/csv
 */
public static function exportCsv(Response $response): ResponseInterface
{
    $response
        ->header('Content-Type', 'text/csv')
        ->header('Content-Disposition', 'attachment; filename="export.csv"');

    // 写入 CSV 表头
    $response->write("ID,Name,Email,CreatedAt\n");

    // 分批查询并写入数据
    $batchSize = 1000;
    $page = 1;

    do {
        $rows = UserService::exportBatch($page, $batchSize);
        foreach ($rows as $row) {
            $csvLine = implode(',', [
                $row['id'],
                $row['name'],
                $row['email'],
                $row['created_at']
            ]);
            $response->write($csvLine . "\n");
        }
        $page++;
    } while (count($rows) === $batchSize);

    return $response;
}

高级功能

detach() - 分离响应对象

分离响应对象使其在销毁时不自动 end(),配合异步任务实现推送:

php
/**
 * @return ResponseInterface 支持链式调用
 * @throws RuntimeException 分离失败时抛出
 */
Response::detach(): ResponseInterface

示例 - 异步推送:

php
/**
 * 长连接异步推送
 *
 * GET /async/task?id=100
 */
public static function asyncTask(#[InjectGet] int $id, Response $response): ResponseInterface
{
    // 分离响应对象
    $response->detach();

    // 通过 Request 获取当前连接的 fd
    $fd = \Viswoole\HttpServer\Facade\Request::getSwooleRequest()->fd;

    // 投递异步任务
    Task::async(function () use ($id, $fd) {
        // 执行耗时任务...
        $result = LongRunningService::process($id);

        // 任务完成后主动推送结果
        Server::push($fd, json_encode($result));
    });

    return $response->json(['status' => 'processing', 'task_id' => $id]);
}

echo() - 调试模式

将响应内容同时输出到控制台(仅开发环境使用):

php
/**
 * @param bool $echo true 开启控制台输出
 * @return ResponseInterface 支持链式调用
 */
Response::echo(bool $echo = true): ResponseInterface

示例:

php
// 开发调试时开启
if (App::isLocal()) {
    $response->echo(true);
}

return $response->json($data); // 同时输出到浏览器和控制台

isWritable() - 检查可写状态

判断响应是否仍可写入(未结束且未分离):

php
/**
 * @return bool 可写返回 true
 */
Response::isWritable(): bool
php
if ($response->isWritable()) {
    $response->write($chunk);
} else {
    Log::warning('响应已不可写');
}

create() - 工厂方法

创建新的 Response 实例(用于非 onRequest 回调场景):

php
/**
 * @param object|array|int $server Swoole Server 或 fd
 * @param int $fd 文件描述符
 * @return ResponseInterface
 */
Response::create(object|array|int $server = -1, int $fd = -1): ResponseInterface

完整 API 示例

标准 RESTful API 响应

php
<?php
declare(strict_types=1);

namespace App\Controller;

use App\Service\UserService;
use Viswoole\HttpServer\AutoInject\InjectGet;
use Viswoole\HttpServer\AutoInject\InjectPost;
use Viswoole\HttpServer\Contract\ResponseInterface;
use Viswoole\HttpServer\Status;
use Viswoole\Router\Annotation\AutoController;
use Viswoole\Router\Annotation\RouteMapping;
use App\Response;

/**
 * 用户 API 控制器 - 展示完整的响应操作
 */
#[AutoController(prefix: 'api/v2')]
class UserApiController
{
    /**
     * 成功响应(列表)
     *
     * GET /api/v2/user-api/list?page=1&size=10
     */
    #[RouteMapping(method: 'GET')]
    public static function list(
        #[InjectGet] int $page = 1,
        #[InjectGet] int $size = 10,
        Response $response
    ): ResponseInterface {
        $result = UserService::getList($page, $size);

        return $response
            ->status(Status::OK)
            ->setHeaders([
                'X-Total-Count' => (string)$result['total'],
                'X-Page' => (string)$page,
                'X-Per-Page' => (string)$size,
            ])
            ->json([
                'code' => 200,
                'message' => 'success',
                'data' => $result['data'],
                'pagination' => [
                    'total' => $result['total'],
                    'page' => $page,
                    'size' => $size,
                    'total_page' => ceil($result['total'] / $size),
                ]
            ]);
    }

    /**
     * 创建成功(201)
     *
     * POST /api/v2/user-api/create
     */
    #[RouteMapping(method: 'POST')]
    public static function create(
        #[InjectPost] CreateUserDto $dto,
        Response $response
    ): ResponseInterface {
        $userId = UserService::create($dto);

        return $response
            ->status(Status::CREATED) // 201 Created
            ->header('Location', "/api/v2/user-api/detail?id={$userId}")
            ->json([
                'code' => 201,
                'message' => '创建成功',
                'data' => ['id' => $userId]
            ]);
    }

    /**
     * 验证错误(422)
     *
     * POST /api/v2/user-api/validate
     */
    #[RouteMapping(method: 'POST')]
    public static function validate(
        #[InjectPost] CreateUserDto $dto,
        Response $response
    ): ResponseInterface {
        // 如果 DTO 验证失败,框架已自动返回 422 错误
        // 这里展示手动验证的场景
        $errors = Validator::validate($dto);

        if (!empty($errors)) {
            return $response
                ->status(Status::UNPROCESSABLE_ENTITY) // 422
                ->json([
                    'code' => 422,
                    'message' => '参数验证失败',
                    'errors' => $errors
                ]);
        }

        return $response->json(['code' => 200, 'message' => '验证通过']);
    }

    /**
     * 未授权(401)
     *
     * GET /api/v2/user-api/profile
     */
    #[RouteMapping(method: 'GET')]
    public static function profile(Response $response): ResponseInterface
    {
        // 模拟未登录
        if (!Auth::check()) {
            return $response
                ->status(Status::UNAUTHORIZED) // 401
                ->setHeader('WWW-Authenticate', 'Bearer realm="API"')
                ->json([
                    'code' => 401,
                    'message' => '未认证,请先登录'
                ]);
        }

        $user = Auth::user();
        return $response->json([
            'code' => 200,
            'data' => $user
        ]);
    }

    /**
     * 禁止访问(403)
     *
     * DELETE /api/v2/user-api/delete?id=1
     */
    #[RouteMapping(method: 'DELETE')]
    public static function delete(
        #[InjectPost] int $id,
        Response $response
    ): ResponseInterface {
        // 权限检查
        if (!Auth::can('delete_user')) {
            return $response
                ->status(Status::FORBIDDEN) // 403
                ->json([
                    'code' => 403,
                    'message' => '权限不足,无法删除用户'
                ]);
        }

        UserService::delete($id);

        return $response
            ->status(Status::NO_CONTENT) // 204 No Content
            ->end();
    }

    /**
     * 文件下载
     *
     * GET /api/v2/user-api/export?format=csv
     */
    #[RouteMapping(method: 'GET')]
    public static function export(
        #[InjectGet] string $format = 'csv',
        Response $response
    ): bool {
        $filePath = UserService::exportUsers($format);

        $fileName = 'users_' . date('Ymd_His') . '.' . $format;

        return $response
            ->header('Content-Disposition', "attachment; filename=\"{$fileName}\"")
            ->sendfile($filePath);
    }
}

最佳实践

1. 统一 API 响应格式

建议封装统一的响应方法:

php
<?php
declare(strict_types=1);

namespace App\Support;

use Viswoole\HttpServer\Contract\ResponseInterface;
use Viswoole\HttpServer\Status;
use App\Response;

class ApiResponse
{
    /**
     * 成功响应
     */
    public static function success(mixed $data = null, string $message = 'success'): ResponseInterface
    {
        $response = app(Response::class);
        return $response
            ->status(Status::OK)
            ->json([
                'code' => 200,
                'message' => $message,
                'data' => $data,
                'timestamp' => time()
            ]);
    }

    /**
     * 分页响应
     */
    public static function paginated(
        array $data,
        int $total,
        int $page,
        int $size
    ): ResponseInterface {
        $response = app(Response::class);
        return $response
            ->status(Status::OK)
            ->setHeaders([
                'X-Total-Count' => (string)$total,
            ])
            ->json([
                'code' => 200,
                'data' => $data,
                'pagination' => [
                    'total' => $total,
                    'page' => $page,
                    'size' => $size,
                    'total_page' => (int)ceil($total / $size),
                ]
            ]);
    }

    /**
     * 错误响应
     */
    public static function error(
        int $code,
        string $message,
        ?array $errors = null
    ): ResponseInterface {
        $response = app(Response::class);
        $statusCode = match(true) {
            $code >= 400 && $code < 500 => $code,
            default => Status::BAD_REQUEST,
        };

        $data = [
            'code' => $code,
            'message' => $message,
            'timestamp' => time()
        ];

        if ($errors !== null) {
            $data['errors'] = $errors;
        }

        return $response
            ->status($statusCode)
            ->json($data);
    }
}

使用:

php
// 在控制器中使用
public static function list(): ResponseInterface
{
    $data = UserService::getList(...);
    return ApiResponse::paginated($data['list'], $data['total'], $page, $size);
}

public static function notFound(): ResponseInterface
{
    return ApiResponse::error(404, '资源不存在');
}

2. 合理设置缓存头

对于静态资源和 API 响应:

php
// API 响应:禁用缓存
return $response
    ->setHeaders([
        'Cache-Control' => 'no-store, no-cache, must-revalidate',
        'Pragma' => 'no-cache',
        'Expires' => '0',
    ])
    ->json($data);

// 静态资源:启用缓存
return $response
    ->setHeaders([
        'Cache-Control' => 'public, max-age=3600',
        'ETag' => md5_file($filePath),
    ])
    ->sendfile($filePath);

3. 安全响应头

始终添加安全相关的响应头:

php
$securityHeaders = [
    'X-Content-Type-Options' => 'nosniff',
    'X-Frame-Options' => 'DENY',
    'X-XSS-Protection' => '1; mode=block',
    'Referrer-Policy' => 'strict-origin-when-cross-origin',
    'Content-Security-Policy' => "default-src 'self'",
];

// 可以通过中间件全局添加

4. CORS 跨域配置

API 接口需要支持跨域请求时:

php
return $response
    ->setHeaders([
        'Access-Control-Allow-Origin' => '*',
        'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS',
        'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-Requested-With',
        'Access-Control-Max-Age' => '86400',
    ])
    ->json($data);

常见问题

Q: 返回数组和返回 ResponseInterface 有什么区别?

A:

  • 返回数组:框架自动将其 JSON 编码并输出,简单场景推荐
  • 返回 ResponseInterface:完全控制响应头、状态码、Cookie 等,复杂场景必需

Q: 如何实现 API 版本控制?

A: 通过路由前缀或自定义 Header:

php
#[AutoController(prefix: 'api/v1')]
class V1UserController { ... }

#[AutoController(prefix: 'api/v2')]
class V2UserController { ... }

Q: 如何处理大规模数据的分块响应?

A: 使用 write() 方法进行流式传输,避免一次性加载到内存。

Q: sendfile 有什么限制?

A:

  • 只能发送本地文件
  • 不支持 HTTP/2
  • 一旦调用 sendfile,不能再调用其他输出方法