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

PHP 批量檔案上傳完整實作指南

     編輯:PHP 相關知識

前言

批量檔案上傳是後台管理系統、內容發布平台的常見需求。本文提供一套結構清晰、可直接落地的 PHP 批量上傳方案,包含前端表單、後端處理邏輯與可複用的上傳類別,支援多檔案同時上傳、類型/大小驗證、隨機命名與錯誤彙總。

目錄結構

upload/
├── index.html       # 上傳表單頁面
├── upload.php       # 接收處理入口
└── UploadFile.class.php  # 核心上傳類別

1. 前端表單 (index.html)

<!DOCTYPE html>
<html xmlns="https://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8">
    <title>批量檔案上傳示範</title>
    <style>
        body { font-family: "Microsoft JhengHei", sans-serif; padding: 20px; }
        .file-group { margin: 8px 0; }
        .btn { padding: 8px 20px; margin-right: 10px; cursor: pointer; }
        .msg { margin-top: 15px; line-height: 1.8; }
        .success { color: #28a745; } .error { color: #dc3545; }
    </style>
    <script>
        // 移除未選檔案的 input,避免空值提交
        function cleanEmptyInputs() {
            for (let i = 1; i <= 8; i++) {
                const el = document.getElementById('file' + i);
                if (el && !el.value) el.name = 'ignore';
            }
        }
    </script>
</head>
<body>
    <h2>批量圖片上傳 (最多 8 張)</h2>
    <form name="upfile" action="upload.php" method="post" 
          enctype="multipart/form-data" onsubmit="return cleanEmptyInputs()">
        <div class="file-group"><input type="file" name="spic[]" id="file1" accept="image/*" /></div>
        <div class="file-group"><input type="file" name="spic[]" id="file2" accept="image/*" /></div>
        <div class="file-group"><input type="file" name="spic[]" id="file3" accept="image/*" /></div>
        <div class="file-group"><input type="file" name="spic[]" id="file4" accept="image/*" /></div>
        <div class="file-group"><input type="file" name="spic[]" id="file5" accept="image/*" /></div>
        <div class="file-group"><input type="file" name="spic[]" id="file6" accept="image/*" /></div>
        <div class="file-group"><input type="file" name="spic[]" id="file7" accept="image/*" /></div>
        <div class="file-group"><input type="file" name="spic[]" id="file8" accept="image/*" /></div>
        <button type="submit" name="sub" class="btn">開始上傳</button>
        <button type="reset" class="btn">重新選擇</button>
    </form>
    <div class="msg" id="result"></div>
</body>
</html>

2. 核心上傳類別 (UploadFile.class.php)

<?php
/**
 * 通用檔案上傳類別
 * 支援單/多檔案、類型驗證、大小限制、隨機命名、多級目錄自動建立
 */
class UploadFile
{
    // ----- 配置屬性 -----
    private $maxSize     = 1048576;        // 1 MB
    private $allowTypes  = ['jpg','png','gif','jpeg','bmp','webp'];
    private $randomName  = true;           // 是否啟用隨機檔名
    private $uploadPath  = '';             // 上傳根目錄
    private $subDir      = '';             // 子目錄 (可選)

    // ----- 運行時屬性 -----
    private $originNames = [];
    private $tmpNames    = [];
    private $newNames    = [];
    private $sizes       = [];
    private $types       = [];
    private $errorCode   = 0;
    private $errorMsgs   = [];

    /**
     * 建構子:接受關聯陣列覆蓋預設配置
     * @param array $options ['maxsize'=>,'allowtype'=>,'israndfile'=>,'filepath'=>]
     */
    public function __construct(array $options = [])
    {
        foreach ($options as $key => $val) {
            $key = strtolower($key);
            if (property_exists($this, $key)) {
                $this->$key = $val;
            }
        }
        // 確保路徑以 DIRECTORY_SEPARATOR 結尾
        $this->uploadPath = rtrim($this->uploadPath, '/\\') . DIRECTORY_SEPARATOR;
    }

    // ----- 公開介面 -----

    /**
     * 執行上傳
     * @param string $formName 表單 name 屬性 (對應 $_FILES 鍵名)
     * @return bool 成功至少一個檔案回傳 true
     */
    public function upload(string $formName): bool
    {
        $this->resetState();
        
        if (!$this->validateUpload($formName)) {
            return false;
        }

        $files = $this->normalizeFilesArray($formName);
        $allOk = true;

        // 第一輪:驗證所有檔案
        foreach ($files as $idx => $file) {
            if (!$this->setFileInfo($file, $idx)) {
                $allOk = false;
                continue;
            }
            if (!$this->checkSize() || !$this->checkType()) {
                $this->errorMsgs[] = $this->getErrorMsg();
                $allOk = false;
            }
        }

        // 第二輪:實際移動檔案 (僅當全部驗證通過)
        if ($allOk) {
            foreach ($files as $idx => $file) {
                $this->setFileInfo($file, $idx); // 重置屬性
                if (!$this->moveFile()) {
                    $this->errorMsgs[] = $this->getErrorMsg();
                    $allOk = false;
                } else {
                    $this->newNames[] = $this->newNames[$idx] ?? '';
                }
            }
        }

        return $allOk;
    }

    /** 取得成功上傳的新檔名陣列 */
    public function getNewNames(): array
    {
        return $this->newNames;
    }

    /** 取得錯誤訊息陣列 */
    public function getErrors(): array
    {
        return $this->errorMsgs;
    }

    // ----- 內部邏輯 -----

    private function resetState(): void
    {
        $this->originNames = $this->tmpNames = $this->newNames = $this->sizes = $this->types = [];
        $this->errorCode = 0;
        $this->errorMsgs = [];
    }

    private function validateUpload(string $formName): bool
    {
        if (empty($_FILES[$formName]) || !is_array($_FILES[$formName]['name'])) {
            $this->errorCode = 4;
            $this->errorMsgs[] = $this->getErrorMsg();
            return false;
        }
        return $this->checkDir($this->uploadPath);
    }

    /** 將 $_FILES 正規化為索引陣列 */
    private function normalizeFilesArray(string $formName): array
    {
        $src = $_FILES[$formName];
        $count = count($src['name']);
        $files = [];
        for ($i = 0; $i < $count; $i++) {
            $files[] = [
                'name'     => $src['name'][$i],
                'tmp_name' => $src['tmp_name'][$i],
                'size'     => $src['size'][$i],
                'error'    => $src['error'][$i],
            ];
        }
        return $files;
    }

    private function setFileInfo(array $file, int $idx): bool
    {
        if (!$this->checkDir($this->uploadPath)) return false;

        if ($file['error'] !== UPLOAD_ERR_OK) {
            $this->errorCode = $file['error'];
            return false;
        }

        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $this->originNames[$idx] = $file['name'];
        $this->tmpNames[$idx]    = $file['tmp_name'];
        $this->sizes[$idx]       = $file['size'];
        $this->types[$idx]       = $ext;
        return true;
    }

    private function checkDir(string $path): bool
    {
        if (empty($path)) {
            $this->errorCode = -5; return false;
        }
        if (!is_dir($path) && !@mkdir($path, 0755, true)) {
            $this->errorCode = -4; return false;
        }
        if (!is_writable($path)) {
            $this->errorCode = -4; return false;
        }
        return true;
    }

    private function checkSize(): bool
    {
        $idx = count($this->sizes) - 1;
        if ($this->sizes[$idx] > $this->maxSize) {
            $this->errorCode = -2; return false;
        }
        return true;
    }

    private function checkType(): bool
    {
        $idx = count($this->types) - 1;
        if (!in_array($this->types[$idx], $this->allowTypes)) {
            $this->errorCode = -1; return false;
        }
        return true;
    }

    private function moveFile(): bool
    {
        $idx = count($this->tmpNames) - 1;
        $ext = $this->types[$idx];

        // 產生目標檔名
        if ($this->randomName) {
            $newName = date('YmdHis') . '_' . mt_rand(1000, 9999) . '.' . $ext;
        } else {
            $newName = $this->originNames[$idx];
        }

        $dest = $this->uploadPath . $newName;
        if (@move_uploaded_file($this->tmpNames[$idx], $dest)) {
            $this->newNames[$idx] = $newName;
            return true;
        }
        $this->errorCode = -3;
        return false;
    }

    private function getErrorMsg(): string
    {
        $prefix = "檔案「{$this->originNames[count($this->originNames)-1] ?? '未知'}」上傳失敗:";
        return $prefix . match ($this->errorCode) {
            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 擴充套件阻擋',
            -1 => '不允許的檔案類型 (僅限: ' . implode(', ', $this->allowTypes) . ')',
            -2 => "檔案過大,不可超過 {$this->maxSize} 位元組",
            -3 => '移動檔案失敗,請檢查目錄權限',
            -4 => '建立/寫入上傳目錄失敗',
            -5 => '未指定上傳路徑',
            default => '未知錯誤 (代碼: ' . $this->errorCode . ')',
        };
    }
}

3. 處理入口 (upload.php)

<?php
require_once 'UploadFile.class.php';

header('Content-Type: text/html; charset=utf-8');

// 實例化:自訂儲存路徑、允許類型、最大 2MB、啟用隨機命名
$uploader = new UploadFile([
    'filepath'   => __DIR__ . '/uploads',
    'allowtype'  => ['jpg','png','gif','jpeg','webp'],
    'maxsize'    => 2 * 1024 * 1024,
    'israndfile' => true,
]);

if ($uploader->upload('spic')) {
    echo '
✅ 所有檔案上傳成功!
'; foreach ($uploader->getNewNames() as $name) { echo "
📄 {$name}
"; } } else { echo '
❌ 上傳遇到錯誤:
'; foreach ($uploader->getErrors() as $err) { echo "
{$err}
"; } }

進階擴充建議

  • 圖片壓縮/縮圖:整合 Intervention Image 或 GD 在 moveFile() 後處理。
  • 非同步上傳:前端改用 fetch 分片上傳,後端支援斷點續傳。
  • 雲端儲存:將 moveFile() 替換為 S3/OSS SDK 上傳邏輯,介面保持不變。
  • 資料庫記錄:成功後寫入檔案資訊表,關聯業務主體 ID。

總結

本方案以類別封裝核心邏輯,前後端分離明確,支援批次驗證與原子性移動(全成功或全失敗),代碼結構清晰易於維護擴充。實際部署時請依專案需求調整允許類型、大小限制與儲存策略,並務必在 Web 伺服器層面禁止上傳目錄執行腳本。

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