【问题标题】:PHP multipart form data PUT request?PHP多部分表单数据PUT请求?
【发布时间】:2012-03-16 22:06:16
【问题描述】:

我正在编写一个 RESTful API。我无法使用不同的动词上传图片。

考虑:

我有一个可以通过对 URL 的 post/put/delete/get 请求创建/修改/删除/查看的对象。当有文件要上传时,请求是多部分形式,当只有文本要处理时,请求是应用程序/xml。

要处理与我正在执行的对象相关联的图像上传:

    if(isset($_FILES['userfile'])) {
        $data = $this->image_model->upload_image();
        if($data['error']){
            $this->response(array('error' => $error['error']));
        }
        $xml_data = (array)simplexml_load_string( urldecode($_POST['xml']) );           
        $object = (array)$xml_data['object'];
    } else {
        $object = $this->body('object');
    }

这里的主要问题是在尝试处理 put 请求时,显然 $_POST 不包含 put 数据(据我所知!)。

作为参考,这是我构建请求的方式:

curl -F userfile=@./image.png -F xml="<xml><object>stuff to edit</object></xml>" 
  http://example.com/object -X PUT

有人知道如何在我的 PUT 请求中访问 xml 变量吗?

【问题讨论】:

    标签: php http codeigniter rest curl


    【解决方案1】:

    首先,$_FILES 在处理 PUT 请求时不会被填充。它仅在处理 POST 请求时由 PHP 填充。

    您需要手动解析它。这也适用于“常规”字段:

    // Fetch content and determine boundary
    $raw_data = file_get_contents('php://input');
    $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
    
    // Fetch each part
    $parts = array_slice(explode($boundary, $raw_data), 1);
    $data = array();
    
    foreach ($parts as $part) {
        // If this is the last part, break
        if ($part == "--\r\n") break; 
    
        // Separate content from headers
        $part = ltrim($part, "\r\n");
        list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);
    
        // Parse the headers list
        $raw_headers = explode("\r\n", $raw_headers);
        $headers = array();
        foreach ($raw_headers as $header) {
            list($name, $value) = explode(':', $header);
            $headers[strtolower($name)] = ltrim($value, ' '); 
        } 
    
        // Parse the Content-Disposition to get the field name, etc.
        if (isset($headers['content-disposition'])) {
            $filename = null;
            preg_match(
                '/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/', 
                $headers['content-disposition'], 
                $matches
            );
            list(, $type, $name) = $matches;
            isset($matches[4]) and $filename = $matches[4]; 
    
            // handle your fields here
            switch ($name) {
                // this is a file upload
                case 'userfile':
                     file_put_contents($filename, $body);
                     break;
    
                // default for all other files is to populate $data
                default: 
                     $data[$name] = substr($body, 0, strlen($body) - 2);
                     break;
            } 
        }
    
    }
    

    在每次迭代中,$data 数组将填充您的参数,$headers 数组将填充每个部分的标题(例如:Content-Type 等),$filename 将包含原始文件名(如果在请求中提供且适用于该字段)。

    请注意,上述内容仅适用于 multipart 内容类型。在使用上述内容解析正文之前,请务必检查请求 Content-Type 标头。

    【讨论】:

    • 谢谢,这更有帮助:)
    • "首先,处理 PUT 请求时不会填充 $_FILES。它仅在处理 POST 请求时由 PHP 填充。"找不到这方面的文档,请您指出正确的方向吗?
    • @M.Ang.:Here:“PHP 还支持 Netscape Composer 和 W3C 的 Amaya 客户端使用的 PUT 方法文件上传。有关详细信息,请参阅 PUT Method Support。”
    【解决方案2】:

    请不要再删了,对大部分来这里的人都有帮助!之前的所有答案都是部分答案,并未涵盖大多数提出此问题的人想要的解决方案。

    这采用了上面所说的内容,另外还处理了多个文件上传,并将它们放在 $_FILES 中,正如人们所期望的那样。要使其正常工作,您必须根据Documentation 将“Script PUT /put.php”添加到项目的虚拟主机中。我还怀疑我必须设置一个 cron 来清理任何“.tmp”文件。

    private function _parsePut(  )
    {
        global $_PUT;
    
        /* PUT data comes in on the stdin stream */
        $putdata = fopen("php://input", "r");
    
        /* Open a file for writing */
        // $fp = fopen("myputfile.ext", "w");
    
        $raw_data = '';
    
        /* Read the data 1 KB at a time
           and write to the file */
        while ($chunk = fread($putdata, 1024))
            $raw_data .= $chunk;
    
        /* Close the streams */
        fclose($putdata);
    
        // Fetch content and determine boundary
        $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
    
        if(empty($boundary)){
            parse_str($raw_data,$data);
            $GLOBALS[ '_PUT' ] = $data;
            return;
        }
    
        // Fetch each part
        $parts = array_slice(explode($boundary, $raw_data), 1);
        $data = array();
    
        foreach ($parts as $part) {
            // If this is the last part, break
            if ($part == "--\r\n") break;
    
            // Separate content from headers
            $part = ltrim($part, "\r\n");
            list($raw_headers, $body) = explode("\r\n\r\n", $part, 2);
    
            // Parse the headers list
            $raw_headers = explode("\r\n", $raw_headers);
            $headers = array();
            foreach ($raw_headers as $header) {
                list($name, $value) = explode(':', $header);
                $headers[strtolower($name)] = ltrim($value, ' ');
            }
    
            // Parse the Content-Disposition to get the field name, etc.
            if (isset($headers['content-disposition'])) {
                $filename = null;
                $tmp_name = null;
                preg_match(
                    '/^(.+); *name="([^"]+)"(; *filename="([^"]+)")?/',
                    $headers['content-disposition'],
                    $matches
                );
                list(, $type, $name) = $matches;
    
                //Parse File
                if( isset($matches[4]) )
                {
                    //if labeled the same as previous, skip
                    if( isset( $_FILES[ $matches[ 2 ] ] ) )
                    {
                        continue;
                    }
    
                    //get filename
                    $filename = $matches[4];
    
                    //get tmp name
                    $filename_parts = pathinfo( $filename );
                    $tmp_name = tempnam( ini_get('upload_tmp_dir'), $filename_parts['filename']);
    
                    //populate $_FILES with information, size may be off in multibyte situation
                    $_FILES[ $matches[ 2 ] ] = array(
                        'error'=>0,
                        'name'=>$filename,
                        'tmp_name'=>$tmp_name,
                        'size'=>strlen( $body ),
                        'type'=>$value
                    );
    
                    //place in temporary directory
                    file_put_contents($tmp_name, $body);
                }
                //Parse Field
                else
                {
                    $data[$name] = substr($body, 0, strlen($body) - 2);
                }
            }
    
        }
        $GLOBALS[ '_PUT' ] = $data;
        return;
    }
    

    【讨论】:

    • 看起来这是唯一接近完整解决方案的答案。我不确定我对假装$_FILES 和$_PUT 的感觉如何,但它确实很好用。谢谢!
    • 这正是我所需要的!
    • 如何在 Laravel 请求中使用它? “照亮\Http\请求”
    • 太棒了!基本上,GreenDot 编写了对 PUT 请求的真正支持,将其转换为 PHP 开发人员已经习惯使用 POST 和 GET 的相同接口。非常感谢,谢谢!
    • 非常有用,谢谢!此外,如果您想支持 JSON 格式的原始数据,在 fclose($putdata); 之后,您可以添加如下内容:$json_data = json_decode($raw_data, true); if(json_last_error() === JSON_ERROR_NONE){ $GLOBALS[ '_PUT' ] = $data; return; }
    【解决方案3】:

    对于使用 Apiato (Laravel) 框架的人: 在下面创建类似文件的新中间件,然后在受保护的 $middlewareGroups 变量(在 web 或 api 中,无论你想要什么)中的 laravel 内核文件中声明这个文件,如下所示:

    protected $middlewareGroups = [
        'web' => [],
        'api' => [HandlePutFormData::class],
    ];
    

    <?php
    
    namespace App\Ship\Middlewares\Http;
    
    use Closure;
    use Symfony\Component\HttpFoundation\ParameterBag;
    
    /**
     * @author Quang Pham
     */
    class HandlePutFormData
    {
        /**
         * Handle an incoming request.
         *
         * @param  \Illuminate\Http\Request $request
         * @param  \Closure                 $next
         *
         * @return mixed
         */
        public function handle($request, Closure $next)
        {
            if ($request->method() == 'POST' or $request->method() == 'GET') {
                return $next($request);
            }
            if (preg_match('/multipart\/form-data/', $request->headers->get('Content-Type')) or
                preg_match('/multipart\/form-data/', $request->headers->get('content-type'))) {
                $parameters = $this->decode();
    
                $request->merge($parameters['inputs']);
                $request->files->add($parameters['files']);
            }
    
            return $next($request);
        }
    
        public function decode()
        {
            $files = [];
            $data  = [];
            // Fetch content and determine boundary
            $rawData  = file_get_contents('php://input');
            $boundary = substr($rawData, 0, strpos($rawData, "\r\n"));
            // Fetch and process each part
            $parts = $rawData ? array_slice(explode($boundary, $rawData), 1) : [];
            foreach ($parts as $part) {
                // If this is the last part, break
                if ($part == "--\r\n") {
                    break;
                }
                // Separate content from headers
                $part = ltrim($part, "\r\n");
                list($rawHeaders, $content) = explode("\r\n\r\n", $part, 2);
                $content = substr($content, 0, strlen($content) - 2);
                // Parse the headers list
                $rawHeaders = explode("\r\n", $rawHeaders);
                $headers    = array();
                foreach ($rawHeaders as $header) {
                    list($name, $value) = explode(':', $header);
                    $headers[strtolower($name)] = ltrim($value, ' ');
                }
                // Parse the Content-Disposition to get the field name, etc.
                if (isset($headers['content-disposition'])) {
                    $filename = null;
                    preg_match(
                        '/^form-data; *name="([^"]+)"(; *filename="([^"]+)")?/',
                        $headers['content-disposition'],
                        $matches
                    );
                    $fieldName = $matches[1];
                    $fileName  = (isset($matches[3]) ? $matches[3] : null);
                    // If we have a file, save it. Otherwise, save the data.
                    if ($fileName !== null) {
                        $localFileName = tempnam(sys_get_temp_dir(), 'sfy');
                        file_put_contents($localFileName, $content);
                        $files = $this->transformData($files, $fieldName, [
                            'name'     => $fileName,
                            'type'     => $headers['content-type'],
                            'tmp_name' => $localFileName,
                            'error'    => 0,
                            'size'     => filesize($localFileName)
                        ]);
                        // register a shutdown function to cleanup the temporary file
                        register_shutdown_function(function () use ($localFileName) {
                            unlink($localFileName);
                        });
                    } else {
                        $data = $this->transformData($data, $fieldName, $content);
                    }
                }
            }
            $fields = new ParameterBag($data);
    
            return ["inputs" => $fields->all(), "files" => $files];
        }
    
        private function transformData($data, $name, $value)
        {
            $isArray = strpos($name, '[]');
            if ($isArray && (($isArray + 2) == strlen($name))) {
                $name = str_replace('[]', '', $name);
                $data[$name][]= $value;
            } else {
                $data[$name] = $value;
            }
            return $data;
        }
    }
    

    请注意:以上代码并非全部是我的,有些来自上面的评论,有些是我修改的。

    【讨论】:

    • 工作。谢谢@Pham Quang
    【解决方案4】:

    引用 netcoder 回复:“请注意,上述内容仅适用于多部分内容类型”

    为了处理任何内容类型,我在 netcoder 先生的解决方案中添加了以下几行:

       // Fetch content and determine boundary
       $raw_data = file_get_contents('php://input');
       $boundary = substr($raw_data, 0, strpos($raw_data, "\r\n"));
    
       /*...... My edit --------- */
        if(empty($boundary)){
            parse_str($raw_data,$data);
            return $data;
        }
       /* ........... My edit ends ......... */
        // Fetch each part
        $parts = array_slice(explode($boundary, $raw_data), 1);
        $data = array();
        ............
        ...............  
    

    【讨论】:

    • @Greg 我在添加解决方案时没有编辑权限。如果我在 netcoder 的线程中添加我的代码作为注释,那么代码将不可读。为什么是-1?我没有试图帮助别人吗?
    【解决方案5】:

    我一直在试图弄清楚如何在不违反 RESTful 约定的情况下解决这个问题,男孩你好,让我告诉你,真是个兔子洞。

    我将其添加到任何我能找到的地方,希望它对将来的人有所帮助。

    我刚刚浪费了一天的开发时间,首先弄清楚这是一个问题,然后弄清楚问题出在哪里。

    如前所述,这不是 symfony(或 laravel 或任何其他框架)的问题,而是 PHP 的限制。

    在浏览了几个关于 php 核心的 RFC 之后,核心开发团队似乎有点反对实施与现代化 HTTP 请求处理有关的任何事情。该问题于 2011 年首次报告,看起来与拥有原生解决方案没有任何关系。

    也就是说,我设法找到了名为“始终填充表单数据”的 this PECL extension。我对 pecl 不是很熟悉,而且似乎无法使用 pear 让它工作。但我使用的是 CentOS 和 Remi PHP,它们有一个 yum 包。

    我运行了yum install php-pecl-apfd,它立即解决了这个问题(好吧,我不得不重新启动我的 docker 容器,但这是给定的)。

    我相信在各种风格的 linux 中还有其他包,我相信任何对 pear/pecl/general php 扩展有更多了解的人都可以让它在 windows 或 mac 上运行而没有问题。

    【讨论】:

    • 这个解决方案在 CakePHP 中适用于我。对于 Laravel,有一个解决方法。是一个可以用这个扩展解决的 PHP 问题。
    猜你喜欢
    • 2012-11-27
    • 2016-12-17
    • 2016-11-11
    • 2013-02-12
    • 2018-07-02
    • 1970-01-01
    • 2017-10-05
    • 1970-01-01
    相关资源
    最近更新 更多