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

PHP 圖片上傳類完整實作:驗證、縮圖、水印、安全強化與 Intervention Image 現代化遷移

     編輯:PHP 相關知識

原始類別架構回顧

原文 `UpImages` 類別整合:上傳驗證、儲存、縮圖、水印。屬性公開、錯誤直接 `die()`、字體路徑寫死、不支援 WebP/HEIC、無單元測試介面。

現代化重構要點 (PSR-12、嚴格型別、依賴注入、介面隔離)

<?php
declare(strict_types=1);

namespace App\Service;

use Intervention\Image\ImageManagerStatic as Image;

interface ImageUploaderInterface {
    public function upload(UploadedFile $file): UploadedImageResult;
}

class UploadedImageResult {
    public function __construct(
        public string $originalName,
        public string $storedPath,      // 相對儲存路徑
        public string $thumbnailPath,   // 縮圖路徑
        public string $watermarkedPath, // 水印圖路徑
        public int $width,
        public int $height,
        public string $mimeType,
        public int $sizeBytes,
    ) {}
}

class InterventionImageUploader implements ImageUploaderInterface {
    public function __construct(
        private string $storageRoot,      // /var/www/storage
        private string $publicPrefix,     // /storage
        private array $allowedMimes = ['image/jpeg','image/png','image/gif','image/webp'],
        private int $maxSizeBytes = 10 * 1024 * 1024,
        private int $thumbMaxW = 400,
        private int $thumbMaxH = 300,
        private ?string $watermarkText = '© MySite',
        private ?string $fontPath = null, // 內建字體或 TTF 路徑
    ) {}

    public function upload(UploadedFile $file): UploadedImageResult {
        // 1. 驗證
        $this->validate($file);

        // 2. 產生安全檔名
        $ext = $file->getClientOriginalExtension() ?: 'jpg';
        $filename = bin2hex(random_bytes(16)) . '.' . $ext;
        $dateDir = date('Y/m/d');
        $storeDir = $this->storageRoot . '/' . $dateDir;
        if (!is_dir($storeDir)) mkdir($storeDir, 0750, true);

        $originalPath = $storeDir . '/' . $filename;
        $file->move($storeDir, $filename); // Symfony/Laravel UploadedFile::move()

        // 3. 讀取圖片資訊
        $img = Image::make($originalPath);
        $width = $img->width();
        $height = $img->height();
        $mime = $img->mime();

        // 4. 產生縮圖
        $thumbFilename = 'thumb_' . $filename;
        $thumbPath = $storeDir . '/' . $thumbFilename;
        $img->fit($this->thumbMaxW, $this->thumbMaxH, function ($c) { $c->upsize(); });
        $img->save($thumbPath, 85);

        // 5. 產生水印圖 (可選)
        $wmPath = '';
        if ($this->watermarkText) {
            $wmFilename = 'wm_' . $filename;
            $wmPath = $storeDir . '/' . $wmFilename;
            $img2 = Image::make($originalPath);
            $img2->text($this->watermarkText, $img2->width() - 120, $img2->height() - 20, function ($font) {
                $font->size(14)->color('#ffffff')->align('right')->valign('bottom');
                if ($this->fontPath) $font->file($this->fontPath);
            });
            $img2->save($wmPath, 85);
        }

        // 6. 回傳結果 (儲存相對路徑,對外輸出時加上 $publicPrefix)
        return new UploadedImageResult(
            originalName: $file->getClientOriginalName(),
            storedPath: $dateDir . '/' . $filename,
            thumbnailPath: $dateDir . '/' . $thumbFilename,
            watermarkedPath: $wmPath ? $dateDir . '/' . $wmFilename : '',
            width: $width,
            height: $height,
            mimeType: $mime,
            sizeBytes: $file->getSize(),
        );
    }

    private function validate(UploadedFile $file): void {
        if (!$file->isValid()) throw new \InvalidArgumentException('上傳檔案無效: ' . $file->getErrorMessage());
        if ($file->getSize() > $this->maxSizeBytes) throw new \InvalidArgumentException('檔案過大');
        if (!in_array($file->getMimeType(), $this->allowedMimes, true)) throw new \InvalidArgumentException('檔案類型不允許');
        // 進階:finode 檢測真實 MIME
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $realMime = $finfo->file($file->getRealPath());
        if (!in_array($realMime, $this->allowedMimes, true)) throw new \InvalidArgumentException('檔案內容類型不符');
    }
}
?>

控制器使用範例 (Laravel 風格)

<?php
class ImageController {
    public function store(Request $request, ImageUploaderInterface $uploader) {
        $request->validate(['image' => 'required|file']);
        try {
            $result = $uploader->upload($request->file('image'));
            return response()->json([
                'url' => $result->storedPath,
                'thumb' => $result->thumbnailPath,
                'watermark' => $result->watermarkedPath,
                'width' => $result->width,
                'height' => $result->height,
            ]);
        } catch (\InvalidArgumentException $e) {
            return response()->json(['error' => $e->getMessage()], 422);
        }
    }
}
?>

安全強化清單 (必做)

項目做法
檔案型態驗證`finfo` 檢測真實 MIME + 副檔名白名單,雙重檢查
檔名安全化**不使用用戶原始檔名**,`bin2hex(random_bytes(16))` + 正確副檔名
儲存路徑隔離儲存於 **公開目錄外** (`storage/app`),透過簽名 URL / 控制器輸出
圖片重新處理縮圖/水印過程**移除 EXIF**、壓縮品質統一,**防止圖片炸彈 (Decompression Bomb)**
大小限制Nginx `client_max_body_size` + PHP `upload_max_filesize` + 程式碼雙重限制
掃毒上傳後送 ClamAV 掃描 (非同步),惡意隔離
權限控制儲存目錄權限 750,擁有者 www-data,禁止執行位

遷移建議

  1. 新專案**直接使用 Intervention Image + 介面導向設計**。
  2. 舊專案逐步重構:建立 `ImageUploaderInterface`,舊 `UpImages` 實作介面做為 Legacy 適配器,新功能注入新實作。
  3. 單元測試:Mock `UploadedFile`,測試驗證邏輯、路徑生成、例外拋出。

總結

圖片上傳類核心職責:**驗證、安全儲存、衍生圖產生 (縮圖/水印)、回傳結構化結果**。現代化關鍵在於**介面隔離、依賴注入、Intervention Image 驅動抽象、安全預設值、錯誤用例外而非 die()**。

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