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

PHP 圖片上傳類封裝與使用範例

     編輯:PHP 相關知識

設計目標

封裝一個可復用的 ImageUploader 類,負責:文件類型/大小驗證、安全重命名、目錄自動創建、移動上傳文件、可選縮略圖生成、錯誤統一拋出異常。採用面向對象風格,便於單元測試與擴展。

核心類實現

<?php
namespace App\Utils;

use Exception;

class ImageUploader
{
    /** @var array 允許的 MIME 類型 */
    protected array $allowedMimes = [
        'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'
    ];
    /** @var int 最大文件大小(位元組),默認 2MB */
    protected int $maxSize = 2 * 1024 * 1024;
    /** @var string 儲存根目錄 */
    protected string $saveRoot;
    /** @var bool 是否生成縮略圖 */
    protected bool $makeThumb = false;
    /** @var int 縮略圖最大寬/高 */
    protected int $thumbMaxDim = 300;

    // 運行時屬性
    protected string $originalName = '';
    protected string $tmpPath = '';
    protected int $fileSize = 0;
    protected string $mimeType = '';
    protected array $imageInfo = []; // getimagesize 結果

    public function __construct(string $saveRoot)
    {
        $this->saveRoot = rtrim($saveRoot, '/\\') . DIRECTORY_SEPARATOR;
    }

    /** 設置允許類型 */
    public function setAllowedMimes(array $mimes): self
    {
        $this->allowedMimes = $mimes;
        return $this;
    }

    /** 設置最大尺寸 */
    public function setMaxSize(int $bytes): self
    {
        $this->maxSize = $bytes;
        return $this;
    }

    /** 啟用縮略圖 */
    public function enableThumb(int $maxDim = 300): self
    {
        $this->makeThumb = true;
        $this->thumbMaxDim = $maxDim;
        return $this;
    }

    /** 處理 $_FILES 項目 */
    public function handleUpload(array $file): string
    {
        $this->extractFileInfo($file);
        $this->validate();
        $savePath = $this->moveFile();
        
        if ($this->makeThumb) {
            $this->createThumbnail($savePath);
        }
        
        return $savePath; // 返回相對或絕對路徑
    }

    /** 從 $_FILES 提取信息 */
    protected function extractFileInfo(array $file): void
    {
        $this->originalName = $file['name'] ?? '';
        $this->tmpPath      = $file['tmp_name'] ?? '';
        $this->fileSize     = $file['size'] ?? 0;
        $this->mimeType     = $file['type'] ?? '';
        
        if ($this->tmpPath && is_uploaded_file($this->tmpPath)) {
            $this->imageInfo = @getimagesize($this->tmpPath) ?: [];
        }
    }

    /** 綜合驗證 */
    protected function validate(): void
    {
        if (!$this->tmpPath || !is_uploaded_file($this->tmpPath)) {
            throw new Exception('非法上傳文件或文件丟失', 1001);
        }
        if ($this->fileSize > $this->maxSize) {
            throw new Exception(sprintf('文件大小超過限制 (%.2f MB)', $this->maxSize / 1024 / 1024), 1002);
        }
        if (!in_array($this->mimeType, $this->allowedMimes, true)) {
            throw new Exception('不支持的文件類型: ' . $this->mimeType, 1003);
        }
        // 二次驗證:檢查文件頭魔數(防偽裝)
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
        $realMime = $finfo->file($this->tmpPath);
        if (!in_array($realMime, $this->allowedMimes, true)) {
            throw new Exception('文件內容類型不匹配: ' . $realMime, 1004);
        }
        if (empty($this->imageInfo)) {
            throw new Exception('無法識別的圖片格式或損壞文件', 1005);
        }
    }

    /** 生成安全儲存路徑並移動文件 */
    protected function moveFile(): string
    {
        // 目錄結構:saveRoot / Y-m / 隨機名.擴展名
        $dateDir = date('Y-m');
        $saveDir = $this->saveRoot . $dateDir;
        if (!is_dir($saveDir)) {
            mkdir($saveDir, 0755, true);
        }
        
        $ext = strtolower(pathinfo($this->originalName, PATHINFO_EXTENSION));
        $ext = $ext ?: 'jpg'; // 兜底
        $filename = bin2hex(random_bytes(16)) . '.' . $ext; // 64位隨機十六進制
        $destPath = $saveDir . DIRECTORY_SEPARATOR . $filename;

        if (!move_uploaded_file($this->tmpPath, $destPath)) {
            throw new Exception('文件移動失敗,請檢查目錄權限', 1006);
        }
        
        // 返回相對於 saveRoot 的路徑,便於資料庫存儲
        return $dateDir . '/' . $filename;
    }

