一個健壯、可復用的 PHP 圖片上傳類,涵蓋檔案類型/大小驗證、目錄自動建立、隨機命名防衝突、GD 庫生成縮略圖、上傳後預覽等核心功能。採用物件導向設計,支援鏈式調用,易於整合至任何框架或原生專案。
$_FILES 入口。is_uploaded_file() 來源驗證。<?php
namespace App\Upload;
class ImageUploader
{
// 配置屬性(可透過建構子注入覆蓋)
protected $uploadDir = 'uploads/images/';
protected $allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
protected $allowedExts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
protected $maxSize = 5 * 1024 * 1024; // 5MB
protected $thumbEnabled = true;
protected $thumbPrefix = 'thumb_';
protected $thumbMaxWidth = 800;
protected $thumbMaxHeight = 600;
protected $thumbQuality = 85;
protected $thumbMode = 'fit'; // fit|crop|fill
// 運行時狀態
protected $files = [];
protected $results = [];
public function __construct(array $config = [])
{
foreach ($config as $key => $val) {
if (property_exists($this, $key)) {
$this->$key = $val;
}
}
// 確保上傳目錄存在
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
/** 處理單個檔案上傳 */
public function upload($fileInputName)
{
if (!isset($_FILES[$fileInputName])) {
throw new \InvalidArgumentException("檔案輸入欄位 '$fileInputName' 不存在");
}
$file = $_FILES[$fileInputName];
// 支援多檔陣列結構
$files = $this->normalizeFilesArray($file);
foreach ($files as $f) {
$this->processSingleFile($f);
}
return $this;
}
/** 批量上傳 */
public function uploadMultiple(array $fileInputNames)
{
foreach ($fileInputNames as $name) {
$this->upload($name);
}
return $this;
}
/** 獲取結果 */
public function getResults()
{
return $this->results;
}
/** 獲取成功上傳的檔案資訊 */
public function getSuccessFiles()
{
return array_filter($this->results, fn($r) => $r['success']);
}
/** 獲取錯誤資訊 */
public function getErrors()
{
return array_filter($this->results, fn($r) => !$r['success']);
}
// ===== 內部處理邏輯 =====
protected function normalizeFilesArray($file)
{
// 處理 產生的多維陣列
if (isset($file['name']) && is_array($file['name'])) {
$count = count($file['name']);
$normalized = [];
for ($i = 0; $i < $count; $i++) {
$normalized[] = [
'name' => $file['name'][$i],
'type' => $file['type'][$i],
'tmp_name' => $file['tmp_name'][$i],
'error' => $file['error'][$i],
'size' => $file['size'][$i],
];
}
return $normalized;
}
return [$file]; // 單檔包裝為陣列
}
protected function processSingleFile(array $file)
{
$result = [
'original_name' => $file['name'],
'success' => false,
'error' => '',
'path' => '',
'thumb_path' => '',
'size' => $file['size'],
'mime' => '',
'width' => 0,
'height' => 0,
];
try {
// 1. 檢查上傳錯誤碼
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new \RuntimeException($this->getUploadErrorMessage($file['error']));
}
// 2. 驗證來源安全性
if (!is_uploaded_file($file['tmp_name'])) {
throw new \RuntimeException('非法檔案來源');
}
// 3. 檢測真實 MIME (finfo)
$finfo = new \finfo(FILEINFO_MIME_TYPE);
$realMime = $finfo->file($file['tmp_name']);
if (!in_array($realMime, $this->allowedMimes, true)) {
throw new \RuntimeException("檔案類型不允許: $realMime");
}
$result['mime'] = $realMime;
// 4. 檢查副檔名
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $this->allowedExts, true)) {
throw new \RuntimeException("副檔名不允許: $ext");
}
// 5. 檢查大小
if ($file['size'] > $this->maxSize) {
throw new \RuntimeException("檔案過大: {$file['size']} bytes, 最大 {$this->maxSize}");
}
// 6. 獲取圖片尺寸
$imgInfo = getimagesize($file['tmp_name']);
if ($imgInfo === false) {
throw new \RuntimeException('無法識別圖片格式');
}
$result['width'] = $imgInfo[0];
$result['height'] = $imgInfo[1];
// 7. 生成安全檔名
$safeName = $this->generateSafeName($ext);
$destPath = $this->uploadDir . $safeName;
// 8. 移動檔案
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
throw new \RuntimeException('檔案移動失敗');
}
@chmod($destPath, 0644);
$result['path'] = $destPath;
// 9. 生成縮略圖
if ($this->thumbEnabled) {
$thumbPath = $this->createThumbnail($destPath, $safeName);
if ($thumbPath) {
$result['thumb_path'] = $thumbPath;
}
}
$result['success'] = true;
} catch (\Throwable $e) {
$result['error'] = $e->getMessage();
// 若檔案已移動但後續失敗,嘗試清理
if ($result['path'] && file_exists($result['path'])) {
@unlink($result['path']);
}
}
$this->results[] = $result;
}
protected function generateSafeName(string $ext): string
{
return sprintf(
'%s_%s.%s',
date('YmdHis'),
bin2hex(random_bytes(4)),
$ext
);
}
protected function createThumbnail(string $srcPath, string $filename): ?string
{
$thumbName = $this->thumbPrefix . $filename;
$thumbPath = $this->uploadDir . $thumbName;
list($srcW, $srcH, $type) = getimagesize($srcPath);
$srcImg = $this->createImageFromType($srcPath, $type);
if (!$srcImg) return null;
// 計算目標尺寸
[$dstW, $dstH] = $this->calculateThumbSize($srcW, $srcH);
$dstImg = imagecreatetruecolor($dstW, $dstH);
// 保持透明度
if (in_array($type, [IMAGETYPE_GIF, IMAGETYPE_PNG])) {
imagealphablending($dstImg, false);
imagesavealpha($dstImg, true);
$transparent = imagecolorallocatealpha($dstImg, 255, 255, 255, 127);
imagefilledrectangle($dstImg, 0, 0, $dstW, $dstH, $transparent);
}
// 縮放/裁剪邏輯
switch ($this->thumbMode) {
case 'crop': // 中心裁剪填滿
$scale = max($dstW / $srcW, $dstH / $srcH);
$newW = $srcW * $scale;
$newH = $srcH * $scale;
$srcX = ($newW - $dstW) / 2;
$srcY = ($newH - $dstH) / 2;
imagecopyresampled($dstImg, $srcImg, 0, 0, $srcX, $srcY, $dstW, $dstH, $newW, $newH);
break;
case 'fill': // 等比縮放並填充背景色(白)
$scale = min($dstW / $srcW, $dstH / $srcH);
$newW = $srcW * $scale;
$newH = $srcH * $scale;
$dstX = ($dstW - $newW) / 2;
$dstY = ($dstH - $newH) / 2;
imagefilledrectangle($dstImg, 0, 0, $dstW, $dstH, imagecolorallocate($dstImg, 255, 255, 255));
imagecopyresampled($dstImg, $srcImg, $dstX, $dstY, 0, 0, $newW, $newH, $srcW, $srcH);
break;
case 'fit':
default: // 等比縮放至框內
$scale = min($dstW / $srcW, $dstH / $srcH);
$newW = (int)($srcW * $scale);
$newH = (int)($srcH * $scale);
$dstImg = imagescale($srcImg, $newW, $newH, IMG_BICUBIC);
break;
}
// 輸出
$success = false;
switch ($type) {
case IMAGETYPE_JPEG:
$success = imagejpeg($dstImg, $thumbPath, $this->thumbQuality);
break;
case IMAGETYPE_PNG:
$success = imagepng($dstImg, $thumbPath, 9);
break;
case IMAGETYPE_GIF:
$success = imagegif($dstImg, $thumbPath);
break;
case IMAGETYPE_WEBP:
$success = imagewebp($dstImg, $thumbPath, $this->thumbQuality);
break;
}
imagedestroy($srcImg);
if ($dstImg !== $srcImg) imagedestroy($dstImg);
return $success ? $thumbPath : null;
}
protected function calculateThumbSize(int $srcW, int $srcH): array
{
if ($this->thumbMode === 'fit') {
return [$this->thumbMaxWidth, $this->thumbMaxHeight];
}
// crop/fill 模式輸出固定尺寸
return [$this->thumbMaxWidth, $this->thumbMaxHeight];
}
protected function createImageFromType(string $path, int $type)
{
switch ($type) {
case IMAGETYPE_JPEG: return imagecreatefromjpeg($path);
case IMAGETYPE_PNG: return imagecreatefrompng($path);
case IMAGETYPE_GIF: return imagecreatefromgif($path);
case IMAGETYPE_WEBP: return imagecreatefromwebp($path);
default: return null;
}
}
protected function getUploadErrorMessage(int $code): string
{
$messages = [
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 擴展阻止',
];
return $messages[$code] ?? "未知上傳錯誤碼: $code";
}
}
<?php
require_once 'vendor/autoload.php'; // 或 require 'ImageUploader.php';
use App\Upload\ImageUploader;
$uploader = new ImageUploader([
'uploadDir' => __DIR__ . '/public/uploads/',
'maxSize' => 10 * 1024 * 1024, // 10MB
'thumbMaxWidth' => 400,
'thumbMaxHeight' => 300,
'thumbMode' => 'crop',
]);
// 處理上傳
$uploader->upload('photo'); // 對應
// 獲取結果
$results = $uploader->getResults();
foreach ($results as $r) {
if ($r['success']) {
echo "上傳成功: {$r['path']}
";
if ($r['thumb_path']) {
echo "縮略圖: <img src='{$r['thumb_path']}' />
";
}
} else {
echo "失敗: {$r['error']}
";
}
}
// JSON API 回應
header('Content-Type: application/json');
echo json_encode(['code' => 200, 'data' => $uploader->getSuccessFiles()]);
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="photo" accept="image/*" required>
<button type="submit">上傳</button>
</form>
<!-- 多檔上傳 -->
<input type="file" name="photos[]" multiple accept="image/*">
StorageAdapterInterface(Local、S3、OSS、FTP),類別依賴注入儲存器,支援雲儲存。imagemin、mozjpeg、oxipng 等工具,進一步壓縮體積。attachments 表(檔名、路徑、MIME、尺寸、關聯業務 ID、上傳者 IP/UID)。phpunit + vfsStream 模擬檔案系統,覆蓋各種錯誤分支與縮略模式。這個 ImageUploader 類別將圖片上傳的安全驗證、隨機命名、縮略生成、錯誤處理封裝為單一職責元件,代碼結構清晰、配置靈活、易於測試與擴展。直接引入專案即可用於後台管理、用戶頭像、商品圖片、文章插圖等各類上傳場景,是 PHP 開發者工具箱中的利器。