Skip to content

$_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_filesizepost_max_size

基础概念

$_FILES 数组结构

$_FILES 是一个二维数组。第一维的键名是 HTML 表单中文件输入控件的 name 属性值,第二维包含五个固定的键:

php
<?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,                 // 文件大小(字节)
    ],
];

五个字段的详细说明

字段类型说明安全性
namestring客户端文件原始名称不可信,可能被伪造
typestring文件 MIME 类型不可信,由浏览器发送
tmp_namestring临时文件的完整路径可信(PHP 引擎生成)
errorint上传状态错误码可信(PHP 引擎设置)
sizeint文件字节数可信(PHP 引擎检测)

安全警告

$_FILES['userfile']['name']$_FILES['userfile']['type'] 来自客户端 HTTP 请求,可以被伪造。永远不要依赖这两个字段做安全判断。应使用 finfo() 函数检测真实的 MIME 类型。

上传错误码常量

常量说明
UPLOAD_ERR_OK0上传成功,没有错误
UPLOAD_ERR_INI_SIZE1上传文件超过了 php.iniupload_max_filesize 的限制
UPLOAD_ERR_FORM_SIZE2上传文件超过了 HTML 表单中 MAX_FILE_SIZE 的限制
UPLOAD_ERR_PARTIAL3文件只有部分被上传
UPLOAD_ERR_NO_FILE4没有文件被上传
UPLOAD_ERR_NO_TMP_DIR6找不到临时目录(PHP 5.0.3+)
UPLOAD_ERR_CANT_WRITE7文件写入磁盘失败(PHP 5.1+)
UPLOAD_ERR_EXTENSION8PHP 扩展阻止了文件上传(PHP 5.2+)

语法与代码

基本文件上传处理

php
<?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
<?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
<?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 配置直接影响文件上传行为:

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 = 30

post_max_size 必须大于 upload_max_filesize

post_max_size 必须大于 upload_max_filesize,因为 POST 请求体不仅包含文件数据,还包含其他表单字段。如果 post_max_size 太小,$_POST$_FILES 都将为空,且 $_SERVER['CONTENT_LENGTH'] 会超过限制。

多文件上传的 HTML 表单

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">

php
$_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>

php
$_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
<?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
<?php
declare(strict_types=1);

$originalName = $_FILES['upload']['name']; // 可能是 "../../../etc/passwd"

// 不安全:直接使用原始文件名
// $dest = $uploadDir . $originalName;

// 安全:生成新的随机文件名,单独存储原始文件名
$safeName = bin2hex(random_bytes(16)) . '.' . pathinfo($originalName, PATHINFO_EXTENSION);

最佳实践

  1. 始终检查 error 字段:不要假设上传一定成功
  2. 使用 finfo 检测 MIME 类型:不信任客户端的 type 字段
  3. 生成新文件名:不使用客户端原始文件名存储
  4. 限制文件大小:在 PHP 配置和应用层都做限制
  5. 验证扩展名 + MIME 类型:双重验证防止绕过
  6. 上传目录不可执行:将上传目录配置为禁止 PHP 执行
  7. 存储在 Web 根目录外:或将上传目录的访问权限严格限制
  8. 使用 is_uploaded_file() 验证:确保文件是合法的上传文件
  9. 记录上传日志:记录上传操作以便审计
php
<?php
declare(strict_types=1);

// 上传目录安全配置(Nginx 示例)
// location /uploads/ {
//     location ~ \.php$ {
//         deny all;  // 禁止执行 PHP 文件
//     }
// }

下一节

下一节将详细介绍 $_REQUEST 超全局变量,了解它如何合并 GET/POST/Cookie 数据,以及为什么不推荐使用它。

参考链接