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

PHP 檔案上傳類別完整實作:支援大小限制、類型過濾、中文檔名、自動重命名

     編輯:PHP 綜合知識

封裝一個功能完整的 FileUploader 類別,涵蓋檔案大小/類型驗證、上傳目錄管理、中文檔名編碼處理、衝突自動重命名、錯誤代碼對照、回傳結構化資訊等實務需求。

完整類別程式碼

<?php
/**
 * 檔案上傳處理類別
 * 支援:大小限制、類型過濾、中文檔名、自動重命名、多檔上傳
 */
class FileUploader {
    // 錯誤代碼常數
    const ERR_NO_FILE       = 'UPLOAD_ERR_NO_FILE';
    const ERR_INVALID_TYPE  = 'UPLOAD_ERR_INVALID_TYPE';
    const ERR_EXCEEDS_SIZE  = 'UPLOAD_ERR_EXCEEDS_SIZE';
    const ERR_FILE_EXISTS   = 'UPLOAD_ERR_FILE_EXISTS';
    const ERR_MOVE_FAILED   = 'UPLOAD_ERR_MOVE_FAILED';
    const ERR_UPLOAD_FAILED = 'UPLOAD_ERR_UPLOAD_FAILED';
    const ERR_DIR_NOT_WRITABLE = 'UPLOAD_ERR_DIR_NOT_WRITABLE';

    // 內部屬性
    private $file;           // $_FILES 陣列單一元素
    private $upload_dir;     // 上傳目錄
    private $allowed_types;  // 允許類型陣列
    private $max_size;       // 最大位元組數
    private $rename;         // 是否自動重命名
    private $encoding;       // 檔名編碼轉換
    private $overwrite;      // 是否覆蓋同名檔案

    // 回傳結果
    private $result = [
        'success' => false,
        'error' => null,
        'file' => null,      // 成功時的檔案資訊陣列
    ];

    /**
     * 建構子
     * @param array $file $_FILES['field_name'] 單一檔案資訊
     * @param string $upload_dir 上傳目錄路徑
     * @param array $options 選項陣列
     */
    public function __construct($file, $upload_dir, $options = []) {
        $this->file = $file;
        $this->upload_dir = rtrim($upload_dir, '/') . '/';
        
        // 預設選項
        $defaults = [
            'allowed_types' => [],      // 空陣列 = 不限制
            'max_size' => 10 * 1024 * 1024,  // 10MB
            'rename' => true,           // 自動重命名避免衝突
            'encoding' => 'UTF-8',      // 目標編碼
            'overwrite' => false,       // 不覆蓋
        ];
        $options = array_merge($defaults, $options);
        
        $this->allowed_types = array_map('strtolower', $options['allowed_types']);
        $this->max_size = $options['max_size'];
        $this->rename = $options['rename'];
        $this->encoding = $options['encoding'];
        $this->overwrite = $options['overwrite'];
    }

    /**
     * 執行上傳
     * @return array 結果陣列
     */
    public function upload() {
        // 1. 基本檢查
        if (!$this->validateUpload()) return $this->result;

        // 2. 類型檢查
        if (!$this->checkType()) return $this->result;

        // 3. 大小檢查
        if (!$this->checkSize()) return $this->result;

        // 4. 目錄檢查
        if (!$this->ensureDirectory()) return $this->result;

        // 5. 產生目標檔名
        $filename = $this->generateFilename();
        $dest_path = $this->upload_dir . $filename;

        // 6. 處理同名檔案
        if (file_exists($dest_path) && !$this->overwrite) {
            if ($this->rename) {
                $filename = $this->generateUniqueFilename($filename);
                $dest_path = $this->upload_dir . $filename;
            } else {
                $this->setError(self::ERR_FILE_EXISTS, "檔案已存在: $filename");
                return $this->result;
            }
        }

        // 7. 移動檔案
        if (!move_uploaded_file($this->file['tmp_name'], $dest_path)) {
            $this->setError(self::ERR_MOVE_FAILED, '檔案移動失敗,請檢查權限');
            return $this->result;
        }

        // 8. 設定權限(可選)
        @chmod($dest_path, 0644);

        // 9. 成功回傳
        $this->result['success'] = true;
        $this->result['file'] = [
            'original_name' => $this->file['name'],
            'saved_name' => $filename,
            'path' => $dest_path,
            'url' => $this->getPublicUrl($filename),
            'size' => $this->file['size'],
            'mime' => $this->file['type'],
            'extension' => $this->getExtension($this->file['name']),
        ];
        return $this->result;
    }

    /**
     * 批次上傳多個檔案
     * @param array $files $_FILES['field_name'] (多檔案格式)
     * @return array 每個檔案的結果陣列
     */
    public static function uploadMultiple($files, $upload_dir, $options = []) {
        $results = [];
        // 正規化多檔案陣列結構
        $normalized = self::normalizeFilesArray($files);
        foreach ($normalized as $file) {
            $uploader = new self($file, $upload_dir, $options);
            $results[] = $uploader->upload();
        }
        return $results;
    }

    // ===== 內部方法 =====

    private function validateUpload() {
        if (empty($this->file) || !is_array($this->file)) {
            $this->setError(self::ERR_NO_FILE, '無上傳檔案資訊');
            return false;
        }
        if ($this->file['error'] !== UPLOAD_ERR_OK) {
            $this->setError(self::ERR_UPLOAD_FAILED, $this->getUploadErrorMessage($this->file['error']));
            return false;
        }
        if (!is_uploaded_file($this->file['tmp_name'])) {
            $this->setError(self::ERR_UPLOAD_FAILED, '非合法上傳檔案');
            return false;
        }
        return true;
    }

    private function checkType() {
        if (empty($this->allowed_types)) return true;
        
        $ext = strtolower($this->getExtension($this->file['name']));
        if (!in_array($ext, $this->allowed_types)) {
            $this->setError(self::ERR_INVALID_TYPE, 
                "檔案類型不允許: .$ext (允許: " . implode(', ', $this->allowed_types) . ")");
            return false;
        }
        return true;
    }

    private function checkSize() {
        if ($this->file['size'] > $this->max_size) {
            $this->setError(self::ERR_EXCEEDS_SIZE, 
                sprintf('檔案過大: %.2f MB (限制: %.2f MB)', 
                    $this->file['size'] / 1024 / 1024, 
                    $this->max_size / 1024 / 1024));
            return false;
        }
        return true;
    }

    private function ensureDirectory() {
        if (!is_dir($this->upload_dir)) {
            if (!mkdir($this->upload_dir, 0755, true)) {
                $this->setError(self::ERR_DIR_NOT_WRITABLE, '無法建立上傳目錄');
                return false;
            }
        }
        if (!is_writable($this->upload_dir)) {
            $this->setError(self::ERR_DIR_NOT_WRITABLE, '上傳目錄不可寫入');
            return false;
        }
        return true;
    }

    private function generateFilename() {
        $original = $this->file['name'];
        $ext = $this->getExtension($original);
        $name = $this->getBasename($original);
        
        // 編碼轉換(解決中文檔名問題)
        if ($this->encoding && function_exists('mb_convert_encoding')) {
            $name = mb_convert_encoding($name, $this->encoding, 'auto');
        }
        
        // 過濾危險字元
        $name = preg_replace('/[^a-zA-Z0-9_\-\.\(\)\[\] ]/u', '', $name);
        $name = trim($name);
        if ($name === '') $name = 'file';
        
        return $this->rename ? $this->generateUniqueName($name, $ext) : "$name.$ext";
    }

    private function generateUniqueName($name, $ext) {
        return $name . '_' . date('YmdHis') . '_' . substr(md5(uniqid(microtime(true), true)), 0, 8) . ".$ext";
    }

    private function generateUniqueFilename($filename) {
        $info = pathinfo($filename);
        $name = $info['filename'];
        $ext = isset($info['extension']) ? $info['extension'] : '';
        $counter = 1;
        while (file_exists($this->upload_dir . $name . '_' . $counter . ($ext ? ".$ext" : ''))) {
            $counter++;
        }
        return $name . '_' . $counter . ($ext ? ".$ext" : '');
    }

