我正在编写一个RESTful API。我使用不同的动词上传图片时遇到了麻烦。
考虑:
我有一个可以通过post / put / delete / get请求创建/修改/删除/查看的对象。当有文件要上传时,请求是多部分形式,或者当只有要处理的文本时,请求是application / 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
变量?
答案 0 :(得分:26)
首先,处理PUT请求时未填充$_FILES
。在处理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
标题。
答案 1 :(得分:10)
请不要再删除它,这对大多数人来说都很有帮助!之前的所有答案都是部分答案,并未涵盖解决方案,因为大多数人都会问这个问题。
这将采用上述内容并另外处理多个文件上传,并按照人们的预期将它们放在$ _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;
}
答案 2 :(得分:0)
引用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();
............
...............
答案 3 :(得分:0)
使用Apiato(Laravel)框架的用户: 在下面创建类似文件的新中间件,然后在laravel内核文件的受保护的$ middlewareGroups变量(在Web或api中,任何您想要的)内将该文件解码:
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;
}
}
请注意:这些代码并非全部属于我的代码,有些来自上述注释,有些是我修改的。
答案 4 :(得分:0)
我一直在尝试找出如何解决此问题的方法,而不必打破RESTful约定和男孩方法,这真是个兔子漏洞,让我告诉你。
我将其添加到我能找到的任何地方,以希望将来对某人有帮助。
我刚刚失去了发展的一天,首先搞清楚,这是一个问题,然后找出其中的问题所在。
如上所述,这不是symfony(或laravel,或任何其他框架)问题,而是PHP的局限性。
在为php内核浏览了很多RFC之后,核心开发团队似乎有点抵制实现与现代化HTTP请求处理有关的任何事情。该问题最早是在2011年报告的,它看起来似乎离本机解决方案还差得远。
也就是说,我设法找到了this PECL extension,称为“始终填充表单数据”。我对pecl并不是很熟悉,而且似乎无法使用pear来工作。但是我正在使用具有yum软件包的CentOS和Remi PHP。
我运行了yum install php-pecl-apfd
,从字面上解决了这个问题(我不得不重新启动Docker容器,但这是给定的。)
我相信还有其他各种Linux风格的软件包,我相信任何对pear / pecl / general php扩展有更多了解的人都可以使其在Windows或Mac上正常运行。