上传多个文件
概述
PHP 支持通过 HTML 表单同时上传多个文件。在 HTML 中使用 name="files[]" 数组语法或 name="files[...]" 命名语法,PHP 会在 $_FILES 超全局数组中以特定结构组织这些文件。处理多文件上传需要特别注意数组的重组和批量处理逻辑。
适用场景
- 图片批量上传(相册)
- 文档批量导入
- 附件批量处理
- 画廊管理系统
基础概念
HTML 表单语法
html
<!-- 方式一:数组语法 name="files[]" -->
<input type="file" name="files[]" multiple>
<input type="file" name="files[]" multiple accept="image/*">
<!-- 方式二:命名语法 name="files[avatar]" -->
<input type="file" name="files[avatar]">
<input type="file" name="files[cover]">
<!-- 方式三:多次 input(不使用 multiple) -->
<input type="file" name="file1">
<input type="file" name="file2">
<input type="file" name="file3">$_FILES 数组结构对比
php
<?php
// 方式一:name="files[]" —— 多维数组结构
$_FILES['files'] = [
'name' => ['photo1.jpg', 'photo2.png', 'photo3.gif'],
'type' => ['image/jpeg', 'image/png', 'image/gif'],
'tmp_name' => ['/tmp/phpA', '/tmp/phpB', '/tmp/phpC'],
'error' => [0, 0, 0],
'size' => [12345, 67890, 11111],
];
// 方式二:name="files[avatar]" —— 嵌套结构
$_FILES['files'] = [
'name' => ['avatar' => 'face.jpg', 'cover' => 'banner.png'],
'type' => ['avatar' => 'image/jpeg', 'cover' => 'image/png'],
'tmp_name' => ['avatar' => '/tmp/phpA', 'cover' => '/tmp/phpB'],
'error' => ['avatar' => 0, 'cover' => 0],
'size' => ['avatar' => 12345, 'cover' => 67890],
];数组结构
name="files[]" 生成的 $_FILES 结构是按字段分组的(每个字段是一个包含所有文件对应值的数组),而不是按文件分组。需要手动重组为数组。
语法与代码示例
重组 $_FILES 数组
php
<?php
/**
* 将 $_FILES 多维数组重组为按文件索引排列的数组
*/
function reorganizeFiles(array $files): array
{
$result = [];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
$result[$i] = [
'name' => $files['name'][$i] ?? '',
'type' => $files['type'][$i] ?? '',
'tmp_name' => $files['tmp_name'][$i] ?? '',
'error' => $files['error'][$i] ?? UPLOAD_ERR_NO_FILE,
'size' => $files['size'][$i] ?? 0,
];
}
return $result;
}
// 使用
$files = reorganizeFiles($_FILES['files']);
foreach ($files as $index => $file) {
echo "文件 {$index}: {$file['name']} ({$file['size']} bytes)\n";
}基本多文件上传处理
php
<?php
declare(strict_types=1);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit('Method Not Allowed');
}
$uploadDir = '/var/www/html/uploads/' . date('Y/m/d');
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$files = reorganizeFiles($_FILES['files']);
$successCount = 0;
$errors = [];
foreach ($files as $index => $file) {
// 跳过空上传
if ($file['error'] === UPLOAD_ERR_NO_FILE) {
continue;
}
// 检查错误
if ($file['error'] !== UPLOAD_ERR_OK) {
$errors[] = "文件 {$index} ({$file['name']}): 上传错误代码 {$file['error']}";
continue;
}
// 验证大小
if ($file['size'] > 5 * 1024 * 1024) {
$errors[] = "文件 {$index} ({$file['name']}): 超过 5MB 限制";
continue;
}
// 验证 MIME
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mime, $allowed, true)) {
$errors[] = "文件 {$index} ({$file['name']}): 不支持的类型 {$mime}";
continue;
}
// 生成文件名并移动
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newName = date('His') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$destPath = $uploadDir . DIRECTORY_SEPARATOR . $newName;
if (move_uploaded_file($file['tmp_name'], $destPath)) {
$successCount++;
echo "上传成功: {$newName}\n";
} else {
$errors[] = "文件 {$index} ({$file['name']}): 保存失败";
}
}
echo "\n总计成功: {$successCount} 个\n";
if (!empty($errors)) {
echo "错误:\n" . implode("\n", $errors) . "\n";
}命名方式的多文件上传
php
<?php
// HTML: <input name="files[avatar]"> <input name="files[cover]">
// $_FILES['files']['name'] = ['avatar' => 'face.jpg', 'cover' => 'banner.png']
function reorganizeNamedFiles(array $files): array
{
$result = [];
foreach (array_keys($files['name']) as $key) {
$result[$key] = [
'name' => $files['name'][$key],
'type' => $files['type'][$key],
'tmp_name' => $files['tmp_name'][$key],
'error' => $files['error'][$key],
'size' => $files['size'][$key],
];
}
return $result;
}
// 使用
$files = reorganizeNamedFiles($_FILES['files']);
$avatar = $files['avatar'] ?? null;
$cover = $files['cover'] ?? null;实战示例
完整的多文件上传类
php
<?php
declare(strict_types=1);
class MultiFileUploader
{
private string $uploadDir;
private int $maxFileSize;
private array $allowedMimes;
private int $maxFiles;
public function __construct(
string $uploadDir,
int $maxFileSize = 10485760,
array $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
int $maxFiles = 20
) {
$this->uploadDir = $uploadDir;
$this->maxFileSize = $maxFileSize;
$this->allowedMimes = $allowedMimes;
$this->maxFiles = $maxFiles;
}
/**
* 批量处理上传
*/
public function upload(array $filesField): UploadResult
{
$result = new UploadResult();
$files = $this->reorganize($filesField);
if (count($files) > $this->maxFiles) {
$result->addError('超过最大文件数量限制: ' . $this->maxFiles);
return $result;
}
foreach ($files as $index => $file) {
$this->processOne($file, $index, $result);
}
return $result;
}
private function processOne(array $file, int $index, UploadResult $result): void
{
// 跳过空文件
if ($file['error'] === UPLOAD_ERR_NO_FILE) {
return;
}
// 检查错误
if ($file['error'] !== UPLOAD_ERR_OK) {
$result->addError("文件 {$index}: " . $this->errorMessage($file['error']));
return;
}
// 验证大小
if ($file['size'] > $this->maxFileSize) {
$result->addError("文件 {$index} ({$file['name']}): 超过大小限制");
return;
}
// 验证 MIME
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!in_array($mime, $this->allowedMimes, true)) {
$result->addError("文件 {$index} ({$file['name']}): 不允许的类型");
return;
}
// 生成目标路径
$ext = $this->extensionFromMime($mime);
$dateDir = $this->uploadDir . DIRECTORY_SEPARATOR . date('Y/m/d');
if (!is_dir($dateDir)) {
mkdir($dateDir, 0755, true);
}
$filename = date('Ymd_His') . '_' . bin2hex(random_bytes(8)) . '.' . $ext;
$destPath = $dateDir . DIRECTORY_SEPARATOR . $filename;
if (move_uploaded_file($file['tmp_name'], $destPath)) {
chmod($destPath, 0644);
$result->addSuccess($filename, $destPath, $file['size'], $mime);
} else {
$result->addError("文件 {$index}: 保存失败");
}
}
private function reorganize(array $files): array
{
$result = [];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
$result[$i] = [
'name' => $files['name'][$i] ?? '',
'tmp_name' => $files['tmp_name'][$i] ?? '',
'error' => $files['error'][$i] ?? UPLOAD_ERR_NO_FILE,
'size' => $files['size'][$i] ?? 0,
];
}
return $result;
}
private function extensionFromMime(string $mime): string
{
$map = [
'image/jpeg' => 'jpg', 'image/png' => 'png',
'image/gif' => 'gif', 'image/webp' => 'webp',
'application/pdf' => 'pdf',
];
return $map[$mime] ?? 'bin';
}
private function errorMessage(int $code): string
{
$map = [
UPLOAD_ERR_INI_SIZE => '超过服务器限制',
UPLOAD_ERR_FORM_SIZE => '超过表单限制',
UPLOAD_ERR_PARTIAL => '上传不完整',
UPLOAD_ERR_NO_TMP_DIR => '临时目录不存在',
UPLOAD_ERR_CANT_WRITE => '写入失败',
UPLOAD_ERR_EXTENSION => '被扩展阻止',
];
return $map[$code] ?? '未知错误';
}
}
class UploadResult
{
public array $successFiles = [];
public array $errors = [];
public function addSuccess(string $name, string $path, int $size, string $mime): void
{
$this->successFiles[] = [
'name' => $name, 'path' => $path,
'size' => $size, 'mime' => $mime,
];
}
public function addError(string $message): void
{
$this->errors[] = $message;
}
public function isSuccess(): bool
{
return empty($this->errors) && !empty($this->successFiles);
}
public function toArray(): array
{
return [
'success' => $this->successFiles,
'errors' => $this->errors,
'count' => count($this->successFiles),
];
}
}
// 使用示例
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['photos'])) {
$uploader = new MultiFileUploader(
'/var/www/html/uploads',
maxFileSize: 5 * 1024 * 1024,
allowedMimes: ['image/jpeg', 'image/png', 'image/webp']
);
$result = $uploader->upload($_FILES['photos']);
header('Content-Type: application/json');
echo json_encode($result->toArray(), JSON_UNESCAPED_UNICODE);
}拖拽上传 API 端点
php
<?php
declare(strict_types=1);
// 处理通过 JavaScript FormData 上传的多个文件
header('Content-Type: application/json; charset=utf-8');
$uploadDir = '/var/www/html/uploads';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method Not Allowed']);
exit;
}
if (empty($_FILES)) {
http_response_code(400);
echo json_encode(['error' => '没有文件上传']);
exit;
}
// 找到上传文件字段名
$fieldNames = array_keys($_FILES);
$uploadedFiles = [];
foreach ($fieldNames as $fieldName) {
$filesField = $_FILES[$fieldName];
// 单文件
if (!is_array($filesField['name'])) {
$filesField = [
'name' => [$filesField['name']],
'tmp_name' => [$filesField['tmp_name']],
'error' => [$filesField['error']],
'size' => [$filesField['size']],
];
}
$count = count($filesField['name']);
for ($i = 0; $i < $count; $i++) {
if ($filesField['error'][$i] !== UPLOAD_ERR_OK) {
continue;
}
$ext = pathinfo($filesField['name'][$i], PATHINFO_EXTENSION);
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$destPath = $uploadDir . DIRECTORY_SEPARATOR . $newName;
if (move_uploaded_file($filesField['tmp_name'][$i], $destPath)) {
$uploadedFiles[] = [
'name' => $newName,
'original' => $filesField['name'][$i],
'size' => $filesField['size'][$i],
];
}
}
}
echo json_encode([
'success' => true,
'files' => $uploadedFiles,
'count' => count($uploadedFiles),
], JSON_UNESCAPED_UNICODE);注意事项
max_file_uploads 限制
php
<?php
// php.ini 中的 max_file_uploads 控制单次请求最多上传的文件数
// 默认值 20,超出部分会被静默忽略
echo '最大上传文件数: ' . ini_get('max_file_uploads') . PHP_EOL;
// 检查实际收到的文件数
$field = $_FILES['photos'] ?? [];
$actualCount = is_array($field['name']) ? count($field['name']) : 1;
$maxAllowed = (int)ini_get('max_file_uploads');
if ($actualCount > $maxAllowed) {
trigger_error("上传文件数({$actualCount})超过限制({$maxAllowed})", E_USER_WARNING);
}内存限制
php
<?php
// 大量文件上传会占用大量临时内存
// 每个上传文件都会占用一份临时空间
// 注意 post_max_size 和 memory_limit 的配合
function estimateUploadMemory(array $files): int
{
$totalSize = 0;
$field = $_FILES[array_key_first($files)] ?? [];
if (is_array($field['size'])) {
$totalSize = array_sum($field['size']);
} else {
$totalSize = $field['size'] ?? 0;
}
return $totalSize;
}最佳实践
1. 限制总上传大小
php
<?php
// 不仅限制单个文件大小,还要限制总大小
function checkTotalUploadSize(array $files, int $maxTotal): bool
{
$totalSize = 0;
foreach ($files as $field) {
if (is_array($field['size'])) {
$totalSize += array_sum($field['size']);
} else {
$totalSize += $field['size'] ?? 0;
}
}
return $totalSize <= $maxTotal;
}2. 异步批量处理
php
<?php
// 对于大量文件,先保存再异步处理
// 1. 上传阶段:快速保存所有文件
// 2. 处理阶段:后台处理(缩略图、压缩等)
class AsyncUploadProcessor
{
public function saveAll(array $files, string $stagingDir): array
{
$saved = [];
$files = $this->reorganize($files);
foreach ($files as $file) {
if ($file['error'] !== UPLOAD_ERR_OK) continue;
$dest = $stagingDir . DIRECTORY_SEPARATOR . bin2hex(random_bytes(16));
if (move_uploaded_file($file['tmp_name'], $dest)) {
$saved[] = ['path' => $dest, 'original' => $file['name']];
}
}
return $saved;
}
private function reorganize(array $files): array
{
$result = [];
$count = count($files['name']);
for ($i = 0; $i < $count; $i++) {
$result[] = [
'name' => $files['name'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i],
];
}
return $result;
}
}