Response 对象详解
Response 对象是 Viswoole 框架对 Swoole HTTP 响应的面向对象封装,提供了状态码设置、响应头操作、内容输出、Cookie 管理、文件发送等完整功能,并支持链式调用。它实现了 ResponseInterface 接口。
获取 Response 对象
在控制器中注入
use App\Response; // 自定义 Response 类(继承框架 Response)
public static function index(Response $response): ResponseInterface
{
return $response->html('<h1>Hello</h1>');
}使用 Facade
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:
/**
* @param string $html HTML 内容
* @return ResponseInterface 支持链式调用
*/
Response::html(string $html): ResponseInterface示例:
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:
/**
* @param mixed $data 可 JSON 编码的数据
* @return ResponseInterface 支持链式调用
* @throws RuntimeException 编码失败时抛出
*/
Response::json(mixed $data): ResponseInterface示例:
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:
public static function text(Response $response): ResponseInterface
{
return $response->setContentType('text/plain')->setContent('这是纯文本内容');
}redirect() - 重定向响应
发送 HTTP 重定向,默认使用 302 状态码:
/**
* @param string $uri 目标 URI
* @param int $http_code 状态码,默认 302
* @return bool 成功返回 true
*/
Response::redirect(string $uri, int $http_code = 302): bool示例:
// 临时重定向(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() - 设置状态码
/**
* @param int $http_status_code 状态码 (100-599)
* @param string $reasonPhrase 状态描述,为空时自动获取
* @return ResponseInterface 支持链式调用
*/
Response::status(int $http_status_code, string $reasonPhrase = ''): ResponseInterface常用状态码常量(Status 类):
| 常量 | 值 | 说明 |
|---|---|---|
Status::OK | 200 | 成功 |
Status::CREATED | 201 | 创建成功 |
Status::NO_CONTENT | 204 | 无内容 |
Status::BAD_REQUEST | 400 | 请求错误 |
Status::UNAUTHORIZED | 401 | 未认证 |
Status::FORBIDDEN | 403 | 禁止访问 |
Status::NOT_FOUND | 404 | 未找到 |
Status::METHOD_NOT_ALLOWED | 405 | 方法不允许 |
Status::UNPROCESSABLE_ENTITY | 422 | 无法处理 |
Status::INTERNAL_SERVER_ERROR | 500 | 服务器错误 |
示例:
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() - 设置单个响应头
/**
* @param string $key 标头名称
* @param string $value 标头值
* @param bool $format 是否按 HTTP 规范格式化键名
* @return ResponseInterface 支持链式调用
*/
Response::header(string $key, string $value, bool $format = true): ResponseInterface示例:
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() - 批量设置响应头
/**
* @param array<string, string|string[]> $headers 标头键值对
* @return ResponseInterface 支持链式调用
*/
Response::setHeaders(array $headers): ResponseInterface示例:
$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
/**
* @param string $contentType MIME 类型
* @param string $charset 字符集,默认 utf-8
* @return ResponseInterface 支持链式调用
*/
Response::setContentType(string $contentType, string $charset = 'utf-8'): ResponseInterface常用 Content-Type:
// 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() - 获取已设置的响应头
/**
* @return array<string, string> 所有已设置的标头
*/
Response::getHeader(): arrayCookie 操作
cookie() - 设置 Cookie(URL 编码)
值会经过 urlencode 处理:
/**
* @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示例:
// 设置会话 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):
// 存储 JWT Token(避免 URL 编码破坏 Token 格式)
return $response->rawCookie(
key: 'auth_token',
value: 'eyJhbGciOiJIUzI1NiIs...',
httponly: true,
secure: true,
samesite: 'Strict'
);发送文件
sendfile() - 发送本地文件
使用 Swoole 的零拷贝技术高效发送文件,适合大文件下载:
/**
* @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示例:
发送文件下载
/**
* 下载文件
*
* 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);
}发送图片/视频等媒体文件
/**
* 显示图片
*
* 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);
}断点续传
/**
* 视频流式播放(支持断点续传)
*
* 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() - 结束响应
/**
* 发送响应内容并结束请求
*
* @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):
/**
* 分段写入数据
*
* @param string $data 数据内容
* @return ResponseInterface 支持链式调用
* @throws RuntimeException 写入失败时抛出
*/
Response::write(string $data): ResponseInterface示例 - SSE 推送:
/**
* 实时日志推送(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 异步推送数据...
}示例 - 大文件生成:
/**
* 导出 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(),配合异步任务实现推送:
/**
* @return ResponseInterface 支持链式调用
* @throws RuntimeException 分离失败时抛出
*/
Response::detach(): ResponseInterface示例 - 异步推送:
/**
* 长连接异步推送
*
* 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() - 调试模式
将响应内容同时输出到控制台(仅开发环境使用):
/**
* @param bool $echo true 开启控制台输出
* @return ResponseInterface 支持链式调用
*/
Response::echo(bool $echo = true): ResponseInterface示例:
// 开发调试时开启
if (App::isLocal()) {
$response->echo(true);
}
return $response->json($data); // 同时输出到浏览器和控制台isWritable() - 检查可写状态
判断响应是否仍可写入(未结束且未分离):
/**
* @return bool 可写返回 true
*/
Response::isWritable(): boolif ($response->isWritable()) {
$response->write($chunk);
} else {
Log::warning('响应已不可写');
}create() - 工厂方法
创建新的 Response 实例(用于非 onRequest 回调场景):
/**
* @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
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
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);
}
}使用:
// 在控制器中使用
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 响应:
// 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. 安全响应头
始终添加安全相关的响应头:
$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 接口需要支持跨域请求时:
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:
#[AutoController(prefix: 'api/v1')]
class V1UserController { ... }
#[AutoController(prefix: 'api/v2')]
class V2UserController { ... }Q: 如何处理大规模数据的分块响应?
A: 使用 write() 方法进行流式传输,避免一次性加载到内存。
Q: sendfile 有什么限制?
A:
- 只能发送本地文件
- 不支持 HTTP/2
- 一旦调用 sendfile,不能再调用其他输出方法
