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

PHP 文件上傳全攻略(中):生產級驗證封裝與單文件上傳類實戰

     編輯:PHP 相關知識

PHP 文件上傳全攻略(中):生產級驗證封裝與單文件上傳類實戰

承接上篇,本文將分散的驗證邏輯封裝為可復用的 UploadHandler 類,涵蓋 MIME/擴展名雙重校驗、魔數檢測防偽、唯一文件名生成、目錄自動創建、錯誤信息國際化等生產級特性。

核心驗證邏輯

  1. 錯誤碼優先:$_FILES['error'] !== UPLOAD_ERR_OK 直接映射友好提示;
  2. 文件大小:對比 $_FILES['size'] 與配置上限;
  3. 擴展名白名單:pathinfo($name, PATHINFO_EXTENSION) 取後綴,strtolower 統一小寫,in_array 判斷;
  4. MIME 類型輔助判斷:$_FILES['type'] 參考,但**不可信**;
  5. 魔數/真實類型檢測(核心防偽):

魔數檢測實現

function getRealMime(string $tmpPath): ?string {
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    return $finfo->file($tmpPath); // 如 image/png, application/pdf
}

// 圖片專用:getimagesize() 同時獲取尺寸
function isTrueImage(string $tmpPath): bool {
    $info = @getimagesize($tmpPath);
    return $info !== false && in_array($info[2], [IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP]);
}

完整 UploadHandler 類

<?php
class UploadHandler {
    private $config = [
        'allowExt'   => ['jpg','jpeg','png','gif','webp','pdf'],
        'allowMime'  => ['image/jpeg','image/png','image/gif','image/webp','application/pdf'],
        'maxSize'    => 20 * 1024 * 1024, // 20MB
        'saveRoot'   => '/var/www/uploads',
        'checkMime'  => true, // 是否啟用魔數檢測
    ];
    private $errors = [];

    public function __construct(array $customConfig = []) {
        $this->config = array_merge($this->config, $customConfig);
    }

    public function upload(string $fieldName): ?array {
        if (!isset($_FILES[$fieldName])) {
            $this->errors[] = "字段 $fieldName 不存在";
            return null;
        }
        $file = $_FILES[$fieldName];

        // 1. 錯誤碼
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->errors[] = $this->mapError($file['error']);
            return null;
        }

        // 2. 大小
        if ($file['size'] > $this->config['maxSize']) {
            $this->errors[] = '文件超過大小限制';
            return null;
        }

        // 3. 擴展名
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($ext, $this->config['allowExt'])) {
            $this->errors[] = '不允許的文件類型';
            return null;
        }

        // 4. MIME 雙重校驗
        if ($this->config['checkMime']) {
            $realMime = $this->getRealMime($file['tmp_name']);
            if (!$realMime || !in_array($realMime, $this->config['allowMime'])) {
                $this->errors[] = '文件內容類型非法';
                return null;
            }
        }

        // 5. HTTP POST 上傳驗證
        if (!is_uploaded_file($file['tmp_name'])) {
            $this->errors[] = '非合法上傳方式';
            return null;
        }

        // 6. 目標目錄準備
        $dateDir = date('Y/m/d');
        $saveDir = $this->config['saveRoot'] . '/' . $dateDir;
        if (!is_dir($saveDir)) {
            if (!mkdir($saveDir, 0755, true)) {
                $this->errors[] = '無法創建保存目錄';
                return null;
            }
        }

        // 7. 唯一文件名:md5(uniqid(microtime(true), true)) + 擴展名
        $filename = md5(uniqid(microtime(true), true)) . '.' . $ext;
        $destPath = $saveDir . '/' . $filename;

        // 8. 移動文件
        if (!move_uploaded_file($file['tmp_name'], $destPath)) {
            $this->errors[] = '文件移動失敗';
            return null;
        }

        // 9. 返回標準化信息
        return [
            'origin_name' => $file['name'],
            'save_path'   => $destPath,
            'url'         => str_replace($_SERVER['DOCUMENT_ROOT'], '', $destPath), // 相對 Web 根目錄
            'size'        => $file['size'],
            'mime'        => $this->getRealMime($destPath),
            'ext'         => $ext,
            'width'       => null,
            'height'      => null,
        ];
    }

    private function getRealMime(string $path): ?string {
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        return $finfo->file($path);
    }

    private function mapError(int $code): string {
        static $map = [
            UPLOAD_ERR_INI_SIZE   => '超過服務器配置上限 (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 $map[$code] ?? '未知上傳錯誤';
    }

    public function getErrors(): array { return $this->errors; }
}

使用示例

<?php
require_once 'UploadHandler.php';

$uploader = new UploadHandler([
    'allowExt'  => ['jpg','png','pdf'],
    'maxSize'   => 5 * 1024 * 1024,
    'saveRoot'  => __DIR__ . '/data/uploads',
]);

$result = $uploader->upload('avatar'); // 對應 
if ($result) {
    echo json_encode(['code'=>0, 'msg'=>'上傳成功', 'data'=>$result]);
} else {
    echo json_encode(['code'=>1, 'msg'=>implode('; ', $uploader->getErrors())]);
}

關鍵設計決策解析

決策點方案理由
命名策略md5(uniqid(microtime(true), true))極低碰撞概率、無業務含義、不洩露用戶信息
目錄結構Y/m/d 日期分層單目錄文件數可控、利於備份清理、天然分佈
魔數檢測finfo + getimagesize 雙重finfo 通用、getimagesize 專精圖片尺寸,雙重保險
錯誤處理收集數組而非拋異常上傳屬業務邏輯分支,非異常流,收集便於前端聚合展示
返回 URL相對 DocumentRoot解耦存儲路徑與訪問路徑,支援 CDN/對象存儲遷移

下篇將講解多文件上傳、分片斷點續傳、對象存儲(S3/OSS)適配、圖片處理管道(縮略圖/水印/格式轉換)等進階主題。

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