首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 圖片上傳類完整實現:驗證、縮略、儲存、預覽一站式解決

     編輯:PHP 相關知識

一個健壯、可復用的 PHP 圖片上傳類,涵蓋檔案類型/大小驗證、目錄自動建立、隨機命名防衝突、GD 庫生成縮略圖、上傳後預覽等核心功能。採用物件導向設計,支援鏈式調用,易於整合至任何框架或原生專案。

類別功能清單

  • 多圖支援:單次調用可處理多個 $_FILES 入口。
  • 安全驗證:MIME 白名單、副檔名檢查、檔案大小限制、is_uploaded_file() 來源驗證。
  • 智能命名:時間戳 + 隨機字串 + 原始副檔名,避免覆蓋與注入。
  • 自動縮略:基於 GD 庫,支援等比縮放、裁剪填充、品質控制。
  • 靈活配置:儲存路徑、允許類型、最大尺寸、縮略參數皆可實例化時注入。
  • 錯誤處理:拋出異常/返回結構化錯誤碼,便於前端提示與日誌記錄。

核心代碼

<?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()]);

HTML 表單

<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),類別依賴注入儲存器,支援雲儲存。
  • 異步處理:大檔/高併發時,將檔案先存暫存區,推送訊息至隊列,Worker 異步生成縮略圖、掃毒、入庫。
  • 圖片優化管線:整合 imagemin、mozjpeg、oxipng 等工具,進一步壓縮體積。
  • 資料庫記錄:上傳成功後寫入 attachments 表(檔名、路徑、MIME、尺寸、關聯業務 ID、上傳者 IP/UID)。
  • 單元測試:使用 phpunit + vfsStream 模擬檔案系統,覆蓋各種錯誤分支與縮略模式。

總結

這個 ImageUploader 類別將圖片上傳的安全驗證、隨機命名、縮略生成、錯誤處理封裝為單一職責元件,代碼結構清晰、配置靈活、易於測試與擴展。直接引入專案即可用於後台管理、用戶頭像、商品圖片、文章插圖等各類上傳場景,是 PHP 開發者工具箱中的利器。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved