批量檔案上傳是後台管理系統、內容發布平台的常見需求。本文提供一套結構清晰、可直接落地的 PHP 批量上傳方案,包含前端表單、後端處理邏輯與可複用的上傳類別,支援多檔案同時上傳、類型/大小驗證、隨機命名與錯誤彙總。
upload/
├── index.html # 上傳表單頁面
├── upload.php # 接收處理入口
└── UploadFile.class.php # 核心上傳類別
<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<title>批量檔案上傳示範</title>
<style>
body { font-family: "Microsoft JhengHei", sans-serif; padding: 20px; }
.file-group { margin: 8px 0; }
.btn { padding: 8px 20px; margin-right: 10px; cursor: pointer; }
.msg { margin-top: 15px; line-height: 1.8; }
.success { color: #28a745; } .error { color: #dc3545; }
</style>
<script>
// 移除未選檔案的 input,避免空值提交
function cleanEmptyInputs() {
for (let i = 1; i <= 8; i++) {
const el = document.getElementById('file' + i);
if (el && !el.value) el.name = 'ignore';
}
}
</script>
</head>
<body>
<h2>批量圖片上傳 (最多 8 張)</h2>
<form name="upfile" action="upload.php" method="post"
enctype="multipart/form-data" onsubmit="return cleanEmptyInputs()">
<div class="file-group"><input type="file" name="spic[]" id="file1" accept="image/*" /></div>
<div class="file-group"><input type="file" name="spic[]" id="file2" accept="image/*" /></div>
<div class="file-group"><input type="file" name="spic[]" id="file3" accept="image/*" /></div>
<div class="file-group"><input type="file" name="spic[]" id="file4" accept="image/*" /></div>
<div class="file-group"><input type="file" name="spic[]" id="file5" accept="image/*" /></div>
<div class="file-group"><input type="file" name="spic[]" id="file6" accept="image/*" /></div>
<div class="file-group"><input type="file" name="spic[]" id="file7" accept="image/*" /></div>
<div class="file-group"><input type="file" name="spic[]" id="file8" accept="image/*" /></div>
<button type="submit" name="sub" class="btn">開始上傳</button>
<button type="reset" class="btn">重新選擇</button>
</form>
<div class="msg" id="result"></div>
</body>
</html>
<?php
/**
* 通用檔案上傳類別
* 支援單/多檔案、類型驗證、大小限制、隨機命名、多級目錄自動建立
*/
class UploadFile
{
// ----- 配置屬性 -----
private $maxSize = 1048576; // 1 MB
private $allowTypes = ['jpg','png','gif','jpeg','bmp','webp'];
private $randomName = true; // 是否啟用隨機檔名
private $uploadPath = ''; // 上傳根目錄
private $subDir = ''; // 子目錄 (可選)
// ----- 運行時屬性 -----
private $originNames = [];
private $tmpNames = [];
private $newNames = [];
private $sizes = [];
private $types = [];
private $errorCode = 0;
private $errorMsgs = [];
/**
* 建構子:接受關聯陣列覆蓋預設配置
* @param array $options ['maxsize'=>,'allowtype'=>,'israndfile'=>,'filepath'=>]
*/
public function __construct(array $options = [])
{
foreach ($options as $key => $val) {
$key = strtolower($key);
if (property_exists($this, $key)) {
$this->$key = $val;
}
}
// 確保路徑以 DIRECTORY_SEPARATOR 結尾
$this->uploadPath = rtrim($this->uploadPath, '/\\') . DIRECTORY_SEPARATOR;
}
// ----- 公開介面 -----
/**
* 執行上傳
* @param string $formName 表單 name 屬性 (對應 $_FILES 鍵名)
* @return bool 成功至少一個檔案回傳 true
*/
public function upload(string $formName): bool
{
$this->resetState();
if (!$this->validateUpload($formName)) {
return false;
}
$files = $this->normalizeFilesArray($formName);
$allOk = true;
// 第一輪:驗證所有檔案
foreach ($files as $idx => $file) {
if (!$this->setFileInfo($file, $idx)) {
$allOk = false;
continue;
}
if (!$this->checkSize() || !$this->checkType()) {
$this->errorMsgs[] = $this->getErrorMsg();
$allOk = false;
}
}
// 第二輪:實際移動檔案 (僅當全部驗證通過)
if ($allOk) {
foreach ($files as $idx => $file) {
$this->setFileInfo($file, $idx); // 重置屬性
if (!$this->moveFile()) {
$this->errorMsgs[] = $this->getErrorMsg();
$allOk = false;
} else {
$this->newNames[] = $this->newNames[$idx] ?? '';
}
}
}
return $allOk;
}
/** 取得成功上傳的新檔名陣列 */
public function getNewNames(): array
{
return $this->newNames;
}
/** 取得錯誤訊息陣列 */
public function getErrors(): array
{
return $this->errorMsgs;
}
// ----- 內部邏輯 -----
private function resetState(): void
{
$this->originNames = $this->tmpNames = $this->newNames = $this->sizes = $this->types = [];
$this->errorCode = 0;
$this->errorMsgs = [];
}
private function validateUpload(string $formName): bool
{
if (empty($_FILES[$formName]) || !is_array($_FILES[$formName]['name'])) {
$this->errorCode = 4;
$this->errorMsgs[] = $this->getErrorMsg();
return false;
}
return $this->checkDir($this->uploadPath);
}
/** 將 $_FILES 正規化為索引陣列 */
private function normalizeFilesArray(string $formName): array
{
$src = $_FILES[$formName];
$count = count($src['name']);
$files = [];
for ($i = 0; $i < $count; $i++) {
$files[] = [
'name' => $src['name'][$i],
'tmp_name' => $src['tmp_name'][$i],
'size' => $src['size'][$i],
'error' => $src['error'][$i],
];
}
return $files;
}
private function setFileInfo(array $file, int $idx): bool
{
if (!$this->checkDir($this->uploadPath)) return false;
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->errorCode = $file['error'];
return false;
}
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
$this->originNames[$idx] = $file['name'];
$this->tmpNames[$idx] = $file['tmp_name'];
$this->sizes[$idx] = $file['size'];
$this->types[$idx] = $ext;
return true;
}
private function checkDir(string $path): bool
{
if (empty($path)) {
$this->errorCode = -5; return false;
}
if (!is_dir($path) && !@mkdir($path, 0755, true)) {
$this->errorCode = -4; return false;
}
if (!is_writable($path)) {
$this->errorCode = -4; return false;
}
return true;
}
private function checkSize(): bool
{
$idx = count($this->sizes) - 1;
if ($this->sizes[$idx] > $this->maxSize) {
$this->errorCode = -2; return false;
}
return true;
}
private function checkType(): bool
{
$idx = count($this->types) - 1;
if (!in_array($this->types[$idx], $this->allowTypes)) {
$this->errorCode = -1; return false;
}
return true;
}
private function moveFile(): bool
{
$idx = count($this->tmpNames) - 1;
$ext = $this->types[$idx];
// 產生目標檔名
if ($this->randomName) {
$newName = date('YmdHis') . '_' . mt_rand(1000, 9999) . '.' . $ext;
} else {
$newName = $this->originNames[$idx];
}
$dest = $this->uploadPath . $newName;
if (@move_uploaded_file($this->tmpNames[$idx], $dest)) {
$this->newNames[$idx] = $newName;
return true;
}
$this->errorCode = -3;
return false;
}
private function getErrorMsg(): string
{
$prefix = "檔案「{$this->originNames[count($this->originNames)-1] ?? '未知'}」上傳失敗:";
return $prefix . match ($this->errorCode) {
UPLOAD_ERR_INI_SIZE => '超過 php.ini upload_max_filesize 限制',
UPLOAD_ERR_FORM_SIZE => '超過表單 MAX_FILE_SIZE 限制',
UPLOAD_ERR_PARTIAL => '檔案僅部分上傳',
UPLOAD_ERR_NO_FILE => '沒有檔案被上傳',
UPLOAD_ERR_NO_TMP_DIR => '找不到臨時目錄',
UPLOAD_ERR_CANT_WRITE => '寫入磁碟失敗',
UPLOAD_ERR_EXTENSION => '被 PHP 擴充套件阻擋',
-1 => '不允許的檔案類型 (僅限: ' . implode(', ', $this->allowTypes) . ')',
-2 => "檔案過大,不可超過 {$this->maxSize} 位元組",
-3 => '移動檔案失敗,請檢查目錄權限',
-4 => '建立/寫入上傳目錄失敗',
-5 => '未指定上傳路徑',
default => '未知錯誤 (代碼: ' . $this->errorCode . ')',
};
}
}
<?php
require_once 'UploadFile.class.php';
header('Content-Type: text/html; charset=utf-8');
// 實例化:自訂儲存路徑、允許類型、最大 2MB、啟用隨機命名
$uploader = new UploadFile([
'filepath' => __DIR__ . '/uploads',
'allowtype' => ['jpg','png','gif','jpeg','webp'],
'maxsize' => 2 * 1024 * 1024,
'israndfile' => true,
]);
if ($uploader->upload('spic')) {
echo '✅ 所有檔案上傳成功!';
foreach ($uploader->getNewNames() as $name) {
echo "📄 {$name}";
}
} else {
echo '❌ 上傳遇到錯誤:';
foreach ($uploader->getErrors() as $err) {
echo "{$err}";
}
}
Intervention Image 或 GD 在 moveFile() 後處理。fetch 分片上傳,後端支援斷點續傳。moveFile() 替換為 S3/OSS SDK 上傳邏輯,介面保持不變。本方案以類別封裝核心邏輯,前後端分離明確,支援批次驗證與原子性移動(全成功或全失敗),代碼結構清晰易於維護擴充。實際部署時請依專案需求調整允許類型、大小限制與儲存策略,並務必在 Web 伺服器層面禁止上傳目錄執行腳本。