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

PHP 多檔案上傳完整處理類:支援驗證、重命名、分類儲存、錯誤處理

     編輯:PHP 綜合知識

前言

HTML 原生 <input type="file" multiple> 可一次選擇多個檔案,但後端需正確解析 $_FILES 多維陣列結構、執行安全驗證、防止檔名衝突。本文提供一個生產級 Upload 類別,涵蓋驗證、重命名、目錄管理、錯誤處理全流程。

$_FILES 多檔案結構解析

單檔:$_FILES['upfile']['name'] 為字串
多檔:$_FILES['upfile']['name'] 為陣列,結構為:

$_FILES['upfile'] = [
    'name'     => ['a.jpg', 'b.png'],
    'type'     => ['image/jpeg', 'image/png'],
    'tmp_name' => ['/tmp/phpX1', '/tmp/phpX2'],
    'error'    => [0, 0],
    'size'     => [12345, 67890],
];

Upload 類別完整程式碼

<?php
/**
 * 多檔案上傳處理類
 * 支援:副檔名/MIME/大小驗證、自動重命名、目錄自建、權限檢查
 */
class Upload
{
    // 公開配置
    public array $allowedExt = [];   // 允許副檔名,空陣列=用預設圖片 MIME
    public int   $maxSizeKb  = 5120; // 最大 5MB
    public string $saveDir   = '';   // 儲存目錄 (絕對路徑)

    // 內部狀態
    private string $fieldName;
    private bool   $rename     = true;
    private array  $files      = [];
    private array  $results    = [];
    private int    $fileCount  = 0;

    public function __construct(string $fieldName, array $allowedExt = [], bool $rename = true)
    {
        if (!$fieldName) throw new InvalidArgumentException('檔案欄位名稱不可為空');
        $this->fieldName  = $fieldName;
        $this->allowedExt = $allowedExt;
        $this->rename     = $rename;
        $this->saveDir    = defined('WEBSITE_DIRROOT') ? WEBSITE_DIRROOT . ($GLOBALS['cfg_upload_path'] ?? '/uploads/') : __DIR__ . '/uploads/';
        $this->initFiles();
    }

