Skip to content

Instantly share code, notes, and snippets.

@JhowRaul10
Forked from iamntz/HandlePutFormData.php
Last active March 8, 2023 12:58
Show Gist options
  • Save JhowRaul10/cb2f9670ad0a8aa2fc32d263f948342a to your computer and use it in GitHub Desktop.
Save JhowRaul10/cb2f9670ad0a8aa2fc32d263f948342a to your computer and use it in GitHub Desktop.
Laravel: Middleware to support multipart/form-data in PUT, PATH and DELETE requests. Deals with one level of form arrays.
<?php
namespace App\Http\Middleware;
use Closure;
use Symfony\Component\HttpFoundation\ParameterBag;
use Illuminate\Support\Arr;
/**
* @author https://github.com/Stunext
*
* PHP, and by extension, Laravel does not support multipart/form-data requests when using any request method other than POST.
* This limits the ability to implement RESTful architectures. This is a middleware for Laravel 5.7 that manually decoding
* the php://input stream when the request type is PUT, DELETE or PATCH and the Content-Type header is mutlipart/form-data.
*
* The implementation is based on an example by [netcoder at stackoverflow](http://stackoverflow.com/a/9469615).
* This is necessary due to an underlying limitation of PHP, as discussed here: https://bugs.php.net/bug.php?id=55815.
*
*/
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 = array();
$data = array();
// 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 = 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);
$arr = array(
'name' => $fileName,
'type' => $headers['content-type'],
'tmp_name' => $localFileName,
'error' => 0,
'size' => filesize($localFileName)
);
if(substr($fieldName, -2, 2) == '[]') {
$fieldName = substr($fieldName, 0, strlen($fieldName)-2);
}
if(array_key_exists($fieldName, $files)) {
array_push($files[$fieldName], $arr);
} else {
$files[$fieldName] = array($arr);
}
// register a shutdown function to cleanup the temporary file
register_shutdown_function(function() use($localFileName) {
unlink($localFileName);
});
} else {
parse_str($fieldName.'=__INPUT__', $parsedInput);
$dottedInput = Arr::dot($parsedInput);
$targetInput = Arr::add([], array_keys($dottedInput)[0], $content);
$data = array_merge_recursive($data, $targetInput);
}
}
}
$fields = new ParameterBag($data);
return ["inputs" => $fields->all(), "files" => $files];
}
}
@diamondobama
Copy link

diamondobama commented May 9, 2020

This doesn't solve the file upload issue when using validation.

e.g. 'avatar' => 'image'

@diamondobama
Copy link

diamondobama commented May 9, 2020

Digging deeper, it happened that the validation is failing because of Symfony UploadedFile.php class testing file upload validity internally using PHP is_uploaded_file function in the isValid() function, and this PHP function tells whether the file was uploaded via HTTP POST. More here.

@DazDotOne
Copy link

I've been trying to figure out how to work with this issue without having to break RESTful convention and boy howdie, what a rabbit hole, let me tell you.

I'm adding this anywhere I can find in the hope that it will help somebody out in the future.

I've just lost a day of development firstly figuring out that this was an issue, then figuring out where the issue lay.

As mentioned, this isn't a symfony (or laravel, or any other framework) issue, it's a limitation of PHP.

After trawling through a good few RFCs for php core, the core development team seem somewhat resistant to implementing anything to do with modernising the handling of HTTP requests. The issue was first reported in 2011, it doesn't look any closer to having a native solution.

That said, I managed to find this PECL extension. I'm not really very familiar with pecl, and couldn't seem to get it working using pear. but I'm using CentOS and Remi PHP which has a yum package.

I ran yum install php-pecl-apfd and it literally fixed the issue straight away (well I had to restart my docker containers but that was a given).

I believe there are other packages in various flavours of linux and I'm sure anybody with more knowledge of pear/pecl/general php extensions could get it running on windows or mac with no issue.

@ericvvc9
Copy link

Digging deeper, it happened that the validation is failing because of Symfony UploadedFile.php class testing file upload validity internally using PHP is_uploaded_file function in the isValid() function, and this PHP function tells whether the file was uploaded via HTTP POST. More here.

Did you find a workaround to get this work?

@diamondobama
Copy link

Digging deeper, it happened that the validation is failing because of Symfony UploadedFile.php class testing file upload validity internally using PHP is_uploaded_file function in the isValid() function, and this PHP function tells whether the file was uploaded via HTTP POST. More here.

Did you find a workaround to get this work?

Unfortunately, not. Until the PHP team resolves that issue, no workaround for that.

@DazDotOne
Copy link

Digging deeper, it happened that the validation is failing because of Symfony UploadedFile.php class testing file upload validity internally using PHP is_uploaded_file function in the isValid() function, and this PHP function tells whether the file was uploaded via HTTP POST. More here.

Did you find a workaround to get this work?

Did you try the PECL extension? That basically populates the POST data so should fix your issue. I'd be interested to find out if that is the case.

@luizgdi
Copy link

luizgdi commented Sep 18, 2020

Hey guys! Ty for the great workaround!! But I've found one thing that I suppose that isn't intended...
When setting a file to the request, $request->image should return directly the UploadedFile, but insteads it returns an array with the UploadedFile being the index 0, I THINK this isn't the expected return, as the middleware is trying to workaround, and laravel wouldn't handle that file like that... I've solved this editing the line 97 from: $files[$fieldName] = array($arr); to $files[$fieldName] = $arr; but I didn't tested enough to tell if that would affect anything else since my application is new and still pretty small. If you can give me some feedback about that, would appreciate! And again: tyvm!

@cAstraea
Copy link

cAstraea commented Mar 8, 2023

The extension didn't fix for me. $request->allFiles() is still empty on PUT requests

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment