類特性概覽
- 統一介面支援單文件 (
ONE) 與多文件 (ANY) 上傳
- 文件類型白名單、大小限制
- 按年月自動創建上傳目錄
- 隨機文件名防衝突 (
TTR_時間戳_隨機字符.擴展名)
- 鏈式調用風格,錯誤信息常量化管理
- 析構函數自動清理屬性
完整類代碼 upload.class.php
<?php
/**
* TTRUpload - 經典多文件上傳類
* 使用示例見文末
*/
class TTRUpload {
const MAX_SIZE = 81200000; // 80MB
private string $uploadPath = 'uploadfile/';
private string $savePath = '';
private ?string $uploadedNames = null; // 單文件為字符串,多文件用 | 分隔
private array $allowedExt = ['jpg', 'gif', 'png', 'jpeg', 'bmp', 'webp'];
private ?string $error = null;
private array $files = [];
private string $mode = 'ONE'; // ONE | ANY
public function __construct(array $files, string $mode = 'ONE') {
if (!in_array($mode, ['ONE', 'ANY'], true)) {
throw new InvalidArgumentException('模式必須為 ONE 或 ANY');
}
$this->mode = $mode;
$this->files = $files;
}
/** 設置上傳根目錄 */
public function setUploadPath(string $path): self {
$this->uploadPath = rtrim($path, '/') . '/';
return $this;
}
/** 執行上傳 */
public function upload(): bool {
if (!$this->prepareSavePath()) {
return false;
}
if ($this->mode === 'ONE') {
return $this->uploadSingle($this->files);
} else {
return $this->uploadMultiple($this->files);
}
}
/** 獲取上傳後的文件名(多文件以 | 分隔) */
public function getUploadFileName(): ?string {
return $this->uploadedNames;
}
/** 獲取最後錯誤信息 */
public function getError(): ?string {
return $this->error;
}
/** 準備保存目錄 */
private function prepareSavePath(): bool {
$base = rtrim($this->uploadPath, '/');
$this->savePath = $base . '/' . date('Ym') . '/';
if (!is_dir($this->savePath)) {
if (!mkdir($this->savePath, 0755, true)) {
$this->error = '無法創建上傳目錄:' . $this->savePath;
return false;
}
}
return true;
}
/** 單文件上傳 */
private function uploadSingle(array $file): bool {
if (!$this->validateFile($file)) return false;
$newName = $this->generateName($file['name']);
$dest = $this->savePath . $newName;
if (move_uploaded_file($file['tmp_name'], $dest)) {
$this->uploadedNames = $newName;
return true;
}
$this->error = '文件移動失敗';
return false;
}
/** 多文件上傳 */
private function uploadMultiple(array $files): bool {
$names = [];
foreach ($files as $file) {
// 跳過空文件欄位
if ($file['error'] === UPLOAD_ERR_NO_FILE) continue;
if (!$this->validateFile($file)) return false;
$newName = $this->generateName($file['name']);
$dest = $this->savePath . $newName;
if (!move_uploaded_file($file['tmp_name'], $dest)) {
$this->error = '文件移動失敗:' . $file['name'];
return false;
}
$names[] = $newName;
}
$this->uploadedNames = implode('|', $names);
return true;
}
/** 文件驗證 */
private function validateFile(array $file): bool {
if ($file['error'] !== UPLOAD_ERR_OK) {
$this->error = $this->getUploadErrorMsg($file['error']);
return false;
}
$ext = strtolower($this->getExtension($file['name']));
if (!in_array($ext, $this->allowedExt, true)) {
$this->error = "不允許的文件類型:{$ext}";
return false;
}
if ($file['size'] > self::MAX_SIZE) {
$this->error = '文件大小超過限制 (80MB)';
return false;
}
return true;
}
/** 生成唯一文件名 */
private function generateName(string $originalName): string {
$ext = $this->getExtension($originalName);
$rand = $this->randomString(4);
return 'TTR_' . time() . $rand . '.' . $ext;
}
/** 獲取擴展名 */
private function getExtension(string $filename): string {
return strtolower(pathinfo($filename, PATHINFO_EXTENSION));
}
/** 隨機字符串 */
private function randomString(int $len): string {
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$str = '';
for ($i = 0; $i < $len; $i++) {
$str .= $chars[random_int(0, strlen($chars) - 1)];
}
return $str;
}
/** 上傳錯誤代碼轉中文 */
private function getUploadErrorMsg(int $code): string {
return match ($code) {
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 擴展阻止',
default => '未知上傳錯誤',
};
}
public function __destruct() {
$this->uploadedNames = null;
$this->files = [];
$this->savePath = '';
}
}
?>
前端表單 index.html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>多文件上傳演示</title>
</head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="file1"><br>
<input type="file" name="file2"><br>
<input type="file" name="file3"><br>
<input type="file" name="file4"><br>
<button type="submit" name="button">上傳</button>
</form>
</body>
</html>
處理頁面 upload.php
<?php
require_once 'upload.class.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['button'])) {
// 整理文件數組
$files = [
$_FILES['file1'],
$_FILES['file2'],
$_FILES['file3'],
$_FILES['file4'],
];
$upload = new TTRUpload($files, 'ANY');
// $upload->setUploadPath('custom/path/'); // 可選自定義路徑
if ($upload->upload()) {
echo "上傳成功!文件名:{$upload->getUploadFileName()}";
} else {
echo "上傳失敗:{$upload->getError()}";
}
}
?>
單文件上傳用法
$upload = new TTRUpload($_FILES['file1'], 'ONE');
if ($upload->upload()) {
echo "單文件上傳成功:{$upload->getUploadFileName()}";
}
安全加固建議
- 二次驗證圖片:上傳後調用
getimagesize() 或 imagetypes() 確認真實圖片格式。
- 禁用腳本執行:上傳目錄配置 Nginx
location ~ \.php$ { deny all; } 或 Apache SetHandler default-handler。
- 文件名安全:生成名稱不使用用戶原始文件名任何部分,防止路徑遍歷。
- 病毒掃描:生產環境接入 ClamAV 或雲端安全 API 掃描上傳文件。
- 限流:結合 Redis 限制用戶上傳頻率(如每分鐘 10 次)。