    /** 生成縮略圖(需 GD 擴展) */
    protected function createThumbnail(string $relativePath): void
    {
        if (!extension_loaded('gd')) return;
        
        $srcPath = $this->saveRoot . $relativePath;
        list($w, $h, $type) = $this->imageInfo;
        
        // 計算縮放比例
        $ratio = min($this->thumbMaxDim / $w, $this->thumbMaxDim / $h, 1);
        if ($ratio >= 1) return; // 原圖已小於限制
        
        $newW = (int)($w * $ratio);
        $newH = (int)($h * $ratio);
        
        $src = $this->createImageFromType($srcPath, $type);
        if (!$src) return;
        
        $dst = imagecreatetruecolor($newW, $newH);
        // 保持透明通道
        if ($type === IMAGETYPE_PNG || $type === IMAGETYPE_GIF || $type === IMAGETYPE_WEBP) {
            imagealphablending($dst, false);
            imagesavealpha($dst, true);
        }
        imagecopyresampled($dst, $src, 0, 0, 0, 0, $newW, $newH, $w, $h);
        
        $thumbPath = $this->saveRoot . 'thumb_' . $relativePath;
        $this->saveImageByType($dst, $thumbPath, $type);
        imagedestroy($src);
        imagedestroy($dst);
    }

    protected function createImageFromType(string $path, int $type): ?\GdImage
    {
        return match ($type) {
            IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
            IMAGETYPE_PNG  => @imagecreatefrompng($path),
            IMAGETYPE_GIF  => @imagecreatefromgif($path),
            IMAGETYPE_WEBP => @imagecreatefromwebp($path),
            default        => null,
        };
    }

    protected function saveImageByType(\GdImage $img, string $path, int $type): void
    {
        switch ($type) {
            case IMAGETYPE_JPEG: imagejpeg($img, $path, 85); break;
            case IMAGETYPE_PNG:  imagepng($img, $path, 8); break;
            case IMAGETYPE_GIF:  imagegif($img, $path); break;
            case IMAGETYPE_WEBP: imagewebp($img, $path, 85); break;
        }
    }
}

控制器中使用示例

<?php
namespace App\Controller;

use App\Utils\ImageUploader;
use Exception;

class UploadController
{
    public function upload()
    {
        if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
            $this->json(['code' => 405, 'msg' => 'Method not allowed']);
        }

        if (empty($_FILES['image'])) {
            $this->json(['code' => 400, 'msg' => '未接收到文件']);
        }

        try {
            $uploader = new ImageUploader(__DIR__ . '/../../public/uploads');
            // 可鏈式配置
            $uploader->setMaxSize(5 * 1024 * 1024) // 5MB
                     ->enableThumb(400);           // 生成 400px 縮略圖
            
            $relativePath = $uploader->handleUpload($_FILES['image']);
            
            $this->json([
                'code' => 0,
                'msg'  => '上傳成功',
                'data' => [
                    'url'      => '/uploads/' . $relativePath,
                    'thumbUrl' => '/uploads/thumb_' . $relativePath,
                ]
            ]);
        } catch (Exception $e) {
            $this->json(['code' => $e->getCode() ?: 500, 'msg' => $e->getMessage()]);
        }
    }

    protected function json(array $data): void
    {
        header('Content-Type: application/json; charset=utf-8');
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        exit;
    }
}

HTML 表單

<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="image" accept="image/*" required>
    <button type="submit">上傳圖片</button>
</form>

關鍵安全與最佳實踐總結

  1. 雙重類型驗證:$_FILES['type'](瀏覽器提供,不可信)+ finfo 魔數檢測(真實內容)。
  2. 隨機文件名:bin2hex(random_bytes(16)) 避免覆蓋、路徑遍歷、中文文件名編碼問題。
  3. 日期分目錄:避免單目錄文件過多導致 inode 耗盡、ls 卡頓。
  4. 權限隔離:上傳目錄設置 php_admin_flag engine off (Apache) 或 location ~* \.php$ { deny all; } (Nginx) 禁止腳本執行。
  5. 異常而非錯誤碼:拋出帶代碼的異常,上層統一捕獲轉 JSON,邏輯清晰。
  6. 縮略圖異步化:大流量場景下,縮略圖生成應推入隊列由 Worker 處理,HTTP 請求僅返回原圖 URL。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved