文件上传
本指南演示如何在 Viswoole 中接收并处理上传文件:用 #[InjectFile] 注入 UploadedFile 对象、用 #[FileRule] 校验类型与大小、用 moveTo() 落盘保存,并给出可直接复用的完整接口示例。
准备上传表单
上传接口通过 multipart/form-data 编码接收文件,HTML 表单必须设置 enctype:
<form action="/upload/avatar" method="POST" enctype="multipart/form-data">
<input type="file" name="avatar" accept="image/*" />
<button type="submit">上传</button>
</form>步骤一:用 #[InjectFile] 声明上传参数
#[InjectFile](Viswoole\HttpServer\AutoInject\InjectFile)标注在方法参数上,框架自动从上传文件中按表单字段名取值:
- 字段对应单个文件时注入
UploadedFile; - 字段对应多个文件时注入
UploadedFile[]; - 文件缺失时抛出
ValidateException(可空类型或声明默认值则允许缺失)。
use Viswoole\HttpServer\AutoInject\InjectFile;
use Viswoole\HttpServer\Message\UploadedFile;
#[RouteMapping(method: 'POST')]
public function avatar(#[InjectFile] UploadedFile $avatar): array
{
return [
'name' => $avatar->getClientFilename(), // 客户端原始文件名
'size' => $avatar->getSize(), // 字节数
];
}步骤二:用 #[FileRule] 校验文件
#[FileRule](Viswoole\HttpServer\Validate\FileRule)是验证规则注解,与 #[InjectFile] 叠加使用,注入完成后自动校验:
use Viswoole\HttpServer\Validate\FileRule;
#[RouteMapping(method: 'POST')]
public function avatar(
#[FileRule(
fileMime: 'image/png|image/jpeg',
maxSize: 5242880, // 5MB
message: '请上传 5MB 以内的 PNG/JPEG 图片'
), InjectFile]
UploadedFile $avatar,
): array {}| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| fileMime | string | '*' | 允许的 MIME 类型,多个用 | 分隔,'*' 表示不限制 |
| maxSize | int | 0 | 单个文件最大字节数,小于等于 0 不限制 |
| count | int | 0 | 要求的文件数量(必须恰好相等),小于等于 0 不限制 |
| message | string | '' | 自定义错误消息,非空时覆盖默认提示 |
校验失败抛出 ValidateException(400 响应),默认错误消息:
必须上传文件(字段为空数组等异常情况)必须上传 {count} 个文件文件类型必须为 {fileMime}文件大小不能超过 {maxSize} 字节
MIME 校验基于文件内容而非声明值
FileRule 使用 UploadedFile::getRealMimeType()(finfo 内容检测)做白名单匹配,客户端声明的 Content-Type 可被任意伪造,不能作为校验依据。仅支持精确 MIME 列表,不支持 image/* 前缀通配,如需匹配一类文件请逐一列出。
步骤三:保存文件
moveTo() 将临时文件移动到目标路径(含文件名),目标目录不存在时自动递归创建(权限 0755):
$originalName = $file->getClientFilename() ?? 'upload.bin';
// 用随机名避免重名与路径注入,扩展名取自原始文件名
$extension = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
$newName = date('YmdHis') . '_' . bin2hex(random_bytes(8)) . '.' . $extension;
$targetPath = app()->getRootPath() . '/runtime/uploads/avatars/' . $newName;
$file->moveTo($targetPath);移动特性(源码 UploadedFile::moveTo()):
- CLI 环境使用
rename(),其他环境使用move_uploaded_file(); - 文件只能移动一次,重复调用或对已移动/上传出错的文件操作会抛出
RuntimeException; - 移动成功后再调用
getStream()同样会抛异常。
多文件上传
表单字段以 [] 结尾并添加 multiple 属性即可多选:
<form action="/upload/photos" method="POST" enctype="multipart/form-data">
<input type="file" name="photos[]" multiple accept="image/*" />
<button type="submit">批量上传</button>
</form>参数声明为数组即注入 UploadedFile[](框架自动将 Swoole 的多文件结构转置为对象数组,源码 Request::parseFiles()):
#[RouteMapping(method: 'POST')]
public function photos(
#[FileRule(fileMime: 'image/png|image/jpeg', maxSize: 5242880, count: 3), InjectFile]
array $photos, // 要求恰好 3 张图片
): array {
$paths = [];
foreach ($photos as $index => $file) {
$paths[] = $this->save($file, "photo_{$index}");
}
return ['paths' => $paths];
}UploadedFile API 参考
Viswoole\HttpServer\Message\UploadedFile 封装单个上传文件:
| 成员 | 类型 | 说明 |
|---|---|---|
$tmp_path | string(readonly 属性) | 临时文件路径 |
getClientFilename() | ?string | 客户端原始文件名 |
getClientMediaType() | ?string | 客户端声明的 MIME 类型(可伪造,仅供展示) |
getSize() | ?int | 文件大小(字节) |
getError() | int | 上传错误码,UPLOAD_ERR_OK(0) 表示正常 |
getRealMimeType() | ?string | 通过 finfo 检测的真实 MIME 类型,临时文件不存在或检测失败为 null |
isMoved() | bool | 文件是否已被移动 |
getStream() | FileStream | 获取只读文件流(延迟初始化),上传出错或已移动时抛异常 |
moveTo(string $targetPath) | void | 移动文件到目标路径(含文件名) |
需要读取文件内容而非落盘时使用 getStream():
$content = $file->getStream()->getContents(); // 读取全部内容FileStream API 参考
Viswoole\HttpServer\Message\FileStream 是 PHP 文件资源流的面向对象封装,析构时自动关闭底层资源:
| 方法 | 返回值 | 说明 |
|---|---|---|
create(string $filePath, string $mode = 'r') | static | 工厂方法,按 fopen 模式打开文件 |
read(int $length) | string | 从当前位置读取指定字节数 |
getContents() | string | 读取当前位置起的全部剩余内容 |
write(string $string) | int | 写入数据(仅可写模式) |
getSize() | ?int | 流大小(字节) |
tell() | int | 当前指针位置 |
seek(int $offset, int $whence = SEEK_SET) | void | 移动指针 |
rewind() | void | 指针回到开头 |
eof() | bool | 是否到达流末尾 |
isReadable() / isWritable() / isSeekable() | bool | 流能力判断 |
getMetadata(?string $key = null) | mixed | 流元数据 |
detach() | resource|null | 分离并返回底层 PHP 资源流 |
close() | void | 关闭资源流 |
完整示例:头像上传接口
<?php
declare(strict_types=1);
namespace App\Controller;
use App\Service\AvatarService;
use Viswoole\HttpServer\AutoInject\InjectFile;
use Viswoole\HttpServer\Contract\ResponseInterface;
use Viswoole\HttpServer\Message\UploadedFile;
use Viswoole\HttpServer\Validate\FileRule;
use Viswoole\Router\Annotation\AutoController;
use Viswoole\Router\Annotation\RouteMapping;
#[AutoController(prefix: 'upload')]
class UploadController
{
public function __construct(private readonly AvatarService $service)
{
}
/**
* 上传头像
*
* POST /upload/avatar,字段名 avatar
*/
#[RouteMapping('avatar', method: 'POST')]
public function avatar(
#[FileRule(
fileMime: 'image/png|image/jpeg|image/webp',
maxSize: 2097152, // 2MB
message: '请上传 2MB 以内的 PNG/JPEG/WebP 图片'
), InjectFile]
UploadedFile $avatar,
ResponseInterface $response,
): ResponseInterface {
// 随机文件名:避免重名冲突,且不信任客户端文件名
$extension = strtolower(
pathinfo($avatar->getClientFilename() ?? '', PATHINFO_EXTENSION)
);
$newName = date('Ymd') . '_' . bin2hex(random_bytes(8)) . '.' . $extension;
// 按日期分目录存储,moveTo 会自动创建目录
$relativePath = 'uploads/avatars/' . date('Ym') . '/' . $newName;
$avatar->moveTo(app()->getRootPath() . '/runtime/' . $relativePath);
// 业务侧仅登记相对路径与文件信息
$this->service->record($relativePath, $avatar->getSize());
return $response->json([
'code' => 0,
'message' => '上传成功',
'data' => ['path' => $relativePath, 'size' => $avatar->getSize()],
]);
}
}用 curl 验证(默认端口 9501):
curl -X POST http://127.0.0.1:9501/upload/avatar \
-F "avatar=@/path/to/photo.jpg"安全与配置建议
- 服务端检测真实类型:白名单校验交给
FileRule(基于内容检测),不要依赖getClientMediaType()或文件扩展名; - 不要信任原始文件名:客户端文件名可能包含
../等字符,仅用于提取扩展名或展示,落盘一律使用随机生成的文件名; - 上传目录禁止执行:存储目录不放 Web 可执行路径,或关闭该目录的脚本解析;
- 限制请求体大小:上传上限受多重配置约束——PHP 的
upload_max_filesize、Swoole Server 的package_max_length、反向代理(如 nginx 的client_max_body_size),需按需统一调整; - 及时处理错误码:
getError() !== UPLOAD_ERR_OK时文件不可用,moveTo()/getStream()会直接抛异常。
下一步
- Response 响应对象:用
sendfile()向客户端发送文件 - 自动注入注解:
#[InjectFile]与其他注入注解的通用行为 - 验证器:编写自定义验证规则
