PUT 方法上传
概述
除了常见的 POST 方法文件上传外,PHP 还支持通过 HTTP PUT 方法上传文件。PUT 方法直接将请求体作为文件内容,适用于 RESTful API 风格的文件操作(如 WebDAV)。PHP 通过 php://input 流读取 PUT 请求的原始数据。
适用场景
- RESTful API 文件上传
- WebDAV 文件管理
- 大文件流式上传
- 客户端直接上传到服务器
基础概念
PUT vs POST 上传对比
| 特性 | POST 上传 | PUT 上传 |
|---|---|---|
| 数据来源 | $_FILES 数组 | php://input 流 |
| 元数据 | 自动解析(name, type, size) | 需自行解析(Content-Type 头) |
| 临时文件 | PHP 自动创建 | 需手动保存 |
| 安全验证 | is_uploaded_file() / move_uploaded_file() | 无内置验证 |
| 原生支持 | 完整支持 | 需要额外配置 |
| 适用协议 | HTML 表单 | RESTful API / WebDAV |
安全风险
PUT 方法上传没有 PHP 内置的安全机制(如临时文件隔离、move_uploaded_file() 验证),需要开发者自行实现所有安全检查。
语法与代码示例
基本 PUT 上传处理
php
<?php
// PUT 上传通过 php://input 读取请求体
$putData = file_get_contents('php://input');
if ($putData === false || strlen($putData) === 0) {
http_response_code(400);
exit('No data received');
}
// 从 URL 或请求头获取文件名
// 方式一:从请求路径获取
$uri = $_SERVER['REQUEST_URI'];
$filename = basename(parse_url($uri, PHP_URL_PATH));
// 方式二:从请求头获取
$filename = $_SERVER['HTTP_X_FILENAME'] ?? 'unnamed_file';
// 保存文件
$uploadDir = '/var/www/html/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$destPath = $uploadDir . $filename;
if (file_put_contents($destPath, $putData) === false) {
http_response_code(500);
exit('Failed to save file');
}
// 验证文件
$size = filesize($destPath);
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($destPath);
http_response_code(201);
echo json_encode([
'status' => 'created',
'path' => $destPath,
'size' => $size,
'mime' => $mime,
], JSON_UNESCAPED_UNICODE);流式保存大文件(避免内存溢出)
php
<?php
// 对于大文件,使用流式读取避免内存溢出
$uploadDir = '/var/www/html/uploads/';
$filename = $_SERVER['HTTP_X_FILENAME'] ?? 'upload_' . time();
$destPath = $uploadDir . $filename;
$source = fopen('php://input', 'rb');
$dest = fopen($destPath, 'wb');
if ($source === false || $dest === false) {
http_response_code(500);
exit('Failed to open streams');
}
// 流式复制(每次 8KB)
$bytesWritten = stream_copy_to_stream($source, $dest, 8192);
fclose($source);
fclose($dest);
if ($bytesWritten === false) {
http_response_code(500);
@unlink($destPath);
exit('Failed to save file');
}
echo "Saved {$bytesWritten} bytes\n";使用 curl 发送 PUT 上传
bash
# 使用 curl 发送 PUT 请求上传文件
curl -X PUT \
-H "X-Filename: document.pdf" \
--data-binary @/path/to/document.pdf \
http://example.com/upload.php
# 使用 -T 选项(自动设置文件名)
curl -T /path/to/document.pdf http://example.com/upload/php
<?php
// PHP 客户端发送 PUT 上传
function putUpload(string $filePath, string $url, string $filename = ''): array
{
if (!file_exists($filePath)) {
throw new RuntimeException("文件不存在: {$filePath}");
}
if ($filename === '') {
$filename = basename($filePath);
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_PUT => true,
CURLOPT_INFILE => fopen($filePath, 'rb'),
CURLOPT_INFILESIZE => filesize($filePath),
CURLOPT_HTTPHEADER => [
'X-Filename: ' . $filename,
'Content-Type: ' . mime_content_type($filePath),
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'status' => $httpCode,
'body' => $response,
];
}实战示例
RESTful 文件上传 API
php
<?php
declare(strict_types=1);
class PutUploadHandler
{
private string $uploadDir;
private int $maxSize;
private array $allowedMimes;
public function __construct(
string $uploadDir,
int $maxSize = 104857600, // 100MB
array $allowedMimes = []
) {
$this->uploadDir = rtrim($uploadDir, '/\\');
$this->maxSize = $maxSize;
$this->allowedMimes = $allowedMimes;
if (!is_dir($this->uploadDir)) {
mkdir($this->uploadDir, 0755, true);
}
}
public function handle(): array
{
$filename = $this->getFilename();
$destPath = $this->generateDestPath($filename);
try {
$this->saveStream($destPath);
} catch (RuntimeException $e) {
@unlink($destPath);
return ['error' => $e->getMessage()];
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($destPath);
if (!empty($this->allowedMimes) && !in_array($mime, $this->allowedMimes, true)) {
@unlink($destPath);
return ['error' => "不允许的文件类型: {$mime}"];
}
return [
'status' => 'created',
'path' => $destPath,
'name' => basename($destPath),
'size' => filesize($destPath),
'mime' => $mime,
];
}
private function getFilename(): string
{
// 优先从 URL 路径获取
$uri = $_SERVER['REQUEST_URI'] ?? '';
$basename = basename(parse_url($uri, PHP_URL_PATH));
if ($basename && $basename !== '/') {
return $basename;
}
// 从请求头获取
return $_SERVER['HTTP_X_FILENAME'] ?? ('upload_' . time());
}
private function generateDestPath(string $originalName): string
{
$ext = pathinfo($originalName, PATHINFO_EXTENSION);
$safeName = bin2hex(random_bytes(8));
$dateDir = $this->uploadDir . DIRECTORY_SEPARATOR . date('Y/m/d');
if (!is_dir($dateDir)) {
mkdir($dateDir, 0755, true);
}
return $dateDir . DIRECTORY_SEPARATOR . $safeName . '.' . $ext;
}
private function saveStream(string $destPath): void
{
$source = fopen('php://input', 'rb');
$dest = fopen($destPath, 'wb');
if ($source === false || $dest === false) {
throw new RuntimeException('无法打开文件流');
}
$totalBytes = 0;
while (!feof($source)) {
$chunk = fread($source, 8192);
if ($chunk === false) {
fclose($source);
fclose($dest);
throw new RuntimeException('读取请求体失败');
}
$totalBytes += strlen($chunk);
if ($totalBytes > $this->maxSize) {
fclose($source);
fclose($dest);
throw new RuntimeException("文件超过大小限制: {$this->maxSize}");
}
$written = fwrite($dest, $chunk);
if ($written === false) {
fclose($source);
fclose($dest);
throw new RuntimeException('写入文件失败');
}
}
fclose($source);
fclose($dest);
}
}
// 路由处理
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
header('Content-Type: application/json; charset=utf-8');
$handler = new PutUploadHandler(
'/var/www/html/uploads',
maxSize: 50 * 1024 * 1024,
allowedMimes: ['image/jpeg', 'image/png', 'application/pdf']
);
$result = $handler->handle();
$code = isset($result['error']) ? 400 : 201;
http_response_code($code);
echo json_encode($result, JSON_UNESCAPED_UNICODE);
}注意事项
Content-Length 验证
php
<?php
// 验证 Content-Length 防止超限
$contentLength = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
$maxSize = 100 * 1024 * 1024; // 100MB
if ($contentLength > $maxSize) {
http_response_code(413); // Payload Too Large
exit('File too large');
}缺少内置安全机制
php
<?php
// PUT 上传没有以下安全机制,需要手动实现:
// 1. is_uploaded_file() 检查 —— 不适用
// 2. move_uploaded_file() 安全移动 —— 不适用
// 3. $_FILES 元数据 —— 不存在
// 必须手动实现:
// - 文件大小验证
// - MIME 类型验证
// - 文件名安全处理
// - 存储路径隔离
// - 内容安全检查最佳实践
1. 优先使用 POST 上传
php
<?php
// 除非有明确需求,优先使用 POST multipart/form-data 上传
// POST 的优势:
// - 内置安全机制
// - 自动解析文件元数据
// - 临时文件隔离
// - 广泛的客户端支持
// PUT 适用场景:
// - WebDAV 协议
// - RESTful API 严格语义
// - 流式大文件上传2. 结合认证使用
php
<?php
// PUT 上传必须配合认证使用
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$token = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (!validateToken($token)) {
http_response_code(401);
exit('Unauthorized');
}
// ... 处理上传
}进阶用法
调试与测试技巧
php
<?php
declare(strict_types=1);
// 单元测试辅助函数
function createTestResource(): mixed
{
return match (true) {
default => new stdClass(),
};
}
// 调试输出函数
function debugOutput(mixed , string = ''): void
{
= ? ": " : '';
.= print_r(, true);
fwrite(STDERR, . "\n");
}
// 性能基准测试
function benchmark(callable , int = 1000): float
{
= hrtime(true);
for ($i = 0; $i < $iterations; $i++) {
$fn();
}
return (hrtime(true) - $start) / 1e9;
}日志记录实践
php
<?php
declare(strict_types=1);
/**
* 简易日志记录器
*/
class SimpleLogger
{
private string $logFile;
private string $level = 'INFO';
public function __construct(string $logFile)
{
$this->logFile = $logFile;
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
private function log(string $level, string $message, array $context): void
{
$timestamp = date('Y-m-d H:i:s');
$contextStr = $context ? ' ' . json_encode($context, JSON_UNESCAPED_UNICODE) : '';
$line = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
}
}配置与环境检测
php
<?php
declare(strict_types=1);
// 环境检测工具
class EnvironmentChecker
{
public static function checkRequirements(array $requirements): array
{
$results = [];
foreach ($requirements as $name => $check) {
$results[$name] = is_callable($check) ? $check() : false;
}
return $results;
}
public static function getSystemInfo(): array
{
return [
'php_version' => PHP_VERSION,
'os' => PHP_OS,
'sapi' => PHP_SAPI,
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time'),
'loaded_extensions' => get_loaded_extensions(),
];
}
}常见问题排查
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 连接超时 | 网络问题/配置错误 | 检查配置,增加超时时间 |
| 权限不足 | 文件/目录权限 | 使用 chmod/chown 修正 |
| 性能下降 | 索引缺失/数据量大 | 添加索引,优化查询 |
| 数据不一致 | 并发冲突/事务残留 | 使用锁机制和事务 |
| 内存溢出 | 大数据集/未释放资源 | 增大内存限制,分批处理 |
故障排除步骤
- 检查错误日志和异常信息
- 确认配置和环境是否正确
- 使用调试工具逐步排查
- 参考官方文档查找已知问题
版本兼容性说明
| 功能 | 最低版本 | 说明 |
|---|---|---|
| 基础功能 | PHP 8.1 | 本文档基准版本 |
| 只读属性 | PHP 8.1 | public readonly 修饰符 |
| 枚举类型 | PHP 8.1 | enum 类型和 match 表达式 |
| Fiber | PHP 8.1 | 协程/轻量级并发 |
| 命名参数 | PHP 8.0 | foo(arg_name: value) |
| 联合类型 | PHP 8.0 | `int |
| Null 安全运算符 | PHP 8.0 | $obj?->method() |
| 析构器 promotion | PHP 8.0 | __construct(public $x) |
php
<?php
declare(strict_types=1);
// 版本兼容性检测
function ensureVersion(string $minVersion): void
{
if (version_compare(PHP_VERSION, $minVersion, '<')) {
throw new RuntimeException(
sprintf('需要 PHP %s+, 当前版本: %s', $minVersion, PHP_VERSION)
);
}
}
ensureVersion('8.1.0');