Skip to content

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 修正
性能下降索引缺失/数据量大添加索引,优化查询
数据不一致并发冲突/事务残留使用锁机制和事务
内存溢出大数据集/未释放资源增大内存限制,分批处理

故障排除步骤

  1. 检查错误日志和异常信息
  2. 确认配置和环境是否正确
  3. 使用调试工具逐步排查
  4. 参考官方文档查找已知问题

版本兼容性说明

功能最低版本说明
基础功能PHP 8.1本文档基准版本
只读属性PHP 8.1public readonly 修饰符
枚举类型PHP 8.1enum 类型和 match 表达式
FiberPHP 8.1协程/轻量级并发
命名参数PHP 8.0foo(arg_name: value)
联合类型PHP 8.0`int
Null 安全运算符PHP 8.0$obj?->method()
析构器 promotionPHP 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');

参考链接