$_FILES — 文件上传信息
概述
$_FILES 是 PHP 中用于处理 HTTP 文件上传的超全局变量。当 HTML 表单以 enctype="multipart/form-data" 方式提交时,PHP 会将上传文件的信息存储在 $_FILES 数组中。$_FILES 包含了上传文件的原始名称、临时存储路径、文件类型、大小以及上传状态等信息。
前置知识
在阅读本节之前,你需要了解:
- HTML 表单的
enctype属性和文件上传控件(<input type="file">) - HTTP
multipart/form-data编码方式 - PHP 的文件系统函数(
move_uploaded_file()、is_uploaded_file()) - PHP 配置指令(
upload_max_filesize、post_max_size)
基础概念
$_FILES 数组结构
$_FILES 是一个二维数组。第一维的键名是 HTML 表单中文件输入控件的 name 属性值,第二维包含五个固定的键:
<?php
declare(strict_types=1);
// HTML: <input type="file" name="avatar">
// 上传文件后,$_FILES 的结构如下:
$_FILES = [
'avatar' => [
'name' => 'profile-photo.jpg', // 客户端原始文件名
'type' => 'image/jpeg', // MIME 类型(由浏览器提供,不可信)
'tmp_name' => '/tmp/phpXYZ1234', // 服务器上的临时文件路径
'error' => UPLOAD_ERR_OK, // 上传错误码
'size' => 245678, // 文件大小(字节)
],
];五个字段的详细说明
| 字段 | 类型 | 说明 | 安全性 |
|---|---|---|---|
name | string | 客户端文件原始名称 | 不可信,可能被伪造 |
type | string | 文件 MIME 类型 | 不可信,由浏览器发送 |
tmp_name | string | 临时文件的完整路径 | 可信(PHP 引擎生成) |
error | int | 上传状态错误码 | 可信(PHP 引擎设置) |
size | int | 文件字节数 | 可信(PHP 引擎检测) |
安全警告
$_FILES['userfile']['name'] 和 $_FILES['userfile']['type'] 来自客户端 HTTP 请求,可以被伪造。永远不要依赖这两个字段做安全判断。应使用 finfo() 函数检测真实的 MIME 类型。
上传错误码常量
| 常量 | 值 | 说明 |
|---|---|---|
UPLOAD_ERR_OK | 0 | 上传成功,没有错误 |
UPLOAD_ERR_INI_SIZE | 1 | 上传文件超过了 php.ini 中 upload_max_filesize 的限制 |
UPLOAD_ERR_FORM_SIZE | 2 | 上传文件超过了 HTML 表单中 MAX_FILE_SIZE 的限制 |
UPLOAD_ERR_PARTIAL | 3 | 文件只有部分被上传 |
UPLOAD_ERR_NO_FILE | 4 | 没有文件被上传 |
UPLOAD_ERR_NO_TMP_DIR | 6 | 找不到临时目录(PHP 5.0.3+) |
UPLOAD_ERR_CANT_WRITE | 7 | 文件写入磁盘失败(PHP 5.1+) |
UPLOAD_ERR_EXTENSION | 8 | PHP 扩展阻止了文件上传(PHP 5.2+) |
语法与代码
基本文件上传处理
<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['document'])) {
$file = $_FILES['document'];
// 检查上传是否成功
if ($file['error'] !== UPLOAD_ERR_OK) {
$errorMessages = [
UPLOAD_ERR_INI_SIZE => '文件超过服务器限制',
UPLOAD_ERR_FORM_SIZE => '文件超过表单限制',
UPLOAD_ERR_PARTIAL => '文件只上传了部分',
UPLOAD_ERR_NO_FILE => '没有选择文件',
UPLOAD_ERR_NO_TMP_DIR => '服务器临时目录缺失',
UPLOAD_ERR_CANT_WRITE => '磁盘写入失败',
UPLOAD_ERR_EXTENSION => '上传被扩展阻止',
];
$message = $errorMessages[$file['error']] ?? '未知上传错误';
die("上传失败:{$message}");
}
// 获取文件信息
$originalName = $file['name'];
$tempPath = $file['tmp_name'];
$fileSize = $file['size'];
echo "原始文件名: {$originalName}";
echo "临时路径: {$tempPath}";
echo "文件大小: {$fileSize} 字节";
}安全的文件上传处理
<?php
declare(strict_types=1);
/**
* 安全的文件上传处理类
*/
class FileUploader
{
private readonly string $uploadDir;
private readonly int $maxSize;
private readonly array $allowedMimes;
private readonly array $allowedExtensions;
public function __construct(
string $uploadDir = '/var/www/uploads/',
int $maxSize = 5 * 1024 * 1024, // 5MB
array $allowedMimes = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
],
array $allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'],
) {
$this->uploadDir = rtrim($uploadDir, '/') . '/';
$this->maxSize = $maxSize;
$this->allowedMimes = $allowedMimes;
$this->allowedExtensions = $allowedExtensions;
// 确保上传目录存在
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function upload(string $inputName): array
{
if (!isset($_FILES[$inputName])) {
return ['success' => false, 'error' => '未找到上传文件'];
}
$file = $_FILES[$inputName];
// 1. 检查上传错误
if ($file['error'] !== UPLOAD_ERR_OK) {
return $this->getUploadError($file['error']);
}
// 2. 检查文件大小
if ($file['size'] > $this->maxSize) {
$maxMb = round($this->maxSize / 1024 / 1024, 1);
return ['success' => false, 'error' => "文件超过 {$maxMb}MB 限制"];
}
// 3. 使用 finfo 检测真实 MIME 类型(不信任客户端 type 字段)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$detectedMime = $finfo->file($file['tmp_name']);
if (!in_array($detectedMime, $this->allowedMimes, true)) {
return ['success' => false, 'error' => "不支持的文件类型: {$detectedMime}"];
}
// 4. 检查文件扩展名
$extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, $this->allowedExtensions, true)) {
return ['success' => false, 'error' => "不支持的扩展名: .{$extension}"];
}
// 5. 验证是否为合法上传文件
if (!is_uploaded_file($file['tmp_name'])) {
return ['success' => false, 'error' => '非法上传文件'];
}
// 6. 生成安全的新文件名
$newName = $this->generateSafeName($extension);
$destination = $this->uploadDir . $newName;
// 7. 移动文件到目标目录
if (!move_uploaded_file($file['tmp_name'], $destination)) {
return ['success' => false, 'error' => '文件保存失败'];
}
return [
'success' => true,
'originalName'=> $file['name'],
'savedName' => $newName,
'savedPath' => $destination,
'size' => $file['size'],
'mime' => $detectedMime,
];
}
private function generateSafeName(string $extension): string
{
return date('Ymd') . '_' . bin2hex(random_bytes(8)) . '.' . $extension;
}
private function getUploadError(int $errorCode): array
{
$messages = [
UPLOAD_ERR_INI_SIZE => '文件超过服务器配置的大小限制',
UPLOAD_ERR_FORM_SIZE => '文件超过表单指定的大小限制',
UPLOAD_ERR_PARTIAL => '文件上传不完整',
UPLOAD_ERR_NO_FILE => '未选择上传文件',
UPLOAD_ERR_NO_TMP_DIR => '服务器缺少临时目录',
UPLOAD_ERR_CANT_WRITE => '文件写入磁盘失败',
UPLOAD_ERR_EXTENSION => '文件上传被 PHP 扩展阻止',
];
return [
'success' => false,
'error' => $messages[$errorCode] ?? '未知上传错误',
];
}
}
// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$uploader = new FileUploader(
uploadDir: __DIR__ . '/uploads/',
maxSize: 10 * 1024 * 1024, // 10MB
);
$result = $uploader->upload('avatar');
print_r($result);
}多文件上传处理
<?php
declare(strict_types=1);
/**
* 处理多文件上传
* HTML:
* <input type="file" name="gallery[]" multiple>
*/
class MultiFileUploader
{
public function handleMultiple(string $inputName, string $uploadDir): array
{
if (!isset($_FILES[$inputName])) {
return ['success' => false, 'error' => '未找到上传文件'];
}
$files = $_FILES[$inputName];
// 单文件上传时,多文件数组结构不同
if (!is_array($files['name'])) {
$files = [
'name' => [$files['name']],
'type' => [$files['type']],
'tmp_name' => [$files['tmp_name']],
'error' => [$files['error']],
'size' => [$files['size']],
];
}
$results = [];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
// 检查上传错误
if ($files['error'][$i] !== UPLOAD_ERR_OK) {
$results[] = [
'success' => false,
'name' => $files['name'][$i],
'error' => "上传错误码: {$files['error'][$i]}",
];
continue;
}
$tmpName = $files['tmp_name'][$i];
$origName = $files['name'][$i];
$fileSize = $files['size'][$i];
// MIME 类型检测
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmpName);
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
$newName = date('Ymd') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$dest = rtrim($uploadDir, '/') . '/' . $newName;
if (move_uploaded_file($tmpName, $dest)) {
$results[] = [
'success' => true,
'originalName'=> $origName,
'savedName' => $newName,
'size' => $fileSize,
'mime' => $mime,
];
} else {
$results[] = [
'success' => false,
'name' => $origName,
'error' => '文件保存失败',
];
}
}
return $results;
}
}
// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$uploader = new MultiFileUploader();
$results = $uploader->handleMultiple('gallery', __DIR__ . '/uploads/');
foreach ($results as $result) {
echo $result['success']
? "成功: {$result['savedName']}"
: "失败: {$result['name']} - {$result['error']}";
echo PHP_EOL;
}
}详细说明
PHP 文件上传相关配置
以下 php.ini 配置直接影响文件上传行为:
; 是否启用 HTTP 文件上传
file_uploads = On
; 最大上传文件大小(每个文件)
upload_max_filesize = 2M
; POST 请求体最大大小(所有文件 + 表单数据的总和)
post_max_size = 8M
; 最大输入变量数量
max_input_vars = 1000
; 临时上传目录(默认系统临时目录)
upload_tmp_dir = /tmp
; 最大上传文件数量
max_file_uploads = 20
; 内存限制(影响大文件上传)
memory_limit = 128M
; max_execution_time = 30post_max_size 必须大于 upload_max_filesize
post_max_size 必须大于 upload_max_filesize,因为 POST 请求体不仅包含文件数据,还包含其他表单字段。如果 post_max_size 太小,$_POST 和 $_FILES 都将为空,且 $_SERVER['CONTENT_LENGTH'] 会超过限制。
多文件上传的 HTML 表单
<!-- 方式一:使用 name[] 数组语法 -->
<form method="post" enctype="multipart/form-data">
<input type="file" name="photos[]" multiple accept="image/*">
<button type="submit">上传</button>
</form>
<!-- 方式二:多个独立的文件输入 -->
<form method="post" enctype="multipart/form-data">
<input type="file" name="avatar">
<input type="file" name="cover">
<input type="file" name="resume">
<button type="submit">上传</button>
</form>
<!-- 方式三:带 MAX_FILE_SIZE 限制 -->
<form method="post" enctype="multipart/form-data">
<!-- MAX_FILE_SIZE 必须在文件输入之前 -->
<input type="hidden" name="MAX_FILE_SIZE" value="1048576">
<input type="file" name="document">
<button type="submit">上传</button>
</form>$_FILES 多文件数组结构对比
单文件 <input type="file" name="doc">:
$_FILES['doc']['name'] = 'file.txt'
$_FILES['doc']['type'] = 'text/plain'
$_FILES['doc']['tmp_name'] = '/tmp/phpXXX'
$_FILES['doc']['error'] = 0
$_FILES['doc']['size'] = 1234多文件 <input type="file" name="docs[]" multiple>:
$_FILES['docs']['name'][0] = 'file1.txt'
$_FILES['docs']['name'][1] = 'file2.txt'
$_FILES['docs']['type'][0] = 'text/plain'
$_FILES['docs']['type'][1] = 'image/jpeg'
$_FILES['docs']['tmp_name'][0] = '/tmp/phpAAA'
$_FILES['docs']['tmp_name'][1] = '/tmp/phpBBB'
$_FILES['docs']['error'][0] = 0
$_FILES['docs']['error'][1] = 0
$_FILES['docs']['size'][0] = 1234
$_FILES['docs']['size'][1] = 5678注意事项
1. 临时文件的生命周期
上传的临时文件在脚本结束时会自动删除。如果未调用 move_uploaded_file() 或 copy(),文件将丢失。大文件上传时需注意 max_execution_time 的限制。
2. MIME 类型不可信
$_FILES['file']['type'] 来自客户端的 Content-Type 头,可以被伪造:
<?php
declare(strict_types=1);
// 不安全:使用客户端提供的 MIME 类型
$mime = $_FILES['upload']['type']; // 可被伪造
// 安全:使用 finfo 检测真实 MIME 类型
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['upload']['tmp_name']); // 基于文件内容检测3. 文件名安全问题
客户端提供的文件名可能包含特殊字符、路径遍历序列(../)、空字节等:
<?php
declare(strict_types=1);
$originalName = $_FILES['upload']['name']; // 可能是 "../../../etc/passwd"
// 不安全:直接使用原始文件名
// $dest = $uploadDir . $originalName;
// 安全:生成新的随机文件名,单独存储原始文件名
$safeName = bin2hex(random_bytes(16)) . '.' . pathinfo($originalName, PATHINFO_EXTENSION);最佳实践
- 始终检查 error 字段:不要假设上传一定成功
- 使用 finfo 检测 MIME 类型:不信任客户端的 type 字段
- 生成新文件名:不使用客户端原始文件名存储
- 限制文件大小:在 PHP 配置和应用层都做限制
- 验证扩展名 + MIME 类型:双重验证防止绕过
- 上传目录不可执行:将上传目录配置为禁止 PHP 执行
- 存储在 Web 根目录外:或将上传目录的访问权限严格限制
- 使用
is_uploaded_file()验证:确保文件是合法的上传文件 - 记录上传日志:记录上传操作以便审计
<?php
declare(strict_types=1);
// 上传目录安全配置(Nginx 示例)
// location /uploads/ {
// location ~ \.php$ {
// deny all; // 禁止执行 PHP 文件
// }
// }下一节
下一节将详细介绍 $_REQUEST 超全局变量,了解它如何合并 GET/POST/Cookie 数据,以及为什么不推荐使用它。