專案中處理檔案上傳若散落在各控制器,難以維護且易留下安全漏洞。本文提供一個結構清晰、參數可配置的 Files::upload() 靜態類,涵蓋副檔名白名單、大小限制、目錄自動建立(按日期分層)、隨機重命名、權限設定、資料庫路徑回傳等完整流程,適合直接納入專案共用庫。
<?php
class Files {
/**
* 檔案上傳統一入口
* @param string $basePath 儲存根目錄絕對路徑(如 /var/www/uploads)
* @param array $file $_FILES['fieldName'] 陣列
* @param string $type 類型鍵名:image, flash, media, file(對應下方 $extArr)
* @return array ['error'=>msg] 或 ['file_url'=>url, 'file_size'=>size, 'db_path'=>relativePath]
*/
public static function upload($basePath, $file, $type = 'image') {
$msg = [];
// 1. 基礎路徑正規化
$savePath = rtrim(realpath($basePath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$saveUrl = rtrim($basePath, '/') . '/'; // 假設 Web 可存取路徑同根目錄
// 2. 允許的副檔名對照表
$extArr = [
'image' => ['gif','jpg','jpeg','png','bmp','webp'],
'flash' => ['swf','flv'],
'media' => ['swf','flv','mp3','wav','wma','wmv','mid','avi','mpg','asf','rm','rmvb','mp4'],
'file' => ['doc','docx','xls','xlsx','ppt','htm','html','txt','zip','rar','gz','bz2','pdf'],
];
// 3. 最大檔案大小(位元組,預設 1MB)
$maxSize = 1000000; // 可改為配置參數
// 4. 基礎驗證
if (empty($file) || $file['error'] !== UPLOAD_ERR_OK) {
$msg['error'] = '請選擇有效檔案。';
return $msg;
}
if (!is_dir($savePath) || !is_writable($savePath)) {
$msg['error'] = '上傳目錄不存在或不可寫入。';
return $msg;
}
if (!isset($extArr[$type])) {
$msg['error'] = "不支援的類型:$type";
return $msg;
}
if ($file['size'] > $maxSize) {
$msg['error'] = '檔案大小超過限制(' . self::formatSize($maxSize) . ')。';
return $msg;
}
// 5. 副檔名驗證
$origName = $file['name'];
$ext = strtolower(pathinfo($origName, PATHINFO_EXTENSION));
if (!in_array($ext, $extArr[$type])) {
$msg['error'] = '不允許的副檔名。僅支援:' . implode(', ', $extArr[$type]);
return $msg;
}
// 6. 建立日期分層目錄:/type/Y/m/d/
$dateDir = $type . '/' . date('Y/m/d/');
$fullDir = $savePath . $dateDir;
$fullUrl = $saveUrl . $dateDir;
if (!is_dir($fullDir)) {
mkdir($fullDir, 0755, true);
}
// 7. 產生唯一檔名:YmdHis_隨機數.副檔名
$newName = date('YmdHis') . '_' . mt_rand(10000, 99999) . '.' . $ext;
$destPath = $fullDir . $newName;
$destUrl = $fullUrl . $newName;
$dbPath = $dateDir . $newName; // 存入資料庫的相對路徑
// 8. 移動檔案
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
$msg['error'] = '檔案移動失敗(權限或磁碟空間不足)。';
return $msg;
}
// 9. 設定權限並回傳結果
@chmod($destPath, 0644);
$msg['file_url'] = $destUrl;
$msg['file_size'] = $file['size'];
$msg['db_path'] = $dbPath;
return $msg;
}
private static function formatSize($bytes) {
$units = ['B','KB','MB','GB','TB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units)-1) {
$bytes /= 1024; $i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
}
<?php
// 接收前端
$result = Files::upload('/var/www/html/uploads', $_FILES['avatar'], 'image');
if (isset($result['error'])) {
// 失敗處理
echo json_encode(['success' => false, 'msg' => $result['error']]);
} else {
// 成功:儲存 $result['db_path'] 到資料庫,前端用 $result['file_url'] 顯示
echo json_encode([
'success' => true,
'url' => $result['file_url'],
'path' => $result['db_path'],
'size' => $result['file_size'],
]);
}
| 特性 | 說明 |
|---|---|
| 類型白名單 | 依業務區分 image/flash/media/file,防止上傳可執行腳本 |
| 日期分層目錄 | 避免單一目錄檔案過多導致 inode 耗盡、列目錄緩慢 |
| 隨機重命名 | 防止檔名衝突、路徑遍歷、覆蓋敏感檔案 |
| 不依賴 php.ini | 大小、副檔名、路徑全在類別內配置,部署更彈性 |
| 回傳結構統一 | 成功含 url/size/db_path,失敗含 error,前後端介面穩定 |
php_flag engine off (Apache) 或 location ~ \.php$ { deny all; } (Nginx) 禁止腳本執行。getimagesize() 或 imagecreatefromstring() 二次驗證真偽。league/flysystem 抽象儲存層,支援 S3、雲端儲存無縫遷移。