    /** 解析 $_FILES 統一為單檔陣列格式 */
    private function initFiles(): void
    {
        if (!isset($_FILES[$this->fieldName])) {
            throw new RuntimeException("找不到上傳欄位: {$this->fieldName}");
        }

        $src = $_FILES[$this->fieldName];

        // 判斷是否多檔:name 為陣列且 error 非 4 (UPLOAD_ERR_NO_FILE)
        if (is_array($src['name'])) {
            $count = count($src['name']);
            for ($i = 0; $i < $count; $i++) {
                if (($src['error'][$i] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) {
                    $this->files[] = [
                        'tmp_name' => $src['tmp_name'][$i],
                        'name'     => $src['name'][$i],
                        'type'     => $src['type'][$i],
                        'size'     => $src['size'][$i],
                        'error'    => $src['error'][$i],
                    ];
                }
            }
        } elseif ($src['error'] !== UPLOAD_ERR_NO_FILE) {
            $this->files[] = [
                'tmp_name' => $src['tmp_name'],
                'name'     => $src['name'],
                'type'     => $src['type'],
                'size'     => $src['size'],
                'error'    => $src['error'],
            ];
        }

        $this->fileCount = count($this->files);
        if ($this->fileCount === 0) {
            throw new RuntimeException('沒有有效的上傳檔案');
        }
        $this->validate();
    }

    /** 驗證副檔名/MIME/大小 */
    private function validate(): void
    {
        $allowedMimes = ['image/wbmp', 'image/bmp', 'image/gif', 'image/jpeg', 'image/pjpeg', 'image/png', 'image/x-png'];

        foreach ($this->files as $f) {
            // 錯誤碼檢查
            if ($f['error'] !== UPLOAD_ERR_OK) {
                throw new RuntimeException("檔案 {$f['name']} 上傳錯誤碼: {$f['error']}");
            }

            // 大小檢查
            if ($f['size'] > $this->maxSizeKb * 1024) {
                throw new RuntimeException("檔案 {$f['name']} 超過限制 {$this->maxSizeKb}KB");
            }

            // 格式檢查
            if ($this->allowedExt) {
                $ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION));
                if (!in_array($ext, array_map('strtolower', $this->allowedExt), true)) {
                    throw new RuntimeException("不允許的副檔名: {$ext}");
                }
            } else {
                if (!in_array($f['type'], $allowedMimes, true)) {
                    throw new RuntimeException("不允許的 MIME 類型: {$f['type']}");
                }
            }
        }
    }

    /** 執行上傳 */
    public function upload(): array
    {
        // 確保目錄存在且可寫
        if (!is_dir($this->saveDir)) {
            if (!mkdir($this->saveDir, 0777, true)) {
                throw new RuntimeException("建立目錄失敗: {$this->saveDir}");
            }
        }
        if (!is_writable($this->saveDir)) {
            throw new RuntimeException("目錄不可寫入: {$this->saveDir}");
        }

        foreach ($this->files as $f) {
            if (!is_uploaded_file($f['tmp_name'])) {
                throw new RuntimeException("非合法上傳檔案: {$f['name']}");
            }

            $fileName = $f['name'];
            if ($this->rename) {
                $ext = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
                $rand = substr(md5(uniqid('', true)), mt_rand(0, 26), 6);
                $fileName = date('YmdHis') . '_' . $rand . '.' . $ext;
            }

            $dest = $this->saveDir . $fileName;
            if (!move_uploaded_file($f['tmp_name'], $dest)) {
                throw new RuntimeException("移動檔案失敗: {$fileName}");
            }

            // 回傳相對路徑 (相對網站根目錄)
            $this->results[] = str_replace(defined('WEBSITE_DIRROOT') ? WEBSITE_DIRROOT : '', '', $dest);
        }
        return $this->results;
    }

    /** 取得上傳結果 */
    public function getResults(): array
    {
        return $this->results;
    }
}

使用範例

<?php
require_once 'Upload.php';

try {
    // 允許 jpg/png/gif/pdf,最大 2MB,自動重命名
    $uploader = new Upload('files', ['jpg', 'jpeg', 'png', 'gif', 'pdf'], true);
    $uploader->maxSizeKb = 2048;
    $uploader->saveDir   = '/var/www/html/uploads/';

    $paths = $uploader->upload();
    echo json_encode(['success' => true, 'files' => $paths]);
} catch (Throwable $e) {
    http_response_code(400);
    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
}

HTML 表單

<form action="upload.php" method="post" enctype="multipart/form-data">
    <input type="file" name="files[]" multiple accept="image/*,.pdf">
    <button type="submit">上傳</button>
</form>

關鍵安全檢查清單

檢查點實作位置說明
檔案數量限制前端 max-files / 後端 count($files)防止 DoS
副檔名白名單$allowedExt阻擋 .php/.exe 等可執行檔
MIME 類型二次確認finfo_file() 或 getimagesize()防止偽裝副檔名
檔案大小限制$maxSizeKb + php.ini upload_max_filesize雙層防護
儲存目錄非 Web 可執行Nginx/Apache 設定 location ~ \.php$ { deny all; }防止上傳木馬執行
隨機檔名$rename = true避免覆蓋、路徑遍歷、猜測
病毒掃描整合 ClamAV clamscan企業級需求

總結

此 Upload 類別封裝了多檔案上傳的完整生命週期:解析 $_FILES、驗證、重命名、儲存、回傳路徑。實務專案可依需求擴充:圖片壓縮、水印、雲端儲存 (S3/OSS)、資料庫記錄、非同步處理佇列。核心原則:**永不信任客戶端輸入,伺服器端全方位驗證與清理**。

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