Symfony在FrankenPHP下上传失败主因是upload_tmp_dir未显式配置、move()权限不足及upload_max_filesize被Go层截断;需在framework.yaml设tmp_dir、校验目标目录可写、Caddy中配upload_limit并生成安全文件名。

Symfony 上传文件在 FrankenPHP 下失败,大概率是 upload_tmp_dir 未显式配置或与 FrankenPHP 的进程模型冲突 —— 它不读取系统级 php.ini 的临时目录设置,也不自动继承 Nginx/FPM 的权限上下文。
FrankenPHP 不继承 php.ini 的 upload_tmp_dir
FrankenPHP 启动时默认使用 Go 运行时的临时路径(如 /tmp),但不会解析或加载传统 PHP-FPM 所依赖的 php.ini 中的 upload_tmp_dir 配置。即使你在 php.ini 里写了 upload_tmp_dir = /var/tmp/php-uploads,FrankenPHP 也完全忽略它。
后果是:上传大文件时,$_FILES 数组为空,$request->files->get('file') 返回 null,且无明确错误提示;小文件可能侥幸成功,但一旦并发或磁盘满就随机失败。
解决方式只有显式覆盖:
立即学习“PHP免费学习笔记(深入)”;
- 在
config/packages/framework.yaml中强制指定 Symfony 的上传临时路径:framework: # ... 其他配置 php_errors: log: true # ? 关键:告诉 Symfony 使用哪个临时目录(FrankenPHP 会尊重此值) uploads: tmp_dir: '%kernel.project_dir%/var/tmp/uploads' - 确保该路径存在且可写:
mkdir -p var/tmp/uploads && chmod 755 var/tmp/uploads - 不要复用
sys_get_temp_dir()或ini_get('upload_tmp_dir')—— 在 FrankenPHP 中它们返回不可靠值
$file->move() 在 FrankenPHP 下容易因权限中断
FrankenPHP 是单进程常驻模型,worker 模式下 PHP 实例长期存活,但它的 UID/GID 默认是启动用户(比如 root 或 www-data),而 UploadedFile::move() 调用的是底层 move_uploaded_file(),后者依赖 PHP 进程对目标目录的写权限 —— 和传统 FPM 不同,它不经过 Unix socket 权限协商,也不受 php-fpm.conf 的 user/group 隔离保护。
常见现象:"The file could not be uploaded" 异常,堆栈指向 move(),但 $file->getError() 却是 UPLOAD_ERR_OK —— 说明上传已进临时区,只是移动失败。
必须做三件事:
- 检查目标目录是否真实可写:
is_dir($targetDir) && is_writable($targetDir),不能只信mkdir(..., 0755, true)—— FrankenPHP 下父目录权限缺失时,mkdir可能静默失败 - 避免用
Filesystem::mkdir()自动递归创建,它不校验父级权限;改用原生mkdir($dir, 0755, true)+ 显式chmod补位 - 禁止把
$file->getRealPath()当源路径去copy()—— 该路径指向 FrankenPHP 内部管理的临时区,请求结束后立即失效,且路径格式可能含非标准前缀(如/tmp/frankenphp-xxxx)
大文件上传时 upload_max_filesize 截断发生在 FrankenPHP 解析层
当上传超限文件(比如设了 upload_max_filesize = 2M,却传了 5M ZIP),传统 FPM 会在 PHP 解析阶段丢弃整个 $_FILES 并返回 UPLOAD_ERR_INI_SIZE;而 FrankenPHP 的 Go 层在 HTTP body 解析时就做了截断,导致 $request->files->get('file') 直接为 null,连 $file->getError() 都调不到。
验证方法:打印 $request->getContent() 长度,若明显小于原始文件大小,说明已在 FrankenPHP 层被截断。
解决方案只能从 FrankenPHP 入口控制:
- 在
Caddyfile中显式限制上传大小:php_server { upload_limit 10MB } - 同时在
php.ini里保持同步(虽不生效,但避免混淆):upload_max_filesize = 10M、post_max_size = 12M - 前端必须配合校验:Dropzone 或
fetch上传前读取file.size,否则用户永远收不到“文件太大”的提示
安全文件名生成必须绕过 FrankenPHP 的临时路径污染
FrankenPHP 的临时文件名不是标准 phpXXXXXX 格式,而是类似 frankenphp_abc123,且 $file->getClientOriginalName() 在某些版本中会混入路径头(尤其当客户端用 WebKit 内核伪造路径时)。直接拼接会导致 move() 写入任意目录。
最稳妥的做法是彻底丢弃原始名:
- 用
pathinfo($file->getClientOriginalName(), PATHINFO_EXTENSION)提取扩展名,再比对 MIME 白名单(如image/jpeg→.jpg) - 生成纯哈希文件名:
sprintf('%s.%s', bin2hex(random_bytes(10)), $safeExt) - 绝对禁止用
basename()或正则替换清理原始名 —— 空字节、../、Unicode 零宽字符都可能绕过 - 如果必须保留原始语义(如用户上传报告),应存入数据库字段,而非文件系统路径
FrankenPHP 的常驻模型放大了所有路径和权限问题的隐蔽性 —— 错误不报在 Symfony 层,也不报在 PHP 层,而是在 Go 和 C 交界处静默失败。调试时别盯着 $form->isValid(),先确认 $request->files->get('file') 是否为 UploadedFile 实例,再查 tmp_dir 是否真实可写、是否被 Caddy 的 upload_limit 截断。



















