文件上傳失敗時,$_FILES['file']['error'] 是排查的第一現場。本文提供完整錯誤碼常量對照、中文說明、觸發場景、處理建議,並封裝統一異常處理類。
| 常量 | 值 | 含義 | 典型觸發場景 | 處理建議 |
|---|---|---|---|---|
| UPLOAD_ERR_OK | 0 | 上傳成功 | — | 正常處理 |
| UPLOAD_ERR_INI_SIZE | 1 | 超過 php.ini upload_max_filesize | 上傳大文件、配置過小 | 提示用戶壓縮/分片;調大配置並重啟 PHP-FPM |
| UPLOAD_ERR_FORM_SIZE | 2 | 超過表單 MAX_FILE_SIZE 隱藏域 | 前端限制小於實際文件 | 移除或調大隱藏域;注意此限制極易繞過,不可信任 |
| UPLOAD_ERR_PARTIAL | 3 | 文件只被部分上傳 | 網絡中斷、客戶端取消、PHP 超時 | 提示重試;檢查 max_execution_time、Nginx client_body_timeout |
| UPLOAD_ERR_NO_FILE | 4 | 沒有文件被上傳 | 用戶未選文件直接提交、input name 不匹配 | 前端校驗必填;後端兜底判斷 |
| UPLOAD_ERR_NO_TMP_DIR | 6 | 缺少臨時目錄 | upload_tmp_dir 未配置或目錄不可寫 | 配置有效可寫目錄;檢查 open_basedir 限制 |
| UPLOAD_ERR_CANT_WRITE | 7 | 寫入臨時文件失敗 | 磁盤滿、權限不足、inode 耗盡、SELinux 限制 | 磁盤清理、權限修復、df -i 檢查 inode |
| UPLOAD_ERR_EXTENSION | 8 | PHP 擴展阻止上傳 | 安裝了類似 suhosin、mod_security 的擴展並攔截 | 檢查擴展配置白名單;必要時禁用擴展 |
<?php
class UploadException extends Exception {
public function __construct(public int $code, string $message = '') {
parent::__construct($message ?: self::getMessageByCode($code), $code);
}
public static function getMessageByCode(int $code): string {
return match($code) {
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 => '上傳被服務器擴展攔截',
default => '未知上傳錯誤',
};
}
}
function handleUpload(array $file, array $allowedTypes = [], int $maxSize = 10485760): string {
// 1. 錯誤碼檢查
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new UploadException($file['error']);
}
// 2. 文件大小二次檢查(防繞過 MAX_FILE_SIZE)
if ($file['size'] > $maxSize) {
throw new UploadException(UPLOAD_ERR_INI_SIZE, "文件大小 {$file['size']} 超過限制 $maxSize");
}
// 3. MIME 類型檢查(不要信任瀏覽器傳來的 type,用 finfo 驗證真實類型)
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if ($allowedTypes && !in_array($mime, $allowedTypes, true)) {
throw new UploadException(0, "文件類型 $mime 不在允許列表中");
}
// 4. 構建安全存儲路徑
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeName = bin2hex(random_bytes(16)) . ($ext ? ".$ext" : '');
$dateDir = date('Y/m/d');
$destDir = "/var/www/uploads/$dateDir";
if (!is_dir($destDir)) mkdir($destDir, 0755, true);
$destPath = "$destDir/$safeName";
// 5. 移動文件(移動比複製+刪除原子、快)
if (!move_uploaded_file($file['tmp_name'], $destPath)) {
throw new UploadException(0, '移動上傳文件失敗');
}
return $destPath; // 返回相對或絕對路徑供業務入庫
}
// 使用
try {
$path = handleUpload($_FILES['avatar'], ['image/jpeg', 'image/png', 'image/webp'], 5*1024*1024);
// 入庫 $path ...
echo json_encode(['code' => 0, 'path' => $path]);
} catch (UploadException $e) {
echo json_encode(['code' => $e->getCode(), 'msg' => $e->getMessage()]);
}
$_FILES['type']: 瀏覽器根據擴展名猜測,極易偽造。必須用 finfo 或 getimagesize() 驗證真實內容。MAX_FILE_SIZE 隱藏域無安全性: 客戶端可篡改,僅作用戶體驗提示,服務端必須以 upload_max_filesize 和代碼邏輯為準。tmp_name 文件,必須用 move_uploaded_file 移動,否則文件丟失。post_max_size > upload_max_filesize、max_execution_time、max_input_time、Nginx client_max_body_size、client_body_timeout、proxy_read_timeout。上傳看似簡單,實則涉及客戶端、Web Server、PHP、文件系統、安全策略多層交互。建立標準化異常處理流程,能覆蓋 99% 線上上傳故障排查場景。