    private function getExtension($filename) {
        $pos = strrpos($filename, '.');
        return $pos !== false ? substr($filename, $pos + 1) : '';
    }

    private function getBasename($filename) {
        $pos = strrpos($filename, '.');
        return $pos !== false ? substr($filename, 0, $pos) : $filename;
    }

    private function getPublicUrl($filename) {
        // 根據實際部署調整:可能是 /uploads/xxx 或完整 URL
        return '/uploads/' . $filename;
    }

    private function getUploadErrorMessage($code) {
        static $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 . ')';
    }

    private function setError($code, $message) {
        $this->result['success'] = false;
        $this->result['error'] = ['code' => $code, 'message' => $message];
    }

    // 正規化 $_FILES 多檔案結構
    private static function normalizeFilesArray($files) {
        $normalized = [];
        if (isset($files['name']) && is_array($files['name'])) {
            // 多檔案:name, type, tmp_name, error, size 皆為陣列
            $count = count($files['name']);
            for ($i = 0; $i < $count; $i++) {
                $normalized[] = [
                    'name'     => $files['name'][$i],
                    'type'     => $files['type'][$i],
                    'tmp_name' => $files['tmp_name'][$i],
                    'error'    => $files['error'][$i],
                    'size'     => $files['size'][$i],
                ];
            }
        } else {
            // 單檔案
            $normalized[] = $files;
        }
        return $normalized;
    }
}
?>

使用範例

單檔上傳

<?php
require_once 'FileUploader.php';

$upload_dir = __DIR__ . '/uploads/';
$options = [
    'allowed_types' => ['jpg', 'jpeg', 'png', 'gif', 'pdf'],
    'max_size' => 5 * 1024 * 1024,  // 5MB
    'rename' => true,
];

$uploader = new FileUploader($_FILES['avatar'], $upload_dir, $options);
$result = $uploader->upload();

if ($result['success']) {
    echo "上傳成功!\n";
    echo "檔案: {$result['file']['saved_name']}\n";
    echo "大小: {$result['file']['size']} bytes\n";
    echo "網址: {$result['file']['url']}\n";
    // 寫入資料庫: $result['file']['saved_name']
} else {
    echo "上傳失敗: [{$result['error']['code']}] {$result['error']['message']}\n";
}
?>

多檔上傳

<?php
// HTML: 
$results = FileUploader::uploadMultiple($_FILES['photos'], $upload_dir, $options);

$success = 0;
foreach ($results as $i => $result) {
    if ($result['success']) {
        $success++;
        // 處理成功檔案...
    } else {
        echo "檔案 $i 失敗: {$result['error']['message']}\n";
    }
}
echo "成功: $success / " . count($results) . "\n";
?>

HTML 表單注意事項

<!-- 必須加上 enctype -->
<form action="upload.php" method="POST" enctype="multipart/form-data">
    <!-- 單檔 -->
    <input type="file" name="avatar" accept="image/*">
    
    <!-- 多檔 -->
    <input type="file" name="photos[]" multiple accept="image/*">
    
    <!-- 可選:限制客戶端檔案大小(瀏覽器支援度有限,後端必須再驗證) -->
    <input type="hidden" name="MAX_FILE_SIZE" value="5242880"> <!-- 5MB -->
    
    <button type="submit">上傳</button>
</form>

進階擴充建議

  • 圖片處理:整合 Intervention Image 自動產生縮圖、加浮水印、格式轉換
  • 雲端儲存:實作 StorageInterface,支援 S3、GCS、Azure Blob、阿里雲 OSS
  • 分片上傳:大檔案斷點續傳,前端用 WebUppy/Resumable.js,後端合併分片
  • 病毒掃描:整合 ClamAV 掃描上傳檔案
  • 權限控管:依使用者角色限制可上傳類型、大小、數量

這個類別解決了實務上最常見的中文檔名亂碼、同名覆蓋、類型大小驗證、錯誤處理等問題,可直接投入生產環境使用